JVM类加载机制
Java程序运行过程
-
首先通过Javac编译器将==.java转为JVM可加载的.class==字节码文件。
javac是由Java编写的程序,编译过程可分为
- 词法分析。通过空格分割出单词、操作符、控制符等信息,形成token信息流,传递给语法解析器。
- 语法解析。把token信息流按照Java语法规则组装成语法树。
- 语义分析。检查关键字使用是否合理、类型是否匹配、作用域是否相等。
- 字节码生成。将前面各个步骤的信息转换成字节码。
字节码必须通过类加载过程加载到JVM才可以执行,执行有三种模式,解释执行、JIT编译执行、JIT编译与解释器混合执行(主流JVM默认执行的方式)。混合模式的优势在于解释器在启动时先解释执行,省去编译时间。
-
之后通过即时编译器JIT把字节码文件编译成本地机器码。
Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为“热点代码”,热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。
-
还可以通过静态的提前编译器AOT直接把程序编译成与目标机器指令集相关的二进制代码。
类加载
Class文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程称为虚拟机的类加载机制。
与编译时需要连接的语言不同,Java中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
一个类型从被加载到虚拟机开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序时确定的,解析则不一定:可能在初始化后再开始,这是为了支持Java的动态绑定。
类加载的过程
加载
该阶段虚拟机需要完成三件事:
- 通过一个类的全限定类名获取定义类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据区
- 在内存中生成对应该类的Class实例,作为方法区这个类的数据访问入口。
注意:这里不一定非要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
验证
这一阶段是确保Class文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。
准备
为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被final修饰,编译时Javac会为变量生成ConstantValue属性,准备阶段虚拟机会将变量值设为代码值。
比如:
public static int v = 8080;
实际上变量V在准备阶段过后的初始值为0,而不是8080,将v赋值为8080的put static指令是程序编译后,存放于类构造器中
但如果声明为:
public static final int v = 8080;
在编译阶段会为生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080.
解析
该阶段虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用:以一组符号描述为引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。
直接引用:是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局无关,引用目标必须已在虚拟机的内存中存在。
初始化
直到该阶段JVM才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的方法,该方法是javac自动生成的。
使用
卸载
类构造器
初始化阶段就是执行类构造方法中的方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
-
遇到new、getstatic、putstatic或invokestatic字节码指令时,还未初始化。典型场景包括new实例化对象、读取或设置静态字段、调用静态方法。
-
对类反射调用时,还未初始化。
-
初始化类时,父类还没有初始化。
-
虚拟机启动时,会先初始化包含main方法的主类。
-
使用JDK7的动态语言支持时,如果MethodHandle实例的解析结果为指定类型的方法句柄对应的类还未初始化。
-
接口定义了默认方法,如果接口的实现类初始化,接口要在之前初始化。
其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用实例:
- 子类使用父类静态字段时,只有父类被初始化
- 通过数组定义使用类
- 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。
接口和类加载过程的区别:
初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。
类加载器
启动类加载器
在JVM启动时创建,负责加载最核心的类,例如Object、System等。无法被程序直接引用,如果需要把加载委派给启动类加载,直接使用null代替即可,因为启动类加载器通常由操作系统实现,并不存在于JVM体系。
扩展类加载器
从JDK9开始从扩展类加载器更换为平台加载器,负载加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等。
应用程序类加载器
也称系统类加载器。负责加载用户类路径(classpath)上的类库,可以直接在代码中使用,如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器通过继承Class Loader并重写findClass方法实现。
双亲委派模型
类加载器具有等级制度但并非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。
一个类加载器收到了类加载请求,它不会自己去尝试加载,而是将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。
类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。
双亲委派机制的作用
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
类加载器环境中都是同一个,保证程序的稳定性。
[外链图片转存中…(img-I4TJP7vw-1602771672834)]
双亲委派机制的作用
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。