JVM类加载机制详解,建议看这一篇就够了,深入浅出总结的十分详细
类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机
- 遇到new(比如new Student())、getstatic和putstatic(读取或设置一个类的静态字段,如下代码,读取被final修饰并已在编译器把结果放入常量池的静态字段除外)、invokestatic(调用类的静态方法)这四条指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。
public class Student{
private static int age;
public static void method(){
}
}
Student.age
Student.method();
- 使用java.lang.reflect包方法时对类进行反射调用的时候。
- 初始化一个类的时候发现其父类还没初始化,要先初始化其父类。
- 当虚拟机开始启动时,用户需要指定一个主类(main),虚拟机会限制性这个主类的初始化。
类加载的过程
类加载过程是如下图所示的一个流水线过程,其中连接过程可细化为验证、准备和解析三个小步骤。
加载
class文件–>class对象
“加载”过程主要是靠类加载器实现的,包括用户自定义类加载器。
加载的过程
在加载过程中,JVM主要做以下3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流(class文件)。在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程
- 将这个字节流的静态存储结构转化为方法区的运行时数据结构(即Class对象)
- 在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口
程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口。
加载源
JVM规范对于加载过程给予了较大的宽松度,一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从这些地方读取:zip包(jar、war、ear等),由jsp文件中生成对应的Class类,数据库,网络,运行时计算生成(动态代理技术)。
加载过程的注意点
- 类和数组加载的区别:非数组类是由类加载器来完成;数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但数组类与类加载器有很密切的关系,因为数组类的元素类型最终要靠类加载器创建。
- HotSpot将Class对象存放在方法区
验证
各种检查
验证阶段比较耗时,它非常重要但不一定必要,可用-Xverify:none参数关闭,以缩短类加载时间。
验证的目的
保证二进制字节流的信息符合虚拟机规范,并没有安全问题。
验证的必要性
Java语言的安全性是通过编译器来保证的,但编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的。当然,如果是编译器给它的那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。
验证的过程
其中文件格式验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存放到方法区。后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。
准备
为static分配内存并初始化0值。JDK1.7之前在方法区,1.7之后在堆。
仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用final修饰的static,因为final在编译的时候就会分配好,同时这里也不会为实例变量分配初始化。类变量(静态变量)会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
准备阶段主要完成两件事情:
- 为已在方法区中的类的静态成员变量分配内存;
-
为静态成员变量设置初始值,具体初始值为下图所示。
在这里插入图片描述
注意:
public static int x = 1000;
实际上变量x在准备阶段过后的初始值为0,而不是1000。将x赋值为1000是在初始化阶段完成。
解析
将符号引用替换为直接引用
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化
调用<clinit>方法
初始化过程就是调用类初始化方法的过程,完成对static修饰的类变量的手动赋值还有主动调用静态代码块。
注意点:此步骤中虚拟机会保证在多线程环境中一个类的<clinit>方法被正确地加锁(静态内部类)
类加载器介绍
启动类加载器:
由C++实现,不是ClassLoader子类。
负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt,jar)的类。
扩展类加载器:
负责加载JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器:
负责加载用户路径(classpath)上的类库。
自定义类加载器:
上述的加载器只能加载指定目录下的jar和class,如果想加载其他位置的jar或类时,则需要实现自定义类加载器来加载。
比如要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现特定业务逻辑,此时默认的ClassLoader就不能满足我们的需求,需要定义自己的ClassLoader。
双亲委派模型
JVM的类加载是通过多层次的类加载器来完成的,类的层次关系和加载顺序可以由下图来描述:
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader组层检查,只要某个classloader已加载就视为已加载此类,保证此类只加载一次。而加载的顺序是自顶向下,也就是由上层来组层尝试加载此类。这种类加载的层次关系就是双亲委派模型。
需注意的点:
- 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器;
- 只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
为什么使用双亲委派这种模型
因为这样可以避免类的重复加载,当父classloader经加载了该类的时候,就没必要子classloader再加载一次。
考虑到安全因素,我们试想一下,如果不适用这种委托模型,那我们就可以随时使用自定义的String来动态替代java核心api重定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况。因为String已经在启动时就被Bootstrap ClassLoader加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非改编JDK中ClassLoader搜索类的默认算法。
判定两个Class对象是否相同的依据
- class字节码是否相同
- ClassLoader是否相同
JVM在判定两个class是否相同,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。类的全限定名完全相同,但是加载它的类加载器不同,那么在方法区中会产生不同的Class对象。
只有两者同时满⾜的情况下,JVM才认为这两个class是相同的。就算两个class是同⼀份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
破坏双亲委派模型
为什么需要破坏双亲委派
因为在某些情况下父类加载器需要加载的class文件由于受到加载范围的限制,父类加载器无法加载到需要的文件,这个时候就需要委托子类加载器进行加载。
双亲委派模型是在JDK1.2以后才使用的,但是有一些核心的API类在JDK1.2之前就已经写好了。
简单理解双亲委派模型是子类加载器去委托父类加载器完成类加载的工作,而破坏双亲委派模型是父类加载器去委托子类加载器完成类加载的工作。
以Driver接口为例,由于Driver接口定义在jdk当中,而其实现由各个数据库的服务商来提供,比如mysql就写了MySQL Connector,这些实现类都是以jar包的形式放到classpath目录下。
那么问题就来了,DriverManager(也由jdk提供,JDK1.2之前就写好了)要加载各个实现了Driver接口的实现类(在classpath下),然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME\lib下的文件,而其实现是由服务商提供的,有系统类加载器加载,这个时候就需要启动类加载器来委托子类加载器来加载Driver实现,从而破坏了双亲委派。如下图所示。
最后
欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!