浅谈JVM:类加载机制 Posted 2023-04-03 专治八阿哥的孟老师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈JVM:类加载机制相关的知识,希望对你有一定的参考价值。
上一篇: 浅谈JVM(一):Class文件解析
类加载机制
Java虚拟机把类的描述数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是虚拟机的类加载机制。
《Java虚拟机规范》中说:JVM能动态地加载(Loading)、连接(Linking)、初始化(Initializing)类和接口。
加载 是查找具有特定名称的类或接口类型的二进制表示,并根据该二进制表示创建类或接口的过程。
连接 是获取类或接口并将其组合到JVM的运行时状态中以便执行的过程。
初始化 包括执行类或接口初始化方法,是编译后自动生成的方法。
虚拟机规范中使用的名词是"二进制表示"(binary representation),它并非只限定为磁盘中的.class文件,而可以理解为是一个二进制字节流,这个二进制字节流可以从压缩包中获取、通过动态代理生成、通过网络获取等。为了简化描述,统一使用Class文件指代。
此外,虚拟机规范中多数时候的用词是"类或接口"(a class or an interface),多数时候类和接口的描述是一样的,所以我们统一简化用"类"描述,如果class和interface描述不同时,会再区分类和接口。
虚拟机规范中没有指明如何具体实现类加载过程,不同虚拟机在具体实现上有所差异,本文为以HotSpot虚拟机为例进行阐述。
2.1.加载
类加载过程(Class Loading)和加载(Loading)是不一样的概念,类加载包含加载、连接、初始化三个阶段,加载只是类加载过程的第一个阶段。加载过程与类加载器(class loader)密切相关,将在下一篇文章中介绍类加载器详细内容。
非数组类加载动作是由类加载器完成的,在加载阶段,完成三件事:
通过类的全限定名获取定义该类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区(Method Area)的运行时数据结构(Run-Time Data)。
在内存中生成代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
数组类情况有所不同(数组类型如TestArrayClass[ ]),数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(指数组去掉维度后的类型,如TestArrayClass)还是要靠类加载器加载。数组加载规则如下:
如果数组元素类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组将被标识在加载该组件类型的类加载器的类名称空间上。
如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组标记为与引导类加载器关联。
数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到。
加载与连接的部分动作可能是交叉进行的,加载尚未完成连接可能已经开始。
2.2.连接
连接阶段又分为验证(Verification)、准备(Preparation)、解析(Resolution)三个阶段。
验证阶段的目的是要保证Class文件的信息符合虚拟机规范,保证代码运行后不会危害虚拟机自身安全。当字节流信息不符合虚拟机的Class格式规范时,会抛出一个VerifyError。验证阶段可能触发其他相关类的加载动作,但不需要对它们进行验证。
验证阶段大致包含以下四种验证:
文件格式验证:如魔数、版本号、常量池等。
元数据验证:对字节码数据进行语义分析,如继承关系(除java.lang.Object外所有类都要有父类、不能继承final类)、非抽象类是否实现了父类或接口的所有要求实现的方法、方法重载是否正确等。
字节码验证:对方法体(Class文件中的Code属性)进行校验分析,比如父类对象赋值给子类类型、操作数与操作指令不匹配等。
符号引用验证:验证该类是否缺少或被禁止访问某些外部类、方法、字段。如根据字符串描述的类的全限定名能否找到该类,类、字段、方法的访问权限(private、protected、public、default)等。
准备阶段为类变量(static修饰的,不包括实例变量)分配内存并赋默认值。注意这一阶段的赋值不是初始值,而是默认零值。
public static int value = 123 ;
上述代码在准备阶段过后,value的初始值是0。真正赋值成123的动作要在初始化阶段完成。赋值指令会被编译到""方法里,在初始化阶段执行。
特殊情况是变量由final修饰,在准备阶段就会赋值。
public static final int value = 123 ;
解析阶段将虚拟机常量池中的符号引用替换成直接引用(常量池见浅谈JVM(一):Class文件解析 )。符号引用是用一组符号(任意形式字面量)来描述所引用的目标,和虚拟机布局无关;直接引用和虚拟机布局直接相关,是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如描述符是"Ljava/lang/Integer"对应的就是"java.lang.Integer"类。
2.3.初始化
初始化阶段会执行类构造方法"“,此处的"类构造方法"不是构造器(Constructor),而是由编译器自动生成的方法,自动收集类中所有类变量的赋值动作和静态代码块(static块),合并产生名为”“的方法。而创建对象的构造方法会被编译成”"方法。
package com. menglaoshi. test ;
public class TestClass01
static
i = 2 ;
public static int i = 1 ;
上述代码编译后如下:
编译器对于类变量和静态语句中的语句进行合并,根据语句在源文件中的顺序进行收集,静态代码块可以对定义在它之后的变量进行赋值,但不能访问,如下面的代码就不能编译通过。
类和接口初始化有所不同,类的方法在执行时,不需要显式调用父类的,虚拟机会保证子类执行时,父类已经执行完毕。如果类中没有静态语句,则不会产生方法;接口在初始化时,并不要求其父接口全部完成初始化,只有真正使用到父接口的时候才会初始化。
虚拟机要保证一个类的方法在多线程情况下被正确地加同步锁,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的,其他线程都要阻塞,且其他线程唤醒后也不会再次执行,同一个类加载器下,一个类型只会初始化一次。
虚拟机规范没有明确说明什么时候进行加载和连接,但明确规定了哪些情况下类和接口会初始化,而加载和连接必须在初始化之前完成。
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
使用new关键字实例化对象的时候。 读取或设置一个类型的静态字段的时候(被final修饰、已在编译期把结果放入常量池的静态字段除外)。 调用一个类型的静态方法的时候。
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
对于这六种会触发类型进行初始化的场景,虚拟机规范中使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
通过子类引用父类静态字段,不会导致子类初始化,如下:
package com. menglaoshi. test ;
public class Father
static
System . out. println ( "Father init" ) ;
public static int value = 123 ;
public class Sun extends Father
static
System . out. println ( "Sun init" ) ;
public class TestClass
public static void main ( String [ ] args)
System . out. println ( Sun . value) ;
定义数组引用不会导致类的初始化,将main方法修改如下:
public static void main(String[] args) Father[] fathers = new Father[10];
运行后可以看到Father的静态代码块没有执行,查看编译后的代码:
可以看到初始化的类是"**[**Lcom/menglaoshi/test/Facher;“,前面由一个”["代表这是一个数组,这并不是一个合法存在的类型,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
public class ConstantClass
static
System . out. println ( "ConstantClass init" ) ;
public static final String value = "a" ;
public class TestClass
public static void main ( String [ ] args)
System . out. println ( ConstantClass . value) ;
运行上述代码,发现没有输出"ConstantClass init"。因为常量value的值在编译阶段通过常量传播优化,直接存在了测试类TestClass的常量池中。
浅谈JVM-图解类加载机制
一、目录
二、类加载机制流程
1、什么是类加载机制?
JVM把class文件加载到内存里面,并对数据进行校验、准备、解析和初始化,最终能够被形成被JVM可以直接使用的Java类型的过程。
2、类加载流程图
3、加载
将class文件加载在内存中。
将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构)。
在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。
4、链接
链接就是将Java类的二进制代码合并到java的运行状态中的过程。
验证:确保加载的类符合JVM规范与安全。
准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0,其他数据类型参考成员变量声明。
解析:虚拟机将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用 的目标并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
5、初始化
初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并,初始化时机后续再聊。
6、使用
正常使用。
7、卸载
GC把无用对象从内存中卸载。
三、类加载与初始化时机
1、类加载时机
当应用程序启动的时候,所有的类会被一次性加载吗?估计你早已知道答案,当然不能,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。那类什么时候被加载呢?例如,A a=new A(),一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。当我们测试的时候,最先加载拥有main方法的主线程所在类。
2、类初始化时机
主动引用(发生类初始化过程)
new一个对象。
调用类的静态成员(除了final常量)和静态方法。
通过反射对类进行调用。
虚拟机启动,main方法所在类被提前初始化。
初始化一个类,如果其父类没有初始化,则先初始化父类。
被动引用(不会发生类的初始化)
当访问一个静态变量时,只有真正声明这个变量的类才会初始化。(子类调用父类的静态变量,只有父类初始化,子类不初始化)。
通过数组定义类引用,不会触发此类的初始化。
final变量不会触发此类的初始化,因为在编译阶段就存储在常量池中。
四、图解分析类加载
1 public class ClassLoaderProduce {
2 static int d=3;
3 static {
4 System.out.println("我是ClassLoaderProduce类");
5 }
6 public static void main(String [] args){
7 int b=0;
8 String c="hello";
9 SimpleClass simpleClass=new SimpleClass();
10 simpleClass.run();
11 }
12 }
13
14 class SimpleClass{
15 static int a=3;
16 static {
17 a=100;
18 System.out.println(a);
19 }
20
21 public SimpleClass(){
22 System.out.println("对类进行加载!");
23 }
24
25 public void run(){
26 System.out.println("我要跑跑跑!");
27 }
28 }
步骤一:装载ClassLoaderProduce类,在方法区生成动态数据结构(静态变量、静态方法、常量池、类代码),并且在堆中生成java.lang.Class对象;然后进行链接
步骤二:初始化:把static{}与静态变量合并存放在类构造器当中,对静态变量赋值。 1-5行执行完毕。
步骤三:执行main方法,首先在栈里面生成一个main方法的栈祯,定义变量b、c,注意此处的变量b、c存储的常量池存储的变量的地址,如图所示。
步骤四:创建SimpleClass对象;跟上面步骤类似:加载-链接-初始化。然后,调用run()方法的时候,它会通过classLoader局部变量的地址寻找到类的class对象并且调用run()方法
以上是关于浅谈JVM:类加载机制的主要内容,如果未能解决你的问题,请参考以下文章
java类加载机制
JVM类加载机制
JVM 类加载机制
JVM——类加载机制
JVM类加载机制
面试必问:JVM类加载机制详细解析