JAVA类加载阶段详解

JAVA类加载阶段详解

首先我们先来看一道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo03_5 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E{
public static final int a = 10;
public static final String b = "20";
public static Integer c = 30;
static{
System.out.println("E init!");
}
}

问最后输出什么?


从大的阶段来说,类加载一共分为3个阶段,分别为加载,链接,初始化 .

加载

在类被编译成class文件后,需要通过类加载器,加载带方法区中.内部采用的是C++的instanceKlass描述java类,它的重要属性有:

  • _java_mirror 即java的镜像类,例如对String来说,就是String.class,作用是把Klass暴露给java使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类的父类还没有被加载,先加载父类.

加载和链接是可能交替运行的.

链接

链接又分为三个小阶段,分别为验证,准备,解析.

验证

验证:验证类是否符合JVM规范,进行安全性检查.

这里我们使用Sublime等支持二进制的编辑器修改class的魔术,将cafe babe改成cafe baby,然后再控制台运行java类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.lang.ClassFormatError: Incompatible magic value 1667327589 in class file Demo03_1
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"

可以看出,抛出了一个ClassFormatError,这是由于类的格式错误所导致,这就是在验证阶段所做的事情.

准备

准备:为static变量分配空间,设置默认值

  • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值是在初始化阶段完成
  • 如果static变量是final类型的基本类型或字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果static变量是fianl类型的,但属于引用类型,那么赋值在初始化阶段完成

通过下面的例子,我们来确定static变量分配空间和赋值是两个步骤

1
2
3
4
5
6
7
public class Demo03_2 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "30";
static final String e = new String("40");
}

然后使用javap反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
static int a;
descriptor: I
flags: ACC_STATIC

static int b;
descriptor: I
flags: ACC_STATIC

static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20

static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String 30

static final java.lang.String e;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL

public demo03.Demo03_2();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo03/Demo03_2;

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: bipush 10 //准备个常量10
2: putstatic #2 // Field b:I 赋值给b
5: new #3 // class java/lang/String
8: dup
9: ldc #4 // String 40
11: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
14: putstatic #6 // Field e:Ljava/lang/String;
17: return
LineNumberTable:
line 5: 0
line 8: 5
}

首先,我们先了解下,在初始化话过程,会执行static{}方法,也就是方法.

static int a;

通过字节码我们可以看出,只有对a的声明,并没有对a进行赋值.

因为我们只设定a为默认值,所以没有在方法中进行赋值.

static int b = 10;

1
2
0: bipush        10                  //准备个常量10
2: putstatic #2 // Field b:I 赋值给b

我们可以看出是在方法中对b进行赋值

static final int c = 20;

1
2
3
4
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20

我们可以看出,没有进入方法,就已经对c进行了赋值.

也就是没有执行初始化方法,所以final类型的基本类型是在准备阶段赋值.

static final String d = “30”;

1
2
3
4
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String 30

我们可以看出,final类型的字符串常量也是在准备阶段赋值

static final String e = new String(“40”);

1
2
3
4
5
5: new           #3                  // class java/lang/String
8: dup
9: ldc #4 // String 40
11: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
14: putstatic #6 // Field e:Ljava/lang/String;

我们可以看出final类型的引用类型,是在初始化阶段赋值.


解析

解析:将常量池中的符号引用解析为直接引用

这里我们可以通过Classloader.loadClass方法,不会触发解析和初始化的特性.来通过HSDB工具.

来查看,在未触发解析是,常量池中的引用对象,只是一个符号,并不知道具体的类的地址.只有在解析过后,才会把符号变为直接引用.

由于篇幅的原因,这里就不演示了.

初始化

初始化:初始化即调用类的()v方法,改方法是在编译期生成,虚拟机会保证这个类的方法线程安全.

初始化发生的时机

发生初始化的情况:

  • main方法所在的类,总是会被首先初始化
  • 首次访问该类的静态变量或者静态方法时
  • 子类初始化,如果父类还没有初始化,会引发父类的初始化
  • 子类访问父类的静态变量,只触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串常量)不会触发初始化
  • 类对象.class不会触发初始化
  • 新建数组对象不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName 的参数2 为false

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

public class Demo03_4 {
// static{
// // main方法所在的代码块总是先执行初始化
// System.out.println("main init");
// }
public static void main(String[] args) throws ClassNotFoundException {
// //final类型静态常量不会触发初始化
// System.out.println(B.b);
// // 访问类的class对象不会触发初始化,因为在java阶段就已经生成_java_mirror,也就是class对象
// System.out.println(B.class);
// //创建类的数组不会导致初始化
// B[] arr =new B[5];
// //类加载器的loadClass方法
// ClassLoader c1 = Thread.currentThread().getContextClassLoader();
// c1.loadClass("demo03.B");
// //Class.forName 的参数2 为false
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("demo03.B",false,c2);


// 会触发类初始化的情况
// //首次访问这个类的静态变量或者静态方法时
// System.out.println(A.a);
// // 子类初始化,会先触发父类初始化,父类初始化在子类初始化之前
// System.out.println(B.c);
// //子类调用父类的静态变量,只触发父类的初始化
// System.out.println(B.a);
// //Class.forName
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("demo03.B");
// //new 方法
// new A();
}
}

class A {
static int a = 10;
static{
System.out.println("a init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b.init");
}
}

现在,我们再回过头来看开始的那道面试题

很明显输出为:

1
2
3
4
10
20
E init!
30

最后,我们再学习一个类加载的应用

利用类加载机制写一个单例类

话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private Singleton(){}

//私有内部类来保存单例
private static class LazyHolder{
static final Singleton INSTANCE = new Singleton();
}

//第一次调用getInstance的时候,才会导致内部类的加载和初始化
//而jvm会保证类的初始化为线程安全的. 所以只会创建一个Singleton实例
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化的时候线程安全是有保障的

以上仅为个人观点,如有错误,欢迎指正.谢谢!

0%