文章目录
0、前言
从.java类文件创建,经过JVM编译后生成.class文件,并最终被实例化执行,在这整个过程中,Java文件发生了什么变化?JVM又做了哪些工作?笔者结合《深入理解Java虚拟机》,通过这篇文章和各位探讨分析一下。
1、类加载过程分析
JVM中类加载的全过程包括加载、验证、准备、解析和初始化
1.1、加载阶段
在分析这个过程之前,我们先抛四个问题:
加载什么?从哪里加载?由谁来执行加载?加载过程中需要做哪些工作?
1.1.1、加载什么
自然是加载由.java
文件经JVM编译生成的.class
文件。不过这么说还不算准确,实际上是加载.class
文件的二进制字节流,并将这个二进制字节流中的静态存储结构转化为方法区的运行时数据结构。
首先,我们分析下.class
文件包含哪些东西。.class文件由.java
文件编译而成,自然和.java
文件包含的东西保持一致,这里面包含类本身的一些描述性信息,包括成员变量信息、成员方法信息、构造函数信息等等。当然还包含程序员同学定义的数据结构、实现的算法逻辑等等。通过把这些信息加载到内存,从而生成方法区的运行时数据结构。
1.1.2、从哪里加载
我们知道加载时本质上是加载二进制字节流,从哪里加载,换句话说,就是从哪里获得二进制字节流。来源一般有以下几种:
- 从本地系统中将Java源文件动态编译为
.class
文件 - 通过网络获取(Applet)
- 从zip,jar等归档文件中读取
- 运行时计算生成(动态代理技术)
- 由其他文件生成(JSP)
- 从专有数据库中提取
.class
文件(场景较少)
1.1.3、由谁来执行加载
类的加载由类加载器来执行加载。这里的类加载分为两类:系统引导类加载器和用户自定义加载器(重写一个类加载器的loadClass()
方法)。
类加载器会在下文详细说明。
注:摘自《深入理解java虚拟机》以说明,以下元素类型在原文中叫"组件类型"。
数组不需要通过类加载器创建,而是由JVM直接创建的,但是数据和对象有密切关系,因为数据元素的类型是靠类加载器去创建。
一个数组的创建过程遵循以下原则:
1)如果数组的元素类型是引用类型,就需要类加载器加载这个类型,数组将在加载改类型的类加载器的类名称空间上被标识。
2)如果数组的元素类型不是引用类型(如int[]),JVM会吧数组标记为与引导类加载器关联。
3)数组类的可见性与它的元素类型的可见性一致,如果元素类型不是引用类型,那数组类的可见性默认为public。
1.1.4、加载过程中主要做了哪些工作
Java虚拟机规范针对加载过程有一下三点工作要求:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区该类的各种数据的访问入口。
1.2、连接阶段
1.2.1、验证阶段
同样抛出以下几个问题:
为什么验证?验证什么?验证规则是什么?如果不验证会有什么问题?
1.2.1.1、为什么要验证
验证是连接阶段的第一步,该阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
——《深入理解java虚拟机》
1.2.1.2、验证什么以及验证规则是什么
- 文件格式验证-基于二进制流进行;
- 验证字节流是否符合Class文件格式规范,并能被当前版本JVM处理
- 元数据验证-基于方法区的存储结构进行;
- 语义分析,确保描述的信息符合Java语言规范
- 字节码验证-基于方法区的存储结构进行;
- 验证最复杂,通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的
- 符号引用验证-基于方法区的存储结构进行;
- 可以看作是对类自身以外的信息进行匹配性验证
1.2.1.3、如果不验证会有什么问题
可能会因为载入有害的字节流而导致系统崩溃,所以验证是对JVM自身的一种保护。
1.2.2、准备阶段
问题先行:准备什么?怎么准备?
1.2.2.1、准备什么以及怎么准备
为类变量分配内存,并设置类变量的初始值。使用方法区为类变量分配内存。
注意点:
- 这个阶段只为类变量分配内存(
static
修饰),不包含实例变量(实例变量将在对象实例化过程中跟实例对象一起分配到堆中); - 这里说的初始值是指零值(真正的赋值在初始化阶段进行)。
1.2.3、解析阶段
问题先行:解析什么?怎么解析?
解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。
1.2.3.1、符号引用和直接引用
- 符号引用
- 直接引用
1.2.3.2、解析什么
- 类和接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 方法类型解析
- 方法句柄解析
- 调用点限定符解析
1.2.3.2、怎么解析
略
1.3、初始化阶段
类初始化阶段是类加载过程的最后一步。初始化阶段是执行类构造器<clinit>
()的过程,通过分析该方法,我们可以知道该过程都过了哪些操作。
1.3.1 <clinit>
()方法
- 该方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成。收集的顺序是由语句在源文件中出现的顺序所决定。
- 该方法与类的构造方法不同。JVM会保证子类的该方法执行之前,父类的该方法已经执行完毕。
- 由于父类的该方法先执行,所以父类中定义的静态语句块要优先于子类的变量赋值操作。
- 该方法对于类或接口来说并不是必需的。即如果一个类中没有静态语句块,也没有对成员变量的赋值语句,编译器并不会为该类生成该方法。
- 接口中没有静态语句块,但是可以为成员变量赋值,因此接口也可能生成该方法。
- JVM会保证一个类的该方法被正确的加锁、同步。
2、类加载器介绍
问题先行:什么是类加载器?功能是什么?
2.1、什么是类加载器以及功能
前面我们谈到.class文件是由类加载器加载到内存,那什么是类加载器呢?
JVM设计者把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
所以,类加载器是一个代码模块,作用是通过类的全限定名获取二进制字节流,并加载到内存。
2.2、类加载器的分类
JVM的系统类加载器有三个,一方面是为了分工明确,各自负责各自的区块,另一方面为了实现委托模型。
- 系统类加载器
- 启动类加载器(Bootstrap ClassLoader):加载
System.getProperty("sun.boot.class.path")
所指定的路径或jar。JAVA_HOME\lib目录中并且能被虚拟机识别的类库。 - 扩展类加载器(Extension ClassLoader):加载
System.getProperty("java.ext.dirs")
所指定的路径或jar。JAVA_HOME\lib\。 - 应用程序类加载器(Application ClassLoader):加载
System.getProperty("java.class.path")
所指定的路径或jar。加载用户类路径(Classpath)上所指定的类库。
- 启动类加载器(Bootstrap ClassLoader):加载
- 自定义类加载器(继承
java.lang.ClassLoader
类,推荐重写findClass()
方法)
2.3、类和类加载器
对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。
3、类加载的双亲委派模型解析
为什叫双亲委派?具体是怎么样的加载过程?有什么优点?
3.1、双亲委派模型及工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
3.3、优点
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次;
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为
java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class
,这样便可以防止核心API库被随意篡改。
参考:
1、《深入理解Java虚拟机-JVM高级特性与最佳实践》;
2、https://www.jianshu.com/p/ace2aa692f96