JVM笔记11-类加载器和OSGI
一.JVM 类加载器:
一个类在使用前,如何通过类调用静态字段,静态方法,或者new一个实例对象,第一步就是需要类加载,然后是连接和初始化,最后才能使用。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialzation)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking),这 7 个阶段的发生顺序如下图所示:
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里笔者写的是按部就班地 “开始”,而不是按部就班地 “进行” 或 “完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
什么情况下需要开始类加载过程的第一个阶段:加载?Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行 “初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个类。
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结构 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。
对于这 5 种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
类加载器就是将 .java 代码文件编译成 .class 字节码文件后,Java虚拟机的类加载器通过读取此类的二进制流,转换成目标类的实例。
除了Java会生成字节码外,运行在JVM上的JRuby,Scala,Groovy同样需要编译成对应的 .class 文件,这里列举了四种不同的字节码,不单是Java才生成字节码文件。
常用的类加载器有4种:
1.Bootstrap ClassLoader:启动类加载器,加载JAVA_HOME/lib
目录下的类。如下图选中的就是
2.ExtClassLoader:扩张类加载器,加载JAVA_HOME/lib/ext
目录下的类。
3.AppClassLoader:应用程序类加载器,加载用户指定的classpath(存放 src 目录 Java 文件编译之后的 class 文件和 xml、properties 等资源配置文件的 src/main/webapp/WEB-INF/classes 目录)下的类
4.UserClassLoader:用户自定义的类加载器(只要继承 ClassLoader并实现 findClass(String name) 方法),自定义加载路径。
类加载时并不需要等到某个类被首次主动使用时再加载它,JVM类加载器会在预料某个类要使用时预先加载。双亲委派模型,如下图:
Java 类加载基于双亲委派模型——当有类加载请求时,从下往上检查类是否被加载,如果没被加载,UserClassLoader 就委托父类 AppClassLoader 加载,AppClassLoader 继续委托其父类 ExtClassLoader 加载,接着分派给 Bootstrap ClasssLoader 加载;
如果无法加载就返回到发起加载请求的类加载一直到由最开始发起加载请求的 UserClassLoader 加载,所有类最终都会去到顶层。Bootstrap ClasssLoader 开始加载,无法加载就返回子加载器处理,一直到最开始的加载器。
这样子,就算用户自定义了 java.lang.Object 类和系统的 java.lang.Object 类重复,也不会被加载,下面我们就来自定义自己的类加载器。
/** * Created by cong on 2018/8/2. */ public class MyClassLoader extends ClassLoader { public MyClassLoader() { super(); } public MyClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // do something // 自己先不加载,先让父类加载 return super.findClass(name); } public static void main(String[] args) throws ClassNotFoundException { MyClassLoader myLoader = new MyClassLoader(); // 打印当前类路径 System.out.println(System.getProperty("java.class.path")); // ClassPath路径下并不存在Demo.class类,故抛出异常 System.out.println(myLoader.loadClass("Demo").getClassLoader().getClass().getName()); } }
运行结果如下:
学习自定义类加载器后,我们看下源码里双亲委派模型是怎么加载类的。源码如下:
public abstract class ClassLoader { private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先检查类是否已经被加载 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果存在父类加载器,就委托给父类加载器加载 if (parent != null) { c = parent.loadClass(name, false); } else { //如果不存在父类加载器,就委托给顶层的启动类加载器加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException异常被抛出则表明父类加载器加载失败 } if (c == null) { // 如果父类无法加载,就自己加载 long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } }
我们看到上面 loadClass 类里有同步代码块 synchronized (getClassLoadingLock(name)),而在 JDK1.6 之前是方法 protected synchronized Class<?> loadClass(String name, boolean resolve)
上锁,锁住方法当前对象。
这就导致一个问题,当 A 包依赖 B 包,A 在自己的类加载器的 loadClass 方法中,最终调用到 B 的类加载器的 loadClass 方法。A 先锁住自己的类加载器,然后去申请 B 的类加载器的锁,当 B 也依赖 A 包时,B 加载 A 的包时,过程相反,在多线程下,就容易产生死锁。如果类加载器是单线程运行就会安全,但效率会很低 同步代码块 synchronized (getClassLoadingLock(name)) 锁住的是一个特定对象。
private final ConcurrentHashMap<String, Object> parallelLockMap; protected Object getClassLoadingLock(String className) { Object lock = this; // parallelLockMap是一个ConcurrentHashMap if (parallelLockMap != null) { // 锁对象 Object newLock = new Object(); // putIfAbsent(K, V)方法查看K(className)和V(newLock)是否相互对应, // 是的就返回V(newLock),否则返回null // 每个className关联一个锁,并将这个锁返回,缩小了锁定粒度了,只要类名不同,就会匹配不同的锁, // 就是并行加载,类似ConcurrentHashMap里面的分段锁, // 不锁住整个Map,而是锁住一个Segment,每次只需要对Segment上锁或解锁,以空间换时间 lock = parallelLockMap.putIfAbsent(className, newLock); if (lock == null) { // 创建一个新锁对象 lock = newLock; } } return lock;
通过并行加载,可以提升加载效率,然后讲下类加载的面试题,在 Java 反射中 Class.forName() 加载类和使用 ClassLoader 加载类是不一样的。例子如下:
/** * Created by cong on 2018/8/5. */ public class MyCase { static { System.out.println("执行了静态代码块"); } private static String field = methodCheck(); public static String methodCheck() { System.out.println("执行了静态代方法"); return "给静态变量赋值"; } }
----------------------------------------------------
/** * Created by cong on 2018/8/5. */ public class DemoTest { public static void main(String[] args) { try { System.out.println("Class.forName开始执行:"); //hjc是包名 Class.forName("hjc.MyCase"); System.out.println("ClassLoader开始执行:"); ClassLoader.getSystemClassLoader().loadClass("hjc.MyCase"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
运行结果如下:
Class.forName 是加载 MyCase 类并完成初始化,给静态代码块和静态变量赋值,而 ClassLoader 只是将类加载进 JVM 虚拟机,并没有初始化。
接下来我们进入Class.forName的源码探究,源码如下:
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { return forName0(className, true, ClassLoader.getClassLoader(Reflection.getCallerClass())); }
Class.forName 底层也是调用了 ClassLoader,只是第二个参数为 true,即加载类并初始化,默认就会初始化类,JDBC 连接就是用 Class.forName 加载驱动。所以注册连接驱动会在静态代码块执行,Sprng 里的 IOC 是通过 ClassLoader 来产生,可以控制 Bean 的延迟加载(首次使用才创建)。
二.OSGI 实战:
为了实现代码热替换,模块化和动态化,就像鼠标一样即插即用,双亲委派这种树状的加载器就难以胜任,于是出现了 OSGI 加载模型,OSGI 里每个程序模块(Bundle,就是普通的 jar 包, 只是加入了特殊的头信息,是最小的部署模块)都会有自己的类加载器,当需要更换程序时,就连同 Bundle 和类加载器一起替换,是一种网状的加载模型,Bundle 间互相委托加载,并不是层次化的。
Java 类加载机制的隔离是通过不同类加载器加载指定目录来实现的,类加载的共享机制是通过双亲委派模型来实现,而 OSGI 实现隔离靠的是每个 Bundle 都自带一个独立的类加载器 ClassLoader。
OSGI 加载 Bundle 模块的顺序
- 首先检查包名是否以 java.* 开头,或者是否在一个特定的配置文件(org.osgi.framework.bootdelegation)中定义。如果是,则 bundle 类加载器立即委托给父类加载器(通常是 Application 类加载器),如果不是则进入 2
- 检查是否在 Import-Package、Require-Bundle 委派列表里,如果是委托给对应 Bundle 类加载器,如果不是,进入 3
- 检查是否在当前 Bundle 的 Classpath 里,如果是使用自己的类加载器加载,如果不是,进入 4
- 搜索可能附加在当前 bundle 上的 fragment 中的内部类,找到则委派给 Fragment bundle 类加载器加载,如果找不到,进入 5
- 查找动态导入列表里的 Bundle,委派给对应的类加载器加载,否则类加载失败
如果用 Java 的结构的项目去部署,当项目复杂度提升时,每次上线,代码只是增加或者修改了部分功能,但都得关掉服务,重新部署所有的代码和配置,管理沟通成本都很高,很容产生线上事故,而 OSGI 的应用是一个模块化的系统,避免了部署时 jar 或 classpath 错综复杂依赖管理,发布应用和更新应用都很强大,可以热替换特定的 Bundle 模块,提高部署可靠性。
接下来我们用IDE创建一个OSGI应用,首先要去 http://download.eclipse.org/equinox/ 下载最新的OSGI 框架enquinox
创建一个 OSGI 应用。打开 Eclipse,File->New->Project:
选择 OSGI 框架 Equniox(Eclipse 强大的插件机制就是构建于 OSGI Bundle 之上,Eclipse 本身就包含了 Equniox) :
接下来,勾选创建 Activator 类,新建一个创Activator 类,每个 Bundle 启动时都会调用 Bundle(模块)里 Activator(类)的 start 方法,停止时调用 stop 方法,代码如下:
import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; /** * Created by cong on 2018/8/5. */ public class Activator implements BundleActivator { private static BundleContext context; static BundleContext getContext() { return context; } public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; //添加输出This is OSGI Projcect System.out.println("This is OSGI Projcect"); } public void stop(BundleContext bundleContext) throws Exception { Activator.context = null; } }
接下来进行一下配置,Run->Run Configuration-> 双击 OSGI Framework 生成项目配置:如下图:
然后点击运行按钮,可以看到控制台输出 This is OSGI Projcect。在控制台我们输入 ss ( short status) 查看服务状态:
This is OSGI Projcect osgi> ss "Framework is launched." id State Bundle 0 ACTIVE org.eclipse.osgi_3.12.100.v20180210-1608 1 ACTIVE org.apache.felix.gogo.runtime_0.10.0.v201209301036 2 ACTIVE org.apache.felix.gogo.command_0.10.0.v201209301215 // ACTIVE表明 com.osgi.bundle.demo Bundle运行中 3 ACTIVE com.osgi.bundle.demo_1.0.0.qualifier 4 ACTIVE org.apache.felix.gogo.shell_0.10.0.v201212101605 5 ACTIVE org.eclipse.equinox.console_1.1.300.v20170512-2111 // 停止 com.osgi.bundle.demo Bundle osgi> stop com.osgi.bundle.demo osgi> ss "Framework is launched." id State Bundle 0 ACTIVE org.eclipse.osgi_3.12.100.v20180210-1608 1 ACTIVE org.apache.felix.gogo.runtime_0.10.0.v201209301036 2 ACTIVE org.apache.felix.gogo.command_0.10.0.v201209301215 // RESOLVED 表明 Bundle com.osgi.bundle.demo 停止了 3 RESOLVED com.osgi.bundle.demo_1.0.0.qualifier 4 ACTIVE org.apache.felix.gogo.shell_0.10.0.v201212101605 5 ACTIVE org.eclipse.equinox.console_1.1.300.v20170512-2111 // 通过close关闭整个应用框架 osgi> close Really want to stop Equinox? (y/n; default=y) y osgi>
一个 Bundle 包含 MANIFEST.MF,也就是 Bundle 的头信息,Java 代码以及配置文件(XML,Properties),其中 MANIFEST.MF 包含了下面的信息。如下所示:
/*版本号*/ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 /*名字*/ Bundle-Name: Demo Bundle-SymbolicName: com.osgi.bundle.demo Bundle-Version: 1.0.0.qualifier /*Bundle类*/ Bundle-Activator: com.osgi.bundle.demo.Activator Bundle-Vendor: OSGI /*依赖环境*/ Bundle-RequiredExecutionEnvironment: JavaSE-1.7 /*导入的包*/ Import-Package: org.osgi.framework;version="1.3.0" Bundle-ActivationPolicy: lazy
Equinox OSGi 命令列表
1.控制框架
1.launch 启动框架
2.shutdown 停止框架
3.close 关闭、退出框架
4.exit 立即退出,相当于 System.exit
5.init 卸载所有 bundle(前提是已经 shutdown)
6.setprop 设置属性,在运行时进行
2.控制 Bundle
1.Install 安装 uninstall 卸载
2.Stop 停止
3.Refresh 刷新
4.Update 更新
3.展示状态
1.Status 展示安装的 bundle 和注册的服务
2.Ss 展示所有 bundle 的简单状态
3.Services 展示注册服务的详细信息
4.Packages 展示导入、导出包的状态
5.Bundles 展示所有已经安装的 bundles 的状态
6.Headers 展示 bundles 的头信息,即 MANIFEST.MF 中的内容
7.Log 展示 LOG 入口信息
4.其他
Exec 在另外一个进程中执行一个命令(阻塞状态)
1.Fork 和 EXEC 不同的是不会引起阻塞
2.Gc 促使垃圾回收
3.Getprop 得到属性,或者某个属性
5.控制启动级别
1.Sl 得到某个 bundle 或者整个框架的 start level 信息
2.Setfwsl 设置框架的 start level
3.Setbsl 设置 bundle 的 start level
4.setibsl 设置初始化 bundle 的 start level