1、类加载

1、图示结构

alt

2、类文件结构
  • 一个简单的 HelloWorld.java

    alt

  • 执行 javac -parameters -d . HellowWorld.java

  • 通过 javac 类名.java 编译 java 文件后,会生成一个 .class 的文件!

  • 以下是字节码文件:

    alt

  • 根据 JVM 规范,类文件结构如下

    alt

3、魔数
  • u4 magic

  • 对应字节码文件的 0~3 个字节,意思是 .class 文件,

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • 不同的东西有不同的魔数,比如 jpg、png 图片等!

4、版本
  • u2 minor_version;
  • u2 major_version;
  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
  • 00 00 00 34:34H(16进制) = 52(10进制),代表JDK8
5、常量池及其他类结构

2. 字节码指令---执行流程

  • 字节码详情解析:链接
1、先编译以下java代码
public class demo1 {    
	public static void main(String[] args) {        
		int a = 10;        
		int b = Short.MAX_VALUE + 1;        
		int c = a + b;        
		System.out.println(c);   
    } 
}

2、使用 javap 命令反编译:
  • javap -v D:\idea-code\out\demo1.class

    alt

3、常量池载入运行时常量池
  • 常量池也属于方法区,只不过这里单独提出来了

    alt

4、方法字节码载入方法区
  • 方法区中放入字节码指令

    alt

5、 main 线程开始运行,分配栈帧内存
  • stack=2,locals=4 对应操作数栈有 2 个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。

    alt

6、执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)

  • ldc 将一个 int 压入操作数栈

  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)

  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

    alt

istore 1

  • 将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中

  • 对应代码中的 a = 10

    alt

ldc #3

  • 读取运行时常量池中 #3 ,即 32768 (超过 short 最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中

  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的。

    alt

istore 2

  • 将操作数栈中的元素弹出,放到局部变量表的2 号位置

    alt

iload1

  • 将局部变量表中 1 号位置的元素放入操作数栈中

    alt

iload2

  • 将局部变量表中 2 号位置的元素放入操作数栈中,因为只能在操作数栈中执行运算操作

    alt

iadd

  • 将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中

    alt

istore 3

  • 将操作数栈中的元素弹出,放入局部变量表的3号位置

    alt

getstatic #4

  • 在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中

    alt

iload 3

  • 将局部变量表中 3 号位置的元素压入操作数栈中。

    alt

invokevirtual #5

  • 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法

  • 生成新的栈帧(分配 locals、stack等)

  • 传递参数,执行新栈帧中的字节码

    alt

执行完毕,弹出栈帧

  • 清除 main 操作数栈内容

    alt

return

  • 完成 main 方法调用,弹出 main 栈帧
  • 程序结束

3、通过字节码指令分析练习

1、a++ 问题
  • 代码
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
  public static void main(String[] args) {
    int a = 10;
    int b = a++ + ++a + a--;
    System.out.println(a);
    System.out.println(b);
  }
}

  • 反编译字节码
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=3, args_size=1
      0: bipush 10	 //把10放入操作数栈中
      2: istore_1	 //将操作数栈中的10弹出,放入局部变量表的1号位置(a处)
      3: iload_1	 //将局部变量表中1号位置的元素10压入操作数栈中
      4: iinc 1, 1	 //局部变量1号位置上的数字自增1(此时a=11)
      7: iinc 1, 1	 //局部变量1号位置上的数字自增1(此时a=12)
      10: iload_1	 //将局部变量表中1号位置的元素12压入操作数栈中
      11: iadd	    //栈中两数相加,10+12=22,弹出10,12,栈中只留下22
      12: iload_1	 //将局部变量表中1号位置的元素12压入操作数栈中
      13: iinc 1, -1	//局部变量1号位置上的数字自减1(此时a=11)
      16: iadd		//栈中两数相加,12+22=34,弹出12,22,栈中只留下34
      17: istore_2	//将操作数栈中的34弹出,放入局部变量表的2号位置(b处)
      18: getstatic #2 // Field
  • 注意 iinc 指令是直接在局部变量 slot 上进行运算
  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
2、 条件判断指令
  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码
  • 源码
public class Demo3_3 {
  public static void main(String[] args) {
    int a = 0;
    if(a == 0) {
    	a = 10;
    } else {
    	a = 20;
    }
  }
}
  • 反编译字节码
0: iconst_0		//把0入栈,iconst表示常量,范围是-1-5;
1: istore_1		//将操作数栈中的0弹出,放入局部变量表的1号位置(a处)
2: iload_1		//将局部变量表中1号位置的元素0压入操作数栈中
3: ifne 12		//ifne判断是否不等于0,不等于0则跳转第12行指令
6: bipush 10	//把10放入操作数栈中
8: istore_1		//将操作数栈中的10弹出,放入局部变量表的1号位置(a处)
9: goto 15		//跳转第15行指令
12: bipush 20	//把20放入操作数栈中
14: istore_1	//将操作数栈中的20弹出,放入局部变量表的1号位置(a处)
15: return

  • long,float,double 的比较: 链接

