虚拟机类加载机制
why ? when ? how ? what ?
什么是虚拟机类加载机制 ?
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用 Java 类型。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期如下图:
加载、验证、准备、初始化、卸载这 5 个阶段的顺序是确定的,解析阶段不确定可以在初始化阶段之后开始。
必须立即初始化类的5种情况
- 遇到 new,getstatic,putstatic 和 invokestatic 这 4 条指令时,如果类没有初始化时,必须初始化类。四条指令对应我们日常所见的,使用 new 关键字实例化对象,读取一个类的静态字段,设置一个类的静态字段(被final修饰的静态字段除外,因为已在编译期把结果放入常量池中了)和调用一个类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个子类时,发现其父类没有初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,一个类包含main()方法时,当前类需要初始化
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后解析结果REF_putStatic,REF_getStatic,REF_invokeStatic的方法句柄时,并且这个方法句柄所对应的类没有初始化时,需要初始化该类。
这 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
通过子类应用父类静态字段,不会导致子类初始化
/**
* 通过子类引用父类的静态字段,不会导致子类初始化
*
**/
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 NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
结果:
SuperClass Init !!!
123
结果只输出了 ”SuperClass Init !!!”, “SubClass Init !!!” 没有出来,对应静态字段,只有直接定义改字段的类才会初始化。
通过数组定义来引用类,不会出发此类的初始化
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
运行结果并没有打印出”SuperClass Init !!!”,这里并没有出发com.sun.jojo.noinitclass.SuperClass类的初始化.这里触发了另一个名为“[Lcom.sun.jojo.noinitclass.SuperClass”的类的初始化,他是虚拟机自动创建的,直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
常量在编译期间就会调入类的常量池中,本质上没有直接引用的定义变量的类,因此使用常量字段时不会触发累的初始化
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(ConstClass.HELLOWORLD);
}
}
结果:只打印出hello world, “ConstClass Init !!!”并没有打印出来。 因为编译阶段通过常量传播优化,已经将 HELLOWORLD 常量的值存储到了 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用实际上都转化为 NotInitialization 类对自身常量池的引用了。
接口的加载过程和类的加载过程稍微有点一些不同,接口中不能使用 “static{}” 语句块,但编译器仍然会为接口生成“
类的加载过程
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问的入口
验证
文件格式验证、元数据验证、字节码验证、符合引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始化的阶段。这些内存都将会在方法区中进行分配。
如static int num = 100;
静态变量 num 就会在准备阶段被赋默认值0。
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int num = 666; 静态常量 num 就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
解析
解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化和其它资源。
在Java中对类变量进行初始值设定有两种方式:
-
声明类变量时指定初始值
-
使用静态代码块为类变量指定初始值
初始化阶段执行类构造器 < clinit >() 方法的过程:
1、 < clinit >() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
2、 < clinit >()方法与类的构造函数(实例构造器 < init >方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕,因此在虚拟机中第一个执行的< clinit >()方法的类一定是java.lang.Object。
3、 由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
static class Parent{
public static int A =1;
static{
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
结果:值为 2
4、 < clinit >()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。
5、 接口中可能会有变量赋值操作,因此接口也会生成< clinit >()方法。但是接口与类不同,执行接口的< clinit >()方法不需要先执行父接口的< clinit >()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的< clinit >()方法。
6、 虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的< clinit >()方法,其它线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。如果在一个类的< clinit >()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。
static class DeadLoopClass {
static {
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
int i = 0;
while (true) {
System.out.println(Thread.currentThread() + " :" + (++i));
if (i == 10) {
break;
}
}
}
}
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass deadLoopClass = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
结果:
注意:其它线程被阻塞,但如果执行< clinit >() 方法的那条线程退出 < clinit >() 方法后,其她线程唤醒之后不会再次进入 < clinit > 方法,同一个类加载器下,一个类型只会初始化一次。
类加载器
什么叫类加载器?
通过一个类的全限定名来获取描述此类的二进制字节流———让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为 “类加载器”。
类和类加载器的关系?
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
“相等”包括 Class 对象 中的 equals() 方法,isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定情况。
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myClassLoader=new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String fileName=name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is =getClass().getResourceAsStream(fileName);
if(is==null){
return super.loadClass(name);
}
byte[] b=new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch (IOException e){
throw new ClassNotFoundException(name);
}
}
};
Object object=myClassLoader.loadClass("com.ljw.ClassLoaderTest").newInstance();
System.out.println(object.getClass());
System.out.println(object instanceof com.ljw.ClassLoaderTest);
}
}
结果:
为什么是 false,因为虚拟机中存在了两个 ClassLoaderTest 类,一个是系统应用程序类加载器加载的,另一个是由自定义的类加载器加载的,虽然是同一个 class。
双亲委派模型
从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:
- 启动类加载器 ( Bootstrap ClassLoader ),这个类加载器使用 C++ 语言实现,是虚拟机自身一部分
- 所有其他类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外部,并且全部继承抽象类 java.lang。ClassLoader。
从 Java 开发人员角度来看,可以分成更细致一点
- 启动类加载器( Bootstrap ClassLoader )主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
- 扩展类加载器( Extension ClassLoader )是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
- 应用程序类加载器( Application ClassLoader ):也称系统类加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式。
双亲委派模式除了顶层的启动类加载器外,其余的类加载器都有自己的父亲加载器。
双亲委派模型的工作过程:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父亲加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 是不是觉得儿子很懒,父亲很勤劳!!!
双亲委派模型的优势
采用双亲委派模型的好处就是 Java 类随着它的类加载器一起具备了一种带有优先性的层次关系。1、 避免了重复加载,当父亲已经加载了该类时,就没必要子 ClassLoader 再加载一次。2、 安全因素,例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器,因此 Object 类在程序的各个类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放的程序的 ClassPath中,那么系统将会出现多个不同的 Object 类,Java 类型体系中最基本的行为也就无法保证。
loadClass(String):该方法加载指定名称的二进制类型,在JDK1.2 之后不再建议用户重写可以直接调用该方法,loadClass()方法是 ClassLoader类自己实现的,改方法中的逻辑就是双亲委派模式的实现。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 先从缓存查找该class对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
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
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);
}
}
if (resolve) {//是否需要在加载时进行解析
resolveClass(c);
}
return c;
}
当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用 this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass 方法获取到 class 对象。
findClass(String):在JDK1.2之前,在自定义类加载时,总会去继承 ClassLoader 类并重写 loadClass 方法从而实现自定义的类加载,在JDK1.2 之后已不再建议用户去覆盖 loadClass()方法,而是建议把自定义的类加载逻辑写在 findClass()方法中。findClass()方法是在loadClass()方法中被调用,当loadClass()方法中父加载器加载失败,就会调用自己的 findClass()方法完成类的加载,这样就保证自定义类加载器也符合双亲委派模式。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass(byte[] b, int off, int len) :defineClass()方法是用来将 byte字节流解析成 JVM 能够识别的 Class 对象。
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
resolveClass(Class < ? > c):使用该方法可以使用类的 Class 对象创建完成也同时被解析。链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
函数调用流程:
自定义类加载器
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classData = getClassData(name);
return defineClass(name, classData, 0, classData.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] getClassData(String name) {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 读取类文件的字节码
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader = new MyClassLoader();
Class myClass=classLoader.loadClass("com.ljw.ClassLoaderTest");
Object obj=myClass.newInstance();
System.out.println(myClass.newInstance().toString());
System.out.println(obj instanceof com.ljw.ClassLoaderTest);
}
结果:
com.ljw.ClassLoaderTest@12a3a380
true
破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。
-
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前--即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
-
双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?
这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。 -
双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
双亲委派模型的破坏者-线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例
参考
深入理解 JVM
有什么问题欢迎指出,十分感谢!