虚拟机类加载
类加载的生命周期
加载、验证、准备、解析、初始化、使用和卸载
其中验证、准备、解析3个部分统称为连接。
加载、验证、准备、初始化和卸载顺序是确定的的.
5种情况必须立即对类进行“初始化”,且“有且只有”
①遇到new,getstatic,putstatic或invokestatic。生成这四条指令常见java代码场景:使用new创建对象,读取或者设置一个类的静态字段(final修饰实现在编译器,所以除外),调用一个类的静态方法
②使用java.lang.reflect包反射调用
③初始化一个类时,父类未初始化,会先初始化父类
④虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
⑤当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic/REF_putStatic/REF_invokeStatic的方法句柄,这个方法句柄类未初始化时
所有引用类的方式都不会触发初始化化,称为被动引用。
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args){ System.out.println(SubClass.value); System.out.println("-------------------"); SuperClass[] sca = new SuperClass[10]; System.out.println("-------------------"); System.out.println(ConstClass.HELLOWORLD);
/*SuperClass init!
123
-------------------
-------------------
hello world*/
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化。
建立数组并不会初始化类对象,而是newarray创建数组,
final修饰的static编译期优化,不会初始化
当一个类在初始化时,要求其父类全部已初始化,但一个接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化。
加载
在加载阶段,虚拟机需要完成以下3件事情:
①通过一个类的全限定名来获取定义此类的二进制字节流
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③在内存中生成代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接的第一步,这一阶段目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。
①文件格式验证 魔数、版本、常量池常量类型
②元数据验证 是否有父类、父类是否final
③字节码验证 保证任意时刻操作数栈数据类型与指令代码序列都能配合工作
④符号引用验证 private、字段全限定名是否能找到对应的类
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这里所说的初始值“通常情况下”是数据类型的零值,private static int value = 123;初始值是0而不是123.
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号指向目标,与内存无关;
直接引用直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄
虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic这16个
用于操作符号引用的字节码指令之前,县对它们所使用的符号引用进行解析。
除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符7类
符号引用,分别对应常量池的Class_info/fieldref_info、Methodref_info/interfaceMerhodrefInfo/methodTyoe_info/methodHandle_info/invokeDynamic_info7种常量类型。
初始化
初始化阶段是执行类构造器clinit()方法的
clinit方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序决定的,静态与语句块中只能访问到定义在静态语句块之前的变量,定义在之后的变量可以赋值,但是不能访问
clinit方法与类的构造函数不同,他不需要显示的调用父类的构造器,虚拟机会保证在子类clinit执行前,父类clinit方法已经执行完毕。Object类第一个被执行
先执行父类clinit后执行子类clinit
clinit方法对于类或接口并不是必需的
虚拟机会保证一个类的clinit方法在多线程环境中被正确的加锁、同步。
类加载器
同一个类加载器加载是两个类比较的前提。
类加载器分类:
启动类加载器 jdk/lib/或者-Xbootclasspath指定路径,并且虚拟机识别的(文件名)类库
扩展类加载器 jdk/jre/lib/ext或者java.ext.dirs指定路径所有类库
应用程序类加载器 classPath:
自定义加载器
双亲委派模型:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
双亲委派模型是在jdk1.2期间引入的
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每个层次的类加载都是如此,因此所有加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈无法完成这个加载请求时,子加载器才会尝试自己去加载。
实现双亲委派模式的代码都集中在java.lang.ClassLoader的loadClass()方法中
检查是否加载过-->未加载,调用父类加载器的loadClass()-->未找到,调用自己的findClass
以下是jdk1.8ClassLoader抽象类中loadClass方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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 thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
破坏类加载的双亲委派模型
1.由于双亲委派模型是JDK1.2期间加入的,所以JDK1.2之前已经存在的java.lang.ClassLoader(JDK1.0)不存在双亲委派模型的逻辑,
JDK1.2为了向前兼容修改了loadClass方法加入了双亲委派逻辑,多加protected方法findClass,后续自定义类加载器只需要重写findClass就可以符合双亲委派模型,但重写loadClass方法依然可能会打破双亲委派模型。
2.由于双亲委派模型自身缺陷导致,双亲委派模型很好的解决了基础类的加载问题(越基础的类由越上层的类加载器进行加载),但当基础类需要调回用户的代码时就需要破坏双亲委派模型,
例如JDBC服务在jdk/lib/rt.jar中,是由启动类加载器BootstrapClassLoader加载的,
当使用JNDI指定JDBC的DriverManager中的Driver实例(mysql,oracle等)时,根据类加载规则Driver会由当前类的类加载器加载即BootstrapClassLoader加载,BootstrapClassLoader只会加载jdk/lib下的基础类,根本找不到Driver对应的二进制class字节流,导致类加载失败。
为了解决这个问题,Java设计团队引入了线程上下文类加载器,在线程Thread类中引入ClassLoader变量。默认设置为加载应用程序的类加载器即ApplicationClassloader,实现了高层类加载器请求低层类加载器来加载应用程序ClassPath下的类。
所有涉及SPI加载动作的基本都是采用这种方式如:JNDL,JDBC,JCE,JAXB与JBI等
![](https://img2018.cnblogs.com/i-beta/1334786/201912/1334786-20191230171433524-1731804879.png)
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;//BootstrapClassLoader时caller.getClassLoader == null synchronized(DriverManager.class) { // synchronize loading of the correct classloader. if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader();//通过线程上下文类加载器来加载Driver,而不是通过当前类的类加载器加载 } } if(url == null) { throw new SQLException("The url cannot be null", "08001"); }
3.热部署:典型的应用OSGI
在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构
![](https://img2018.cnblogs.com/blog/1334786/201902/1334786-20190219184148758-257242650.png)
并行加载器
在jdk1.6前类加载器的ClassLoader.loadClass方法是synchronized,所以在OSGI中BundleA调用BundleB的类加载器,BundleB调用BundleA的加载,会产生死锁
jdk1.7开始,将synchronized提入loadClass方法内,锁住跟className对应的一个对象。
添加一个并行加载器注册器,当className有注册,返回一个对应的Object对象,无注册返回null,即synchronized无效化。
注册器是一个ConcurrentHashMap<String, Object> parallelLockMap
需要建立的ClassLoader子类及父类类加载器先注册,否则无效
类加载案例
一、主流的JavaWeb服务器如Tomcat、Jetty、WebLogic、WebSphere等都都定义了自己的类加载器。
一个功能健全的Web服务器,要解决以下问题
1.部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现互相隔离。
2.部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。
3.服务器需要尽可能的保证自身的安全不受部署的Web应用程序的影响。
4.支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。
因此,部署Web应用时,单独的ClassPath就无法满足需求了,所有Web服务器都提供了好几个ClassPath路径提供用户存放第三方类库如“lib”“classes”等
每个ClassPath都会有一个相应的自定义类加载器去加载,以Tomcat为例
/common/*:可被Tomcat及所有Web应用程序共同使用
/server/*:可被Tomcat使用,对Web应用程序透明
/shared/*:可被所有Web应用程序共同使用,但对Tomcat透明
/WEB-INF/*:仅被当前Web应用程序使用
对应的双亲委派模如下,各类加载器分别对应上面ClassPath(6.x后默认使用Common加载,后两个需要在catalina.properties中配置server.loader、share.loader)
其中WebApp类加载器和Jsp类加载器通常会存在多个实例
一个Web应用程序对应一个WebApp类加载器
一个JSP文件对应一个Jsp类加载器
问题:如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享,用户程序放在WEB-INF中,那么CommonClassLoader加载的Spring是如何访问WebAppClassLoader加载的用户应用程序的?
第二种破坏双亲委派模型的方法,采用线程上下文类加载器实现高层类加载器请求底层类加载器加载WEB-INF中的应用类。
二、OSGI:灵活的类加载架构
OSGI类加载查找规则:网状结构不再是双亲委派模型的树形结构,Bundle有自己的类加载器,与其他Bundle的类加载器是平级的
①将以java.*开头的类为派给父类加载器加载
②将委派列表名单内的类委派给父类加载器加载
③将import列表中的类委派给Export这个类的Bundle的类加载器加载
④查找当前Bundle的classpath,使用自己的类加载器加载
⑤查找类是否在自己的FragmentBundle中,如果在,则委派给FragmentBundle的类加载器加载
⑥查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
⑦未加载成功,类查找失败。
③④⑤⑥是平级的类加载器
如:
BundleA:声明发布了packageA,依赖了java.*的包
BundleB:声明依赖了packageA和packageC,同时也依赖了java.*的包
BundleC:声明发布了packageC,依赖了packageA
OSGI在提供强大功能的同时,也引入了额外的复杂度,带来了线程死锁和内存泄漏的风险。
线程死锁:由于是网状结构,BundleA依赖BundleB,BundleB依赖BundleA,而且类加载方法loadClass是加锁的(synchronized)的,容易发生死锁
内存泄漏:由于是网状结构,循环依赖可能导致GC时,无用的Class不能被内存回收导致内存泄露
JDK1.7中,为非树状集成关系下的类加载架构进行了一次专门的升级,目的是从底层避免这类死锁出现的可能。