JVM 类加载机制 超详细学习笔记(三)

本篇博客是记录个人学习JVM的笔记,学习的视频是哔哩哔哩中 黑马的jvm教程;

视频链接:jvm学习

上一篇文章:JVM 垃圾回收 超详细学习笔记(二)未来很长,别只看眼前的博客-CSDN博客

目录

1、类文件结构

魔数

版本

常量池部分

javap-详细分析字节码(熟悉)

2、字节码指令

图解方法执行的流程(字节码)

常量池载入运行时常量池:

 方法字节码载入方法区:

 执行引擎开始执行main线程中的字节码

用字节码详解i++与++i

练习---分析x=0

构造方法cinit()V

构造方法init()V

字节码分析方法调用

字节码分析多态原理

字节码分析异常处理

try-catch

多个 single-catch

finally(重点)

finally 中的 return

字节码分析Synchronized

3、编译期处理

默认构造函数

自动拆装箱

泛型集合取值

可变参数...

foreach

switch字符串

枚举类

匿名内部类

4、类加载阶段

加载阶段

链接阶段

初始化阶段(熟悉)

练习---理解初始化

5、类加载器

启动类加载器BootStrap

扩展类的加载器Extension

双亲委派模式(重点)

自定义类加载器

破坏双亲委派模式

6、运行期优化

即时编译器(JIT)与解释器

对象逃逸状态

逃逸分析优化

锁消除

四、Java内存模型


先来大概看看jvm的结构:

1、类文件结构

根据 JVM 规范,类文件结构如下:这些东西先了解就行,主要是会观看javap -v 之后的字节码就行,这个二进制的不做要求;

u4 			   magic			//u4表示的4个字节  魔数
u2             minor_version;  //u2表示的是两个字节    版本 
u2             major_version;    //版本
u2             constant_pool_count;  //常量池   
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文件,然后使用一些可以读取二进制文件的工具打开,就可以看到二进制的内容了:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 
0001120 00 00 02 00 14

魔数

u4 magic 表示的是前面四个字节(当然有时候并不一定是4个字节)表示的是魔数,下面加粗的部分; 对应字节码文件的 0~3 个字节; 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 ca fe ba be :意思是 .class 文件,不同的东西有不同的魔数,比如 jpg、png 图片等!

魔数的作用是表示文件的类型;

版本

4~7字节,表示类的版本,00 34(52)表示的是Java8

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

常量池部分

8~9字节,表示常量池长度,00 23(35)表示常量池有#1~#34项,注意#0 项不计入,也没有值

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

第#1项0a表示一个method信息,00 06 15(8进制的15翻译成10进制为21)表示它引用了常量池中#6和#21项来获得这个方法的【所属类】和【方法名】

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

等等......

有一个专门的表格,对着表格去看。。。。。。。

可以使用idea的一个插件来帮助阅读:

 安装完成后:

 自己看这些二进制的文件一个一个对着表去找,有点太麻烦了,所以Oracle就提供了Javap工具来反编译class文件,反编译后的文件可阅读性更加强;

javap-详细分析字节码(熟悉)

javap -v HelloWorld.class   (对class文件进行反编译)

Classfile /E:/jvm_learn/out/production/jvm_learn/classLoad/HelloWorld.class
  Last modified 2022-6-23; size 553 bytes  //表示文件最后修改时间  以及文件大小
  MD5 checksum afcde712c515c318c66483094430ce48 //md5的矫正签名
  Compiled from "HelloWorld.java" //编译后的源文件
public class classLoad.HelloWorld //类的全路径名称
  minor version: 0
  major version: 52  //代表jdk8
  flags: ACC_PUBLIC, ACC_SUPER //类的访问修饰符