3、x=0

  • 源码:
public class Code_11_ByteCodeTest {
    public static void main(String[] args) {

        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x); // 0
    }
}
  • 反编译字节码
Code:
     stack=2, locals=3, args_size=1	// 操作数栈分配2个空间,局部变量表分配 3 个空间
        0: iconst_0	// 准备一个常数 0
        1: istore_1	// 将常数 0 放入局部变量表的 1 号槽位 i = 0
        2: iconst_0	// 准备一个常数 0
        3: istore_2	// 将常数 0 放入局部变量的 2 号槽位 x = 0	
        4: iload_1		// 将局部变量表 1 号槽位的数放入操作数栈中
        5: bipush 10	// 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
        7: if_icmpge 21	// 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
       10: iload_2		// 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0 
       11: iinc 2, 1	// 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值为 1 
       14: istore_2	//将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了0
       15: iinc 1, 1  // 1 号槽位的值自增 1 
       18: goto  4    //跳转到第4条指令
       21: getstatic #2  //Field java/lang/System.out:Ljava/io/PrintStream;
       24: iload_2
       25: invokevirtual #3  // Method java/io/PrintStream.println:(I)V
       28: return

4、构造方法
  • 源码
public class Code_12_CinitTest {
	static int i = 10;

	static {
		i = 20;
	}

	static {
		i = 30;
	}

	public static void main(String[] args) {
		System.out.println(i); // 30
	}
}

  • 反编译字节码
stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #3                  // Field i:I
         5: bipush        20
         7: putstatic     #3                  // Field i:I
        10: bipush        30
        12: putstatic     #3                  // Field i:I
        15: return

  • 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V

init()V

  • 源码
public class Code_13_InitTest {

    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Code_13_InitTest(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Code_13_InitTest d = new Code_13_InitTest("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }

}
  • 编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
Code:
     stack=2, locals=3, args_size=3
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: aload_0
        5: ldc           #2                  // String s1
        7: putfield      #3                  // Field a:Ljava/lang/String;
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       // 原始构造方法在最后执行
       28: aload_0
       29: aload_1
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2
       35: putfield      #4                  // Field b:I
       38: return
5、方法调用
  • 源码
public class Code_14_MethodTest {

    public Code_14_MethodTest() {

    }

    private void test1() {

    }

    private final void test2() {

    }

    public void test3() {

    }

    public static void test4() {

    }

    public static void main(String[] args) {
        Code_14_MethodTest obj = new Code_14_MethodTest();
        obj.test1();
        obj.test2();
        obj.test3();
        Code_14_MethodTest.test4();
    }
}


  • 不同方法在调用时,对应的虚拟机指令有所区别
  • 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令
  • 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
  • 静态方法在调用时使用 invokestatic 指令
Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  //
         3: dup // 复制一份对象地址压入操作数栈中
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4                  // Method test1:()V
        12: aload_1
        13: invokespecial #5                  // Method test2:()V
        16: aload_1
        17: invokevirtual #6                  // Method test3:()V
        20: invokestatic  #7                  // Method test4:()V
        23: return

  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”: ()V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
  • 终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】

6、多态原理

  • 因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
  • 在执行 invokevirtual 指令时,经历了以下几个步骤
  • 先通过栈帧中对象的引用找到对象
  • 分析对象头,找到对象实际的 Class
  • Class 结构中有 vtable
  • 查询 vtable 找到方法的具体地址
  • 执行方法的字节码
7、异常处理

try-catch

  • 源码
public class Code_15_TryCatchTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (Exception e) {
            i = 20;
        }
    }

}
  • 反编译字节码
Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          12
        8: astore_2
        9: bipush        20
       11: istore_1
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/Exception

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置为 e

多个 single-catch

  • 源码
public class Code_16_MultipleCatchTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (ArithmeticException e) {
            i = 20;
        }catch (Exception e) {
            i = 30;
        }
    }
}
  • 反编译字节码
Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          19
        8: astore_2
        9: bipush        20
       11: istore_1
       12: goto          19
       15: astore_2
       16: bipush        30
       18: istore_1
       19: return
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/ArithmeticException
            2     5    15   Class java/lang/Exception

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

finally

  • 源码
public class Code_17_FinallyTest {
    
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}
  • 反编译字节码
Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        // try块
        2: bipush        10
        4: istore_1
        // try块执行完后,会执行finally    
        5: bipush        30
        7: istore_1
        8: goto          27
       // catch块     
       11: astore_2 // 异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       // catch块执行完后,会执行finally        
       15: bipush        30
       17: istore_1
       18: goto          27
       // 出现异常,但未被 Exception 捕获,会抛出其他异常,这时也需要执行 finally 块中的代码   
       21: astore_3
       22: bipush        30
       24: istore_1
       25: aload_3
       26: athrow  // 抛出异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

  • 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
  • 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次

