类加载过程已经是老生常谈模式了,我曾在讲解tomcat的书中、在Java基础类书、JVM的书、在Spring框架类的书中、以及各种各样的博客和推文中见过,我虽然看了又忘了,但总体还是有些了解,曾经自以为这不是什么大不了的过程。但时间总会教你做人,看得越多,越觉得以前理解不足。
此笔记记录,虚拟机中Java类加载的过程,各个过程发生的时机,具体做了什么事情,例如,在方法区或者堆分配了哪些内存,创建了哪些常量等。由于Java文件会预先编译,得到class文件,虚拟机的类加载,是对class文件进行的操作,所以不可避免的涉及到class文件的解读,只有知道class文件中有什么,虚拟机才能加载对应的内容。
? 不可能完全解读class文件,《虚拟机规范第二版》花了一百多页写class文件,这是class的核心,如果要完全理解,可能还得去复习复习编译原理,词法分析语法分析代码生成之类的。
文件结构定义:u1 = 1个字节,u2 = 2个字节,u4 = 4个字节,u8 = 8个字节;
ClassFile {
u4 magic; // 魔法数
u2 minor_version; // 副版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池计数,从1开始计数
cp_info constant_pool[constant_pool_count-1]; // 常量池[常量数量]
u2 access_flags; // 访问标志
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces_count; // 接口计数器
u2 interfaces[interfaces_count] // 接口表
u2 fields_count; // 字段计数器
field_info fields[fields_count]; // 字段表
u2 methods_count; // 方法计数器
method_info methods[methods_count]; // 方法表
u2 attributes_count; // 属性计数器
attribute_info attributes[attributes_count]; // 属性表
}
根据这个表,一个class文件的16进制文件就可以读取了。此处要注意几点
代码
/**
* @Author: dhcao
* @Version: 1.0
*/
public class ClassTest {
public static final String abc = "ccc";
private static String def = "fff";
public String getAbcdef(){
return abc + def;
}
}
编译为ClassTest.class
直接使用subline或者其他软件打开此二进制文件
解读:根据class文件结构解读
JDK版本号 | Class版本号 10进制 | 16进制 |
---|---|---|
1.1 | 45.0 | 00 00 00 2D |
1.2 | 46.0 | 00 00 00 2E |
1.3 | 47.0 | 00 00 00 2F |
1.4 | 48.0 | 00 00 00 30 |
1.5 | 49.0 | 00 00 00 31 |
1.6 | 50.0 | 00 00 00 32 |
1.7 | 51.0 | 00 00 00 33 |
1.8 | 52.0 | 00 00 00 34 |
00 27:u2, 常量池计数器。十六进制27 = 十进制39,从1开始计数,代表有38项常量。
反编译该class文件: javap -verbose ClassTest
第一部分:常量池部分
Constant pool:
#1 = Methodref #10.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // java/lang/StringBuilder
#3 = Methodref #2.#27 // java/lang/StringBuilder."<init>":()V
#4 = Class #29 // org/relax/jvm/demo/ls/ClassTest
#5 = String #30 // ccc
#6 = Methodref #2.#31 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Fieldref #4.#32 // org/relax/jvm/demo/ls/ClassTest.def:Ljava/lang/String;
#8 = Methodref #2.#33 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = String #34 // fff
#10 = Class #35 // java/lang/Object
#11 = Utf8 abc
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 ConstantValue
#14 = Utf8 def
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lorg/relax/jvm/demo/ls/ClassTest;
#22 = Utf8 getAbcdef
#23 = Utf8 ()Ljava/lang/String;
#24 = Utf8 <clinit>
#25 = Utf8 SourceFile
#26 = Utf8 ClassTest.java
#27 = NameAndType #15:#16 // "<init>":()V
#28 = Utf8 java/lang/StringBuilder
#29 = Utf8 org/relax/jvm/demo/ls/ClassTest
#30 = Utf8 ccc
#31 = NameAndType #36:#37 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#32 = NameAndType #14:#12 // def:Ljava/lang/String;
#33 = NameAndType #38:#23 // toString:()Ljava/lang/String;
#34 = Utf8 fff
#35 = Utf8 java/lang/Object
#36 = Utf8 append
#37 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#38 = Utf8 toString
类文件(class文件)是Java编译器编译之后的结果,它遵循的是编译原理。类加载,是指JVM将class文件加载到虚拟机的过程。只有将class文件加载到虚拟机,才能够使用该class。
将一个class文件(不管是文件还是二进制...只要是class格式)记载到虚拟机,最后移出虚拟机,通常认为有以上步骤,即类等声明周期。对于大多数时候,我们并不关注卸载过程,将“Using、使用”之前对类等处理,统称为“类加载”。所以也有描述类加载为3个主要过程(这也是虚拟机规范定义的过程):加载 --- 连接 --- 初始化
说明一:区分“加载(Loading)”和“类加载”。很明显,当我们定义类加载为“加载、连接、初始化”只有,就知道,“加载(Loading)”只是整个“类加载”过程的一部分。
说明二:Resolution、解析。解析过程是连接的一部分,但是并不一定发生在“初始化”之前。虚拟机规范并没有要求“解析”一定发生在“初始化之前”,虚拟机可根据不同情况,进行不同处理。
说明三:加载、验证、准备、初始化、卸载。这五个阶段是有序的,但是....其界限并非是先加载,加载完毕之后开始验证,验证完毕之后开始准备之类的。此有序,只是“开始时间有序”,即:加载开始时间一定在验证开始之前。但加载和验证可以有交集。
我理解,记载的整个过程,应该是覆盖了“连接”。
过程二:数组类String[] aa = xxx
我们知道数组是一串连续的内存地址。从这个定义上来看,我们就知道,这不是类加载器可以控制的。数组类的创建是由JVM直接创建的。
数组的每个元素(对象)依然要靠类加载器创建。也就是说,数组本身由JVM创建并分配内存,但是其元素依然依靠类加载器加载。
在加载时,可能抛出以下异常:
由上可见,加载本身也含有一定程度上的校验,不可能啥都加载。所以加载发生在验证开始之前, 但并非一定是结束在验证开始之前。
? 在读入了二进制流之后,验证就开始了,验证的目的是保证Class文件的字节流包含的信息符合JVM的要求,并且不会危害JVM的安全。
? 那么需要验证哪些呢,在《虚拟机规范 Java SE 8》中,章节目录4.10详细讲解了JVM加载class文件需要进行的校验,根本目的还是保证class文件的正确性和安全性。
? 该阶段是非常重要的,在经过前面的阶段之后,一个Class文件已经加载到了JVM并验证了其正确性,那么接下来就需要对Class文件进行处理。
? 虚拟机规范规定:准备阶段的任务是创建类或者接口的静态字段,并用默认值初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令。
? 过程
从开始接触Java我们就一直被一些看似简单实际有些意思的题目烦扰,例如:静态变量,静态块的执行顺序、父类子类的执行顺序、变量赋值时间、方法传递的是引用还是值等等乱七八糟的问题。
关于初始值:public static int value = 123; 在准备阶段,这段代码只会得到:value = 0,这是因为int型变量的初始值为0(引用类型初始值为null)。
但是:public static final int value = 123; 在准备阶段,这段代码会得到:value = 123,这是因为final定义常量,其值在编译时确定。
尝试分析,如何标记常量,以及为它赋值。依然是最上述的代码段
反编译该class文件: javap -verbose ClassTest
第二部分:编译码 { public static final java.lang.String abc; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String ccc public org.relax.jvm.demo.ls.ClassTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/relax/jvm/demo/ls/ClassTest; .... ..... ...... }
以上,为javap的反编译结果第二部分。我们看源代码中:
public static final String abc = "ccc"; 编译之后: public static final java.lang.String abc; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String ccc
重点在于ConstantValue: String ccc。
属性ConstantValue:如果同时使用了static 和 final,javac编译器在编译时就在字段上著明该属性,并在类加载的准备阶段,将该属性的值,自动赋值给静态变量。
在最开始定义class格式的时候写到,文件最后定义的是属性表,ConstantValue就是属性表中的属性。
- 作用范围:使用在字段上。
- 如果flags中含有ACC_STATIC和ACC_FINAL,那么ConstantValue的值将直接赋值给该字段。也就是ccc直接赋值给abc。
- 强调:虚拟机规范只要求有ACC_STATIC就可以使用ConstantValue,是sun公司的javac编译器要求同时使用ACC_STATIC和ACC_FINAL
- 只能限于基本类型和String使用
? 解析这个过程,并没有严格的规定在什么时候发生。解析的作用是将符号引用替换为直接引用的过程,只需要在使用符号之前替换这个符号就行。
? 以上描述还有些难以理解。说实话,我也不知道怎么解释了,举个例子描述(类方法解析):
public String getAbcdef(){
int a = 3;
int c = a + 4;
return abc + def + c;
}
执行 javap -verbose ClassTest
--------------------------------------------------------------------------------
public java.lang.String getAbcdef();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_3
1: istore_1
2: iload_1
3: iconst_4
4: iadd
5: istore_2
6: new #2 // class java/lang/StringBuilder
9: dup
10: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
13: ldc #5 // String ccc
15: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: getstatic #7 // Field def:Ljava/lang/String;
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: iload_2
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
28: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: areturn
LineNumberTable:
line 15: 0
line 16: 2
line 17: 6
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 this Lorg/relax/jvm/demo/ls/ClassTest;
2 30 1 a I
6 26 2 c I
--------------------------------------------------------------------------------
在反编译中,一直出现的jvm指令:invokevirtual
解析的目的:
? 在上文 中,该指令调用的是StringBuilder的append方法(直接字符串相加),虚拟机要求,在执行该条指令(invokevirtual)之前,需要对他们所使用的符号进行解析,也就是对StringBuilder.append进行解析。解析的结果是,该指令能够正确的找到该方法的入口!
解析过程:(类方法解析)
#6 = Methodref #2.#31 // java/lang/StringBuilder.append:
? 它是一个Methodref(方法常量),这是由#2(java/lang/StringBuilder)和#31(append)组成的。
? 而#2:
#2 = Class #28 // java/lang/StringBuilder
? 它是一个Class(类)。
解析的目的是将符号引用转为能用的直接引用。主要包含以下:
? Loading、加载阶段读入了Class文件
? Verifition、验证阶段校验了其正确性
? Preparation、准备阶段为其开辟了内存空间,并为static属性赋初始值
? Resolution、解析阶段将符号引用都转为了直接引用,使得Class中的定义都有了实际意义,不再是一串字面量
? Initializaition、初始化阶段,是类加载的最后一步
也是执行字节码的过程,也是执行<clinit>()方法的过程
<clinit>() 顺序:
编译器收集类变量的赋值和静态块的语句,是按照Class文件中的语句顺序来的,所有如下中,static 变量i 在定义赋值之前就想要输出,是不行的。
public class ClassTest {
static {
i = 0; // 可以通过
System.out.println(i); // 编译报错
}
static int i = 1;
}
重要:虚拟机会保证父类的<clinit>()方法在子类之前已经执行完毕。所以第一个执行<clinit>()方法的肯定是java.lang.Object。
这也意味着父类的静态块语句在子类静态块之前执行
虚拟机保证在多线程环境下,同一个类加载器中<clinit>()方法只被执行一次。
? 类的加载过程,主要流程如上,但是更多的细节,没有描述,例如更多的Class文件细节,更多的类加载的内容,更具体的栈与堆的数据结构和分配过程。在后面的笔记中将对这些进行补充。
class Parent {
static {
System.out.println("父类静态块");
}
Parent(){
System.out.println("父类构造函数");
}
}
class Sub extends Parent{
static {
System.out.println("子类静态块");
}
Sub(){
System.out.println("子类构造函数");
}
}
// 如何输出...
class Test{
public static void main(String[] args) {
new Sub();
}
}
原文:https://www.cnblogs.com/dhcao/p/12019859.html