Constant pool:  //常量池,下面的#都是引用关系和给jvm查找使用的
   #1 = Methodref          #6.#20         //这个给的注释,是通过查询常量池表后最后的查询结果就直接告诉我们来,不需要我们再对着这个常量池一个一个去查询了 java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V   string类型的参数  返回的是void
   #5 = Class              #26            // classLoad/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LclassLoad/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               classLoad/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{  //方法信息  
  public classLoad.HelloWorld();  //构造方法
    descriptor: ()V  //方法的参数信息  v  表示void 
    flags: ACC_PUBLIC //访问修饰符
    Code: //代码
      stack=1, locals=1, args_size=1  //分别是 栈道最大操作深度 局部变量表的长度 参数的长度
         0: aload_0 //把局部变量的第0项加载到操作数栈
         1: invokespecial #1//调用方法,常量池中的第一项    // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:  //方法中的属性,对应着一个行号表
        line 7: 0 //前面的行号7代表的是源代码中的行号,后面的行号0代表的是字节码中的行号
      LocalVariableTable: //对应的是本地变量表
        Start  Length  Slot  Name   Signature
            0       5     0  this   LclassLoad/HelloWorld;
		//start表示的是字节码的起始范围,length表示的是【作用的范围】  slot表示的是槽位号  name表示的是变量名  Signature表示的是局部变量的类型
  public static void main(java.lang.String[]); //main 方法
    descriptor: ([Ljava/lang/String;)V //方法的参数是string
    flags: ACC_PUBLIC, ACC_STATIC  //访问修饰符是public static
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3 //ldc表示的是加载常量        // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:  //源代码的行号和字节码中的行号
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

2、字节码指令

图解方法执行的流程(字节码)

public class Demo {    
	public static void main(String[] args) {        
		int a = 10;        
		int b = Short.MAX_VALUE + 1;        
		int c = a + b;        
		System.out.println(c);   
    } 
}

编译后的字节码文件:放在这里仅供参考对照,下面将会使用图解的形式来解释这段代码的字节码指令;

Classfile /E:/jvm_learn/out/production/jvm_learn/classLoad/Demo.class
  Last modified 2022-6-23; size 594 bytes
  MD5 checksum 36ce1276d35f9fadf2c98315263b059b
  Compiled from "Demo.java"
public class classLoad.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // classLoad/Demo
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               LclassLoad/Demo;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               classLoad/Demo
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public classLoad.Demo();
    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   LclassLoad/Demo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 12: 10
        line 13: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo.java"

图解:这样一段Java代码执行的时候回发生什么?

会由Java虚拟机中的类加载器来帮我们加载,把这些字节码加载到内存;而字节码文件中的常量池会被加载到运行时常量池;(这个运行时常量池属于方法区的一个部分)

常量池载入运行时常量池:

 32767 :是short的最大取值;

Java源码中一些较小的数值并不是存储在常量池的而是跟着这个方法的字节码指令存放在一起,一旦这个数值的大小超过了这个short的最大值,那么它就会存储在常量池中

从上面给的参考字节码也可以看出:

0: bipush        10   //a为10
line 9: 0			  //9为源代码中的第九行
#3 = Integer            32768 

 方法字节码载入方法区:

然后main线程开始运行,分配栈帧内存;

(stack=2,locals=4) 对应操作数栈的深度是2,局部变量表的长度为4

 执行引擎开始执行main线程中的字节码

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

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

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

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

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

bipush 10 : 表示的是将 10 这个数值压入栈中

 istore 1:将操作数栈顶数据弹出,存入局部变量表的 slot 1

对应代码中的:a = 10

 ldc #3:

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

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

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

iload1 iload2运算操作是要栈中完成的,在局部变量表中不能完成运算;

将局部变量表中1号位置和2号位置的元素读取出来放到操作数栈中;

 

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

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

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

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

 invokevirtual 5

  • 找到常量池 #5 项

  • 定位到方法区 java/io/PrintStream.println:(I)V 方法

  • 发现是方法后,由虚拟机分配一个新的栈帧(分配 locals、stack等)

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

 执行完毕,弹出栈帧

清除 main 操作【数栈】内容

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

用字节码详解i++与++i

参考本人的另一篇博客:

从字节码角度带你彻底理解i++与++i_未来很长,别只看眼前的博客-CSDN博客https://blog.csdn.net/weixin_53142722/article/details/125434390

练习---分析x=0

注意:while和for循环的字节码,你是发现它们是一模一样的,殊途也能同归;

public class Test {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;  //i++  先执行ilaod_x 把局部变量中的x加载到操作数栈中,此时操作数栈中的x为0,然后执行iinc_x 1 ,把局部变量表中的x自增1,所以局部变量表中的x为1,然后又做了一个赋值操作把操作数栈中的x给赋值给局部变量表中的x,所以局部变量表中的x又变为0 ,循环10次后x依旧为0
            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

构造方法cinit()V

先看一道题:

public class CinitTest {
	static int i = 10;
	static {
		i = 20;
	}
	static {
		i = 30;
	}
	public static void main(String[] args) {
		System.out.println(i); // 30
	}
}

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :(这个方法是没有返回值也没有参数), 这个方法会在类的初始化阶段就会被调用;这个cinit()v是整个类的构造方法;

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

构造方法init()V

这个init()V 构造方法是每个类的构造方法;

先来看一道题:

public class InitTest {

    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

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

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法(比如上面这个题中的InitTest(String a, int b)构造方法就是原始构造方法)内的代码也会附加到这个新的构造方法中,附加到最后;

下面是代码对应的字节码:

Code:
     stack=2, locals=3, args_size=3
        0: aload_0      //把this加载
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: aload_0
        5: ldc           #2                  // String s1  把s1加载到数栈
        7: putfield      #3           // Field a:Ljava/lang/String;  相当于把s3赋值给this.a
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I  把20赋值给this.b
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I  把10赋值给this.b
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       // 原始构造方法在最后执行 ---------下面是原始构造方法中的代码对应的字节码
       28: aload_0				//加载this
       29: aload_1				//加载局部变量slot 1(a)  对应着”s3"
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2	//加载局部变量slot 2(b)  对应着30  在局部变量表中找到的,这里没给出来
       35: putfield      #4                  // Field b:I
       38: return

字节码分析方法调用

看一下几种不同方法调用对应的字节码指令:

public class Demo5 {
	public Demo5() {

	}

	private void test1() {

	}

	private final void test2() {

	}

	public void test3() {

	}

	public static void test4() {

	}

	public static void main(String[] args) {
		Demo5 demo5 = new Demo5();
		demo5.test1(); //私有
		demo5.test2(); //final
		demo5.test3(); //公共
		Demo5.test4(); //静态
	}
}

不同方法在调用时,对应的虚拟机指令有所区别

  • 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令

  • 普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定

  • 静态方法在调用时使用invokestatic指令

  • 注意:这里的invokespecial和invokestatic都是静态绑定,在字节码阶段就可以知道调用的是哪一个类的哪个方法,这个invokevirtual是公有的方法可能会出现重写,所以这个带public 的方法在字节码阶段是不知道这个方法具体是哪个类的,属于动态绑定

code:	 
		 stack=2, locals=2, args_size=1
		 0: new           #2                  // class classLoad/Demo5   这个new分为两个步骤:调用构造方法首先会在堆中分配一块空间,然后空间分配成功后会把这个对象的引用放入操作数栈 
         3: dup  //把刚刚创建的对象的引用地址复制一份,放到栈顶  ;为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”:()V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
         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  final修饰的方法
        16: aload_1
        17: invokevirtual #6                  // Method test3:()V  public修饰的方法
        20: invokestatic  #7                  // Method test4:()V  静态方法,因为静态方法不会被对象调用,所以静态方法一调用后就直接出栈了,所以平常也不要使用对象来调用静态方法了,不然会多产生一些不必要的指令
        23: return

注意:new这个过程实际上是包括两步:开辟空间,空间分配成功后会把这个对象的引用放入操作数栈 (这一份是拷贝的对象的地址值);

字节码分析多态原理

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令

在执行invokevirtual指令时,经历了以下几个步骤

  • 先通过栈帧中对象的引用找到对象

  • 分析对象头,找到对象实际的Class

  • Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

  • 查询vtable找到方法的具体地址

  • 执行方法的字节码

字节码分析异常处理

​​​​​​​try-catch

public class 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 static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (ArithmeticException e) {
            i = 30;
        }catch () {
            i = 40;
        }catch(Exception e){
            i = 50;
        }
    }
Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10  
         4: istore_1
         5: goto          26 //发生异常跳到26行
         8: astore_2      //存储异常对象的引用
         9: bipush        30
        11: istore_1
        12: goto          26 //发生异常跳到26行
        15: astore_2      //存储异常对象的引用
        16: bipush        40
        18: istore_1
        19: goto          26 //发生异常跳到26行
        22: astore_2	  //存储异常对象的引用
        23: bipush        50
        25: istore_1
        26: return
      Exception table:
         from    to  target type   //这里的target和上面的行号对应
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
 LineNumberTable....
      LocalVariableTable:   //Slot  这么多个2,是为了复用,因为这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I

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

finally(重点)

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

对应字节码:

从下面字节码中我们可以看到,finally的作用是把finally中的代码快复制多分,然后分别放到try代码块后,catch代码块后(goto指令前),但是有时候catch并不能完全catch你想要的exception,所以这个字节码指令会多一个保障,就是在异常表中多捕获一个异常2 5 21 any(这个是对应下面的一条指令),和对catch多捕获一个any的异常11 15 21 any(这个是对应下面的一条指令);

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        //try块
        2: bipush        10           //-----------try   try的范围可以从异常表中查询到
        4: istore_1
        //try块执行完后,会执行finally    
        5: bipush        30           //-----------fainal
        7: istore_1					  // 把30赋值给i,这个30的赋值是在finally代码块中的
        8: goto          27
       //catch块     
       11: astore_2 //把异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       //catch块执行完后,会执行finally          
       15: bipush        30          //-----------fainal
       17: istore_1
       18: goto          27
       //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
       21: astore_3
       22: bipush        30         //-----------fainal
       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 FinallyReturnTest {

    public static void main(String[] args) {
        int i = 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      //slot 0  (从栈顶移除了)
        3: iload_0
        4: istore_1  // 暂存返回值
        5: bipush        20
        7: istore_0    //20这个值对10进行了覆盖
        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;   //最后的结果是返回10  !!!
		}
	}
Code:
     stack=1, locals=3, args_size=0
        0: bipush        10  //把10放入栈顶
        2: istore_0 // 把10存储在局部变量表的0号槽位
        3: iload_0	// 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
        4: istore_1 // 加载到局部变量表的1号位置,【目的是为了固定返回值】
        5: bipush        20  //------执行finally代码块
        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

我们发现如果在try中进行了return,那么即便finally中的变量发生了变化,那么返回的依旧是try中的变量值,因为我们可以从字节码指令看到try中的变量会先被备份一次用来返回;

字节码分析Synchronized

public class Demo {
    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	//第二份对象的引用赋值给局部变量表中的lock,从这里结束,第一行代码执行完毕
         8: aload_1     //开始进入synchronized代码快,先把对象加载到操作数栈                    
         9: dup			//对这个lock对象的引用进行复制,分别对应加锁和解锁两个阶段使用
        10: astore_2	//把刚刚复制出来的对象引用给存储起来到二号槽位(二号槽位是没有name的)
        11: monitorenter //这个指令会把栈顶的对象引用给消耗掉,对lock引用所执行的对象进行加锁操作
        12: getstatic     #3   // Field  java/lang/System.out:Ljava/io/PrintStream;开始执行打印方法
        15: ldc           #4                  // String ok  ldc表示加载常量
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2   //加载刚刚暂存在局部变量表的lock对象引用
        21: monitorexit  //完成解锁
        22: goto          30
        25: astore_3   //把异常对象的引用存储到局部变量表中的3号槽位
        26: aload_2    //加载刚刚暂存在局部变量表的lock对象引用
        27: monitorexit  //完成解锁
        28: aload_3    //把刚刚那个异常对象从局部变量表中加载到操作数栈中,进行抛出
        29: athrow
        30: return
      Exception table://如果12-22发生异常,那么就会进入到25行指令,是为了保证进入到异常后还能正常解锁
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 12
        line 14: 20
        line 15: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

注意:方法级别的synchronized不会在字节码指令中有所体现;

3、编译期处理

​​​​​​​

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

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

默认构造函数

public class Candy1 {
}

经过编译期优化后: 前提是你没有额外直接写构造方法;

public class Candy1 {
   //这个无参构造器是java编译器帮我们加上的
   public Candy1() {
      //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
      super();
   }
}

自动拆装箱

基本类型和其包装类型的相互转换过程,称为拆装箱,在JDK 5以后,它们的转换可以在【编译期】自动完成

public class Candy2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}

优化后对应的Java代码:

public class Candy2 {
   public static void main(String[] args) {
      //基本类型赋值给包装类型,称为装箱
      Integer x = Integer.valueOf(1);  //会对整数进行自动包装,范围是-128-127之间,这个阶段的整数是不用new的,内存中已经帮你创建好了,只有是没超过这个范围那么就是直接从缓存中取,超过的话才会去new
      //包装类型赋值给基本类型,称谓拆箱
      int y = x.intValue();
   }
}

泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了(泛型擦除),实际的类型都当做了 Object 类型来处理

public class Candy3 {
   public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();
      list.add(10);  //实际上是调用 List.add(Object o)
      Integer x = list.get(0); //实际上是调用 Object bbj = List.get(int index);
   }
}

可变参数...

public class Candy4 {
   public static void foo(String... args) {
      //将args赋值给arr,可以看出String...实际就是String[] 
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo("hello", "world");
   }
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:

public class Candy4 {
   public Demo4 {}

    
   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo(new String[]{"hello", "world"});
   }
}

注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null;

foreach

public class Candy5 {
	public static void main(String[] args) {
        //数组赋初值的简化写法也是一种语法糖。
		int[] arr = {1, 2, 3, 4, 5};
		for(int x : arr) {  //编译之后就是for循环
			System.out.println(x);
		}
	}
}

编译器会帮我们转换为:

public class Candy5 {
    public Candy5 {}

	public static void main(String[] args) {
		int[] arr = new int[]{1, 2, 3, 4, 5};
		for(int i=0; i<arr.length; ++i) {
			int x = arr[i];
			System.out.println(x);
		}
	}
}

如果是集合使用foreach

public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      for (Integer x : list) {
         System.out.println(x);  //编译器优化后会变成迭代器的遍历
      }
   }

