[原文地址]
总体来说JVM加载类的过程分为三步:加载、链接、初始化
加载
加载,是指查找字节流,并且据此创建类的过程。
说到这个过程我们就要介绍一下几种类加载器
Java9之前类加载器一共分为三种:启动类加载器、拓展类加载器和应用类加载器
启动类加载器
启动类加载器是用来加载最基础最核心的类(jre/lib下的jar包如rt.jar Java的工具类就在这个包里)以及其他类加载器。启动类加载器由C++编写,没有对应的Java对象。
拓展类加载器
扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器
应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
//演示
public static void main(String[] args) {
Test t=new Test();
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader()
.getParent().getParent());
}
// 输出
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$ExtClassLoader@4554617c
// null
从输出中可以看出我们自己写的类是用应用加载器加载的,应用加载器的父类加载器(注意是父类加载器而不是父类)是拓展类加载器,拓展类加载器的父类加载器是启动类加载器(启动类加载器没有对应的Java实现类,所以为null)
类加载器的三大特性
委托性、可见性、单一性
委托性
除了启动类加载器以外的其他类加载器在加载一个类的时候会首先访问他的父类加载器,也就是说一个类应该最先被最底层的启动类加载器获取,如果启动类加载器无法加载这个类才会一层一层的向子类加载器移动,直到能够加载这个类或者此类加载器(指一开始指定的类加载器)加载这个类为止。这个也叫做双亲委派机制。
可见性
可见性指的是父加载器无法利用子加载器加载的类,而子加载器可以利用父加载器加载的类。
单一性
一个类只会被一个类加载器加载一次,不会被重复加载。这也是为什么有委托性,为什么采用双亲委派机制的原因,使用双亲委派机制可以保证在类加载器出现重名等有多个类加载器都可以加载一个类的情况保证由最底层的类加载器来加载类,不会出现重复加载的情况,杜绝了Java核心类被覆盖的情况。
Java 9 引入了模块系统,并且略微更改了上述的类加载器 。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
在JVM中,类的唯一性是由类和类加载器共同决定的,所以同一段字节流,用不同加载器加载出来的类是不同的,大型应用中往往利用这个特性来运行同一个类的不同版本
链接
链接指将加载之后类合并到Java虚拟机中使其成为可运行状态的过程。整个过程分为三个阶段:检验、准备、解析。
检验
Java虚拟机有自己的一套规范,在进行准备之前首先会先确认加载过的这个类是否符合虚拟机的规范。这个规范根据虚拟机不同也是不同的,比如单纯使用Java语言无法实现的功能通过修改字节码就有可能实现,如果虚拟机不对相应的检验加载出来的代码可能会危及到虚拟机本身的运行,可以想象一下一旦代码可以访问任意地址的话虚拟机的安全将没有任何保障。检验过程穿插在整个加载过程中,准备和解析时都会进行相应的验证。
准备
准备过程就是类的内存分配并设置类变量(static)初始值的过程,通常这个初始值是零值(具体各个变量的零值见下表),但当类变量用final修饰的时候,这个变量将成为一个常量(ConstantValue),这个时候初始值为代码中赋予的值。
数据类型 | 零值 |
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
零值不代表他们在内存中的真实值 ,比如int类型、long类型和boolean类型
public class Test{
boolean test=false;
short testB=0;
char testC='C';
int testD=20;
long testE=20;
}
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field test:Z
9: aload_0
10: iconst_0
11: putfield #3 // Field testB:S
14: aload_0
15: bipush 67
17: putfield #4 // Field testC:C
20: aload_0
21: bipush 20
23: putfield #5 // Field testD:I
26: aload_0
27: ldc2_w #6 // long 20l
30: putfield #8 // Field testE:J
33: return
可以看到,boolean类型和short类型都是通过iconst将常量值压入栈通过putfield赋值的,两者值是一样的,唯一的区别在于他们变量的描述符不同,而long和int表示范围不同,压入栈中的方式也不同,但是值是一样的。
解析
Java编译类的时候不可能知道这个类在被加载到虚拟机中之后所在的具体位置,也不会知道这个类中引用的其他类的具体位置,这个时候Java会为这个类的成员变量准备一个相对的符号引用
public class Test{
public String filedA="A";
public void methodA(){}
}
Constant pool:
#1 = Methodref #5.#15 // java/lang/Object."<init>":()V
#2 = String #16 // A
#3 = Fieldref #4.#17 // Test.filedA:Ljava/lang/String;
#4 = Class #18 // Test
#5 = Class #19 // java/lang/Object
#6 = Utf8 filedA
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 methodA
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #8:#9 // "<init>":()V
#16 = Utf8 A
#17 = NameAndType #6:#7 // filedA:Ljava/lang/String;
#18 = Utf8 Test
#19 = Utf8 java/lang/Object
可以看到相关变量和方法的地址都是用符号代替的。
解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符具体解析过程这里不再赘述。
初始化
初始化为类加载的最后一步,主要是为标记为常量的类变量赋值,以及执行clinit方法,除了常量以外其他在声明时直接赋值的操作和静态代码块中的内容都将被放入clinit方法中
public class Test{
static int test=19;
static char testB='S';
static {
testB='A';
}
}
{
static int test;
descriptor: I
flags: ACC_STATIC
static char testB;
descriptor: C
flags: ACC_STATIC
public Test();
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 1: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 19
2: putstatic #2 // Field test:I
5: bipush 83
7: putstatic #3 // Field testB:C
10: bipush 65
12: putstatic #3 // Field testB:C
15: return
LineNumberTable:
line 2: 0
line 3: 5
line 5: 10
line 6: 15
}
可以看到对静态变量的赋值和静态代码块的内容都被放到后面的static{}中,由此可知静态变量赋值与静态代码块是同级的,事实上,如果你试图在一个静态变量未声明之前就在静态代码块中对此变量赋值是无法通过编译的
//编译不通过
static{
filed="";
}
static String filed="";