finally 中的 return

  • 源码
public class Code_18_FinallyReturnTest {

    public static void main(String[] args) {
        int i = Code_18_FinallyReturnTest.test();
        // 结果为 20
        System.out.println(i);
    }

    public static int test() {
        int i;
        try {
            i = 10;
            return i;
        } finally {
            i = 20;
            return i;
        }
    }
}

  • 反编译字节码
Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0
        3: iload_0
        4: istore_1  // 暂存返回值
        5: bipush        20
        7: istore_0
        8: iload_0
        9: ireturn	// ireturn 会返回操作数栈顶的整型值 20
       // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
       10: astore_2
       11: bipush        20
       13: istore_0
       14: iload_0
       15: ireturn	// 这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常!
     Exception table:
        from    to  target type
            0     5    10   any

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
  • 所以不要在finally中进行返回操作

被吞掉的异常

public static int test() {
      int i;
      try {
         i = 10;
         //  这里应该会抛出异常
         i = i/0;
         return i;
      } finally {
         i = 20;
         return i;
      }
   }

  • 会发现打印结果为 20 ,并未抛出异常

finally 不带 return

  • 源码
public static int test() {
		int i = 10;
		try {
			return i;
		} finally {
			i = 20;
		}
	}

  • 反编译字节码
Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0 // 赋值给i 10
        3: iload_0	// 加载到操作数栈顶
        4: istore_1 // 加载到局部变量表的1号位置
        5: bipush        20
        7: istore_0 // 赋值给i 20
        8: iload_1 // 加载局部变量表1号位置的数10到操作数栈
        9: ireturn // 返回操作数栈顶元素 10
       10: astore_2
       11: bipush        20
       13: istore_0
       14: aload_2 // 加载异常
       15: athrow // 抛出异常
     Exception table:
        from    to  target type
            3     5    10   any

8、Synchronized
  • 源码
public class Code_19_SyncTest {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }

}


  • 反编译字节码
Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
         8: aload_1 // 加载到操作数栈
         9: dup // 复制一份,放到操作数栈,用于加锁时消耗
        10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
        11: monitorenter // 加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2 // 加载对象到栈顶
        21: monitorexit // 释放锁
        22: goto          30
        // 异常情况的解决方案 释放锁!
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
        // 异常表!
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any

4、语法糖(编译期处理)

  • 所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

5、类加载

1、类加载的机制及过程
  • 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化

    alt

2、加载
  • 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

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

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

    alt

  • instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中

  • _java_mirror则是保存在堆内存中

  • InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址

  • 类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看
3、连接

验证

  • 验证类是否符合 JVM规范,安全性检查
  • 用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

准备

  • 为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成将常量池中的符号引用解析为直接引用

解析

  • 虚拟机常量池的符号引用替换为字节引用过程
4、初始化

< cinit>()V 方法

  • 初始化阶段是执行类构造器< clinit>() 方法的过程。类构造器< clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

  • 概括得说,类初始化是【懒惰的】
  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
public class Load1 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
//         System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
//         System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
//         System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
//         ClassLoader cl = Thread.currentThread().getContextClassLoader();
//         cl.loadClass("cn.ali.jvm.test.classload.B");
        // 5. 不会初始化类 B,但会加载 B、A
//         ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//         Class.forName("cn.ali.jvm.test.classload.B", false, c2);


        // 1. 首次访问这个类的静态变量或静态方法时
//         System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
//         System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
//         System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
//         Class.forName("cn.ali.jvm.test.classload.B");
    }

}


class A {
    static int a = 0;
    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");
    }
}

5、练习
  • 从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
public class Load2 {

    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        // 会导致 E 类初始化,因为 Integer 是包装类
        System.out.println(E.c);
    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;

    static {
        System.out.println("E cinit");
    }
}


  • 典型应用 - 完成懒惰初始化单例模式
public class Singleton {

    private Singleton() { } 
    // 内部类中保存单例
    private static class LazyHolder { 
        static final Singleton INSTANCE = new Singleton(); 
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员 
    public static Singleton getInstance() { 
        return LazyHolder.INSTANCE; 
    }
}


以上的实现特点是:

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

6、类加载器

1、类加载器的介绍

alt

  • BootstrapClassLoader(启动类加载器)

    可通过在控制台输入指令,使得类被启动类加器加载

  • ExtensionClassLoader(标准扩展类加载器)

    如果 classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。

  • AppClassLoader(系统类加载器)

    负责记载classpath中指定的jar包及目录中class

  • CustomClassLoader(自定义加载器)

    属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

2、类加载器的顺序(双亲委派机制)
  • 双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

    alt

  • 双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。