注意:集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator

public class Candy6 {
    public Candy6 {}
    
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      //获得该集合的迭代器
      Iterator<Integer> iterator = list.iterator();
      while(iterator.hasNext()) {
         Integer x = iterator.next();
         System.out.println(x);
      }
   }
}

switch字符串

public class Demo {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

在编译器中执行的操作:

public class Demo {
   public Demo() {
      
   }
   public static void main(String[] args) {
      String str = "hello";
      int x = -1;
      //通过字符串的hashCode+value来判断是否匹配
      switch (str.hashCode()) {
         //hello的hashCode
         case 99162322 :
            //再次比较,因为字符串的hashCode有可能相等
            if(str.equals("hello")) {
               x = 0;
            }
            break;
         //world的hashCode
         case 11331880 :
            if(str.equals("world")) {
               x = 1;
            }
            break;
         default:
            break;
      }

      //用第二个switch在进行输出判断
      switch (x) {
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

过程说明:

  • 在编译期间,单个的switch被分为了两个

    • 第一个用来匹配字符串,并给x赋值

      • 字符串的匹配用到了字符串的hashCode,还用到了equals方法

      • 使用hashCode是为了提高比较效率(尽量减少equals次数),使用equals是防止有hashCode冲突(如BM和C.)

    • 第二个用来根据x的值来决定输出语句

枚举类

enum SEX {
   MALE, FEMALE;  //这两个就是class的两个对象,和普通对象的区别是枚举类的实例是有限的,而不同类的实例对象是无限的(你可以通过new关键字来不断创建)
}

转换后的代码:

public final class Sex extends Enum<Sex> {    //被final修饰,枚举类不能继承
   //对应枚举类中的元素
   public static final Sex MALE;    
   public static final Sex FEMALE;    
   private static final Sex[] $VALUES;
   
    static {       
    	//调用构造函数,传入枚举元素的值及ordinal
    	MALE = new Sex("MALE", 0);    
        FEMALE = new Sex("FEMALE", 1);   
        $VALUES = new Sex[]{MALE, FEMALE}; 
   }
 	
   //调用父类中的方法  构造方法是私有的,可以不被使用者去new新的对象
    private Sex(String name, int ordinal) {     
        super(name, ordinal);    
    }
   
    public static Sex[] values() {  
        return $VALUES.clone();  
    }
    public static Sex valueOf(String name) { 
        return Enum.valueOf(Sex.class, name);  
    } 
   
}

匿名内部类

public class Demo {
   public static void main(String[] args) {
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println("running...");
         }
      };
   }
}

编译器转换后的代码:

public class Demo {
   public static void main(String[] args) {
      //用额外创建的类来创建匿名内部类对象
      Runnable runnable = new Demo$1();
   }
}

//创建了一个额外的类,实现了Runnable接口
final class Demo$1 implements Runnable {
   public Demo$1() {}

   @Override
   public void run() {
      System.out.println("running...");
   }
}

如果匿名内部类中引用了局部变量:

public class Demo {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

编译器转换后的代码:

public class Demo {
   public static void main(String[] args) {
      int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

final class Demo$1 implements Runnable {
   //多创建了一个变量
   int val$x;
   //变为了有参构造器
   public Demo$1(int x) {
      this.val$x = x;
   }

   @Override
   public void run() {
      System.out.println(val$x);
   }
}


public class Demo { 
	public static void test(final int x) { //把加final编译器会帮你默认加上的
		Runnable runnable = new Candy11$1(x); 
	} 
}

注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Demo1 对象时,将 x 的值赋值给了 Demo1 对象的 值后,如果不是 final 声明的 x 值发生了改变,匿名内部类则值不一致。

4、类加载阶段

加载阶段

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

    • _java_mirror 即 java 的类镜像(桥梁作用,Java和c++之间的桥梁),例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用

    • _super 即父类

    • _fields 即成员变量

    • _methods 即方法

    • _constants 即常量池

    • _class_loader 即类加载器

    • _vtable 虚方法表

    • _itable 接口方法

  • 如果这个类还有父类没有加载,先加载父类

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

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

  • _java_mirror则是保存在堆内存

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

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

链接阶段

验证阶段:验证类是否符合 JVM规范,安全性检查;

准备阶段:为 static 变量分配空间(jdk8后这个静态变量在堆中),设置默认值

  • static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了

  • static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成,由类构造器cinit()来完成赋值;

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

  • 如果 static 变量是 final 的,但属于引用类型(使用new),那么赋值会在初始化阶段完成(就是字节码的code阶段)

解析阶段:将常量池中的符号引用解析为直接引用;因为符号引用仅仅就是一个符号引用,jvm不知道它的具体含义是什么,但是经过实际解析后jvm就可以知道这个类,方法在内存中实实在在的位置了;

  • 未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中

  • 解析以后,会将常量池中的符号引用解析为直接引用

初始化阶段(熟悉)

cinit()v:

初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的『构造方法』的线程安全

  • clinit()方法是由编译器自动收集【类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并】产生的;

  • 编译器收集的顺序是由代码语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如

 类初始化的发生的时机:

类的初始化的懒惰的,以下情况会初始化

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

以下情况不会初始化

  • 访问类的 static final 静态常量(基本类型和字符串),在类的链接的准备阶段就完成了

  • 类对象.class 不会触发初始化

  • 创建该类对象的数组

  • 类加载器的.loadClass方法

  • Class.forNamed的参数2为false时

验证类是否被初始化,可以看改类的静态代码块是否被执行: 放开自己要测试行代码的注释,运行看有没有触发下面的类A和类B ;

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");
    }
}

练习---理解初始化

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

public class Load2 {

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

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");
    }
}

典型应用 - 完成懒惰初始化单例模式 (静态内部类): 类加载的特性,就是只有第一次使用这个类才会去加载这个类,触发类的加载链接; 只有访问这个内部类才会去加载这个类,就是你第一次去调用这个getInstance 方法的时候才会导致内部类加载和初始化其静态成员(INSTANCE); 这种方式是线程安全性的,由类加载器来保证这个单例的线程安全性;

public class Singleton {

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

5、类加载器

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(ClassLoader);

以JDK 8为例:

名称 加载的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(拓展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,【显示为null】
Application ClassLoader(应用程序类加载器) classpath 上级为Extension
自定义类加载器 自定义 上级为Application

加载的过程有一个层级关系,比如这个application 类加载器加载类的时候会先去询问一下它的上级 Extension类加载器有没有加载这个类,如果没有,那么还会继续委托它的上级BootStrap加载器有没有加载这个类,如果application 类加载器的两个上级类加载器都没有加载这个类,那么此时启动类加载器会在自己可以加载的目录下寻找这个类,如果可以找到的话那么启动类加载器就会把这个类加载到内存中,然后下级的加载器就可以不用来加载这个类了,如果启动类加载器和拓展类加载器即没加载过并且在自己可加载路径中也没找到这个类,那么这个时候才能轮到application 类加载器来加载;

这种加载类型在JVM叫做双亲委派加载;

上级类加载器加载后该类对下级加载器可见 ,但是下级加载的类对上级是不可见的;

启动类加载器BootStrap

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

扩展类的加载器Extension

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

双亲委派模式(重点)

双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则;

  1. 一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给自己的上一级的加载器去执行。

  2. 如果父类(自己上一级)的加载器还存在其父类加载器(自己上一级),则进一步向上委托,依次递归,请求最终会到达顶层的启动类加载器。

  3. 如果父类加载器可以完成类加载任务(就是可以在自己可加载目录下找到这个类),就成功返回,倘若无法完成此加载任务,则委派给它的子加载器(下一级)去加载。 比如有个类加载请求来了,它会一直向上委托,直到启动类加载器(BootStrap);然后启动类加载器尝试加载,如果它不能加载,则会给他的子加载器扩展类加载器加载;如果扩展类加载器还是不能加载;则再到下一级应用程序类加载器。

loadClass源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.首先查找本类是否已经被该类加载器加载过了
        Class<?> c = findLoadedClass(name);
        // 如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 看是否被它的上级加载器加载过了 Extension 的上级是Bootstarp,但它显示为null
                if (parent != null) {
                    //2.有上级的话就会委派上级 loadclass
                    c = parent.loadClass(name, false);
                } else {
                    // 3.看是否被启动类加载器加载过
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }

            if (c == null) {
                // 如果还是没有找到,先让拓展类加载器调用 findClass 方法去找到该类,如果还是没找到,就抛出异常
                // 然后让应用类加载器去找 classpath 下找该类
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

自定义类加载器

哪些情况需要到自定义类加载器?

  • 想加载非 classpath 随意路径中的类文件

  • 通过接口来使用实现,希望解耦时,常用在框架设计

  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤

  • 继承 ClassLoader 父类

  • 要遵从双亲委派机制,重写 findClass 方法

    • 不是重写 loadClass 方法,否则不会走双亲委派机制

  • 读取类文件的字节码

  • 调用父类的 defineClass 方法来加载类

  • 使用者调用该类加载器的 loadClass 方法

破坏双亲委派模式

  • 双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK1.2面世以前的“远古”时代

    • 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法

  • 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的

    • 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式

  • 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的

    • 这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

6、运行期优化

即时编译器(JIT)与解释器

VM 将执行状态分成了 5 个层次:

  • 0层:解释执行,用解释器将字节码翻译为机器码

  • 1层:使用 C1 即时编译器编译执行(不带 profiling)

  • 2层:使用 C1 即时编译器编译执行(带基本的profiling)

  • 3层:使用 C1 即时编译器编译执行(带完全的profiling)

  • 4层:使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等

  • 解释器

    • 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释

    • 是将字节码解释为针对所有平台都通用的机器码

  • 即时编译器

    • 将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译

    • 根据平台类型,生成平台特定的机器码

对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter(解释器) < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),并优化这些热点代码;

C可以提升5倍左右到效率,C2可以提高大约10-100倍的效率;

对象逃逸状态

全局逃逸(GlobalEscape)

  • 即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

    • 对象是一个静态变量

    • 对象是一个已经发生逃逸的对象

    • 对象作为当前方法的返回值

参数逃逸(ArgEscape)

  • 即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的

没有逃逸

  • 即方法中的对象没有发生逃逸

逃逸分析优化

针对上面第三点,当一个对象没有逃逸时,可以得到虚拟机的优化;

逃逸分析优化

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术

逃逸分析的 JVM 参数如下:

  • 开启逃逸分析:-XX:+DoEscapeAnalysis

  • 关闭逃逸分析:-XX:-DoEscapeAnalysis

  • 显示分析结果:-XX:+PrintEscapeAnalysis

逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数;

逃逸优化是在c2编译器中进行的,jit在进行逃逸分析后发现你创建的对象或者是是变量外面的类压根就用不到(比如你在循环中一直new对象),它就会帮你把创建对象的字节码给替换掉;

锁消除

我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁

例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作

锁消除的 JVM 参数如下:

  • 开启锁消除:-XX:+EliminateLocks

  • 关闭锁消除:-XX:-EliminateLocks

锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上;

四、Java内存模型

很多人将【Java内存结构】与【Java内存模型】搞混淆;

java内存模型是 Java memory model (JMM)的意思;简单的说,JMM定义了一套多线程读写共享数据时(成员变量,数组)时,对数据的可见性,有序性,和原子性的规则和保障;

参考我的另一篇博客,有更清晰的讲解:Java并发编程(中下篇)从入门到深入 超详细笔记_未来很长,别只看眼前的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/weixin_53142722/article/details/125423522