JVM学习记录

本文详细探讨了Java虚拟机(JVM)内存结构,包括程序计数器、虚拟机栈、堆、方法区等组件,及其运作机制。进一步阐述了垃圾回收策略,如引用计数法、可达性分析算法及不同回收算法,如标记清除、复制算法等,还涉及了垃圾回收器的选择与调优实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM学习记录


前言


一、JVM是什么?

Java Virtual Machine -java程序运行环境(Java二进制字节码运行环境)

好处:
1.一次编写,到处运行
2.自动内存管理,垃圾回收机制
3.数组下标越界检查(抛出异常,否则覆盖其他内存资源)
4.多态

概念比较:JVM JRE JDK
在这里插入图片描述

二、常见JVM

在这里插入图片描述

三、学习路线

在这里插入图片描述
JVM内存结构→GC垃圾回收→类的字节码结构→类加载器→运行时优化(解释器)

四、内存结构

在这里插入图片描述

1 程序计数器

在这里插入图片描述
蓝色路径是Java源代码的实现过程,源代码→jvm指令→解释器→机器码→CPU。
程序计数器的作用体现在解释器解释jvm指令时,是当前线程所执行的字节码的行号指示器,通俗点说就是记住下一条jvm指令的执行地址。由于读写地址非常频繁,因此物理硬件上通过寄存器实现。

特点:
1. 线程私有
每个线程都有自己的程序计数器,线程切换时记录指令执行地址。
2. 没有内存溢出

2 Java虚拟机栈(JVM Stacks)

2.1定义

虚拟机栈——线程运行所需要的内存空间,由多个栈帧组成
栈帧——每个方法调用时所需要的内存空间
活动栈帧——当前正在执行的(虚拟机栈顶的)那个方法

特点:
线程私有
在这里插入图片描述
问题辨析:
1.垃圾回收是否设计栈内存?
不涉及,栈内存的占用在每次调用方法时产生,随着方法调用的结束栈帧内存被自动回收,不需要垃圾回收来管理。垃圾回收管理的是堆内存。

2.栈内存分配是否越大越好?
栈内存的大小在运行时指定虚拟机参数 -Xss,如不指定,不同的操作系统对应的栈内存大小分别为:Linux/x64:1024KB; macOS:1024KB; Oracle Solaris/x64:1024KB; Windows:取决于虚拟内存。
物理内存的大小一定,栈内存划的越大,则线程数越少。栈内存大能进行更多次的方法递归调用,并不能影响运行速度。

3.方法内的局部变量是否线程安全?
取决于局部变量对于每一个线程是共享的还是私有的。若为私有则每个线程对应一个栈,局部变量是线程安全的。若为共享(static类型),线程不安全。
举个例子:
在这里插入图片描述
m1方法中的局部变量sb是线程私有的,线程安全;
m2方法中的局部变量sb作为方法参数,是多个线程共享的,线程不安全,可以改为StringBuffer型(考虑了同步机制);
m2方法中的局部变量sb作为方法返回值,是多个线程共享的,线程不安全。
总结:对于引用类型变量来说,如果方法的局部变量没有逃离方法的作用范围,是线程安全的。对于逃离了方法的作用范围的,若只有一个线程操作这个变量,一定是线程安全的,如果有多个线程操作,且变量考虑了同步机制,是线程安全的,反之,是不安全的。

2.2内存溢出

Exception:StackOverflowError
产生原因:

  • 栈帧过多(如:循环引用…)
  • 栈帧过大

2.3线程运行诊断

案例1:CPU占用过多,定位(Linux系统)

  1. top定位占用cpu较多的进程
  2. ps 命令定位线程
  3. jstack根据线程id找到有问题的线程(16进制),定位到源码行

案例2:程序运行很久都没有结果
top定位占用cpu较多的进程
4. ps 命令定位线程
5. jstack根据线程id找到有问题的线程(16进制),定位到源码行

3 本地方法栈(Native Method Stacks)

本地方法:与操作系统底层交互的方法,jvm通过本地方法接口调用底层功能。
作用:给本地方法运行提供内存空间。
特点:线程私有

4 堆(Heap)

4.1定义

通过new关键字创建的对象都会使用堆内存。

特点:
1. 线程共享,堆中的对象要考虑线程安全的问题。
2.有垃圾回收机制(堆中不再使用的对象会当成垃圾回收以释放内存资源)

4.2堆内存溢出

Exception: OutOfMemoryError
参数:-Xmx 修改堆空间大小

4.3堆内存诊断

  1. jps工具
    查看当前系统中有哪些Java进程
  2. jmap工具
    监测某一时刻堆内存的占用情况
    jmap -heap 进程id
  3. jconsole工具
    连续监测堆内存占用情况
  4. jvisualvm工具
    堆转储 dump 查看对象信息

案例:
垃圾回收后,内存占用依然很高

5 方法区(Method Area)

5.1 定义

方法区是所有线程共享的区域,存储了类结构的相关信息,包括运行时常量池、成员变量、方法数据、成员方法和构造器方法的代码等。
方法区在虚拟机启动时被创建,逻辑上属于堆(并不强制使用堆内存)。

5.2 组成

方法区在jdk1.6和1.8版本的组成和逻辑位置如下图所示。
在这里插入图片描述
在这里插入图片描述
作为一个进程部署在系统内存中。

5.3方法区内存溢出

Exception: OutOfMemoryError
参数:-XX:MaxMetaspaceSize 修改内存大小

1.8以前会导致永久代内存溢出 OutOfMemoryError:PermGen space
1.8以后导致元空间内存溢出 OutOfMemoryError:Metaspace

运行时动态产生并加载Class 实际场景:
- Spring
- mybatis

5.4运行时常量池

二进制字节码文件(.class)组成:类基本信息、常量池、类方法定义(包含了虚拟机指令)。javap -v *.class反编译后可以查看。根据编译原理,执行虚拟机指令。
字节码文件反编译后代码部   分
常量池是一张表,存于.class文件中,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量(如基本数据类型、字符串等)等信息。
字节码文件反编译后常量池
运行时常量池:类被加载时,常量池信息会放入运行时常量池并把其中的符号地址(#1等)变为真实地址。

5.5StringTable

特性:

  • 运行时常量池中的字符串仅是符号,第一次使用时才变为对象,并加载到串池中
  • 采用串池的机制,避免创建重复的字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的对象放入串池。
    将字符串对象尝试放入串池,如果有则并不会放入,如果没有则将同一个对象放入串池,(即串池中的对象和堆中的对象是同一个) 会把串池中的对象返回

面试题:

以常考的面试题为例,
String s1 = “a”; /
String s2 = “b”;
String s3 = “ab”;
String s4 = s1 + s2;
String s5 = “a” + “b”;
String s6 = s4.intern();
String x2 = new String(“c”) + new String(“d”);
String x1 = “cd”;
x2.intern();

	问:
	System.out.println(s3 == s4);
	System.out.println(s3 == s5);
	System.out.println(s3 == s6);
	System.out.println(x1 == x2);
	如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢

我们先来看一段代码的底层原理,首先捋清楚代码在底层运行的逻辑,之后我们再聚焦到字符串在哪里创建的:

public class Demo1 {
public static void main(String[] args) {
        String s1 = "a"; 
        String s2 = "b";
        String s3 = "ab";
        }
}

反编译Demo1.class文件

运行时常量池: 注意逻辑地址,即 #及后面的数字
在这里插入图片描述
Main方法:
在这里插入图片描述
在底层理解一下代码 String s1 = “a”; 的意思:在运行时常量池#7位置加载字符串变量,而#7位置指向#8位置存储的字符串"a",加载后把它存入到局部变量表2中。
局部变量表
在这里插入图片描述
理解了代码运行的逻辑,知道了符号加载的底层逻辑,我们区分一下常量池和串池即StringTable的关系。常量池开始存在于字节码文件中,当程序运行时,常量池被加载到运行时常量池中,但此时运行时常量池中的符号还没有成为Java中的字符串对象。只有执行到引用符号的代码时,才会把符号变为字符串对象。
串池StringTable数据结构上是一个哈希表,长度固定,不能扩容。运行到引用符号的代码,生成一个字符串对象,将生成的字符串对象放入串池中。
所以说,常量池是存在于.class文件中的,而串池是运行时形成的。

那么两个引用的字符串相加的原理是什么呢?按照上面的分析思路,底层原理为
new StringBuilder().append(“a”).append(“b”).toString() new String(“ab”)
可以看出来,最后是new 了一个String。另外一种思路是,s1 s2是引用型变量,运行时引用可以被修改,编译期无法判断相加后是否不变,因此会new新的对象。
因此System.out.println(s3 == s4);的结果也显而易见了,s3位于串池中,而s4是new出来的对象,位于堆中,"==" 做出的是引用地址是否相同,结果为false。

同样的分析原理, String s5 = “a” + “b”;底层实现逻辑为在常量池中找到"ab"(Javac在编译期的优化,因为"a" “b"是常量,在编译期就能确定合并后仍然是常量),并把它放在串池中,而此时串池中已经有了"ab"对象,因此s5指向已经存在的"ab”。
因此System.out.println(s3 == s5);结果为true。

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo2 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        System.out.println(s3 == s4);
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
        System.out.println(s3 == s5);
    }
}

补充小细节:如何理解只有执行到引用符号的代码时,才会把符号变为字符串对象。
我们用IDEA调试证明一下:

public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 2275

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 2285
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数
    }
}

给程序添加断点,依次执行,内存标签中可以观察String的变化,每执行一次,生成一个对象并放入串池。当执行到重复的字符时,String不会变化,即不会放入串池中。
在这里插入图片描述

6 直接内存(Direct Memory)

6.1定义

属于操作系统内存

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不受JVM管理
    在这里插入图片描述
    在这里插入图片描述

6.2内存分配和回收

Exception: OutOfMemory: Direct buffer memory
使用Unsafe对象完成内存的分配回收,且回收需要主动调用freeMemory方法。例如,ByteBuffer的实现类内部,使用了cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用FreeMemory来释放内存。

五、垃圾回收

1 如何判断对象可以被回收

1.1 引用计数法

在这里插入图片描述
缺点:循环引用导致内存泄漏(程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果)

1.2 可达性分析算法

定义根对象,扫描堆中的对象,若对象没有被根对象直接或间接引用,则作为垃圾回收。

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,检测是否能够沿着GC Root对象为起点的引用链找到该对象,若找不到则可以被回收。

GC Root 根对象
系统核心类、操作系统执行时引用的Java对象、被加锁的对象、活动线程中使用的对象(栈帧中局部变量引用的对象)。

1.3引用

在这里插入图片描述
强引用
特点:只要沿着GC Root对象为起点的强引用链找到该对象,就不会被回收。若都使用强引用,不重要的资源也会占用内存,很容易造成内存溢出,因此,可以通过软引用、弱引用解决这个问题。
软引用和弱引用
对于A1 A2的软引用和弱引用来说,如果,A1 A2的强引用都不存在,同时又发生了垃圾回收,那么,A2对象由于仅存在弱引用会被直接回收;A1是否被回收取决于内存是否足够,若内存不足A1被回收。
在这里插入图片描述
释放弱引用和软引用的内存,需要借助引用队列。如果此时软引用或弱引用没有引用别的对象,会被加入到引用队列中,两个对象不被引用则会被回收。
在这里插入图片描述
举个例子:
强引用代码如下:
在这里插入图片描述
软引用代码如下:
在这里插入图片描述
以上两段代码中,new byte[_4MB] 是新建的对象,代码1建立了强引用,会导致内存溢出;代码2由于使用了SoftReference <byte[]>,建立了与对象的软引用,当内存不足时,垃圾回收机制会将仅存在软引用的对象回收。因此,软连接对于内存敏感有效。
软引用的主动移除,要配合引用队列。
在这里插入图片描述
虚引用和终结器引用
配合引用队列使用
在这里插入图片描述
虚引用引用的对象被回收时,虚引用对象进入引用队列,去释放直接内存:由ReferenceHandler线程定时在引用队列中定时寻找新入队的Cleaner,若有,调用clean方法,根据虚引用对象记录的直接内存地址调用Unsafe.FreeMemory来释放内存。
在这里插入图片描述
对于终结器引用,所有的对象继承自Object类,其中由finalize()方法,当对象没有被强引用且重写了finalize()方法时,可以被回收。回收机制是虚拟机创建了终结器引用,当A4第一次被垃圾回收时,终结器引用对象进入队列,finalize进程找到队列中的终结器引用对象时,找到被引用的对象并且调用finalize()方法,下一次垃圾回收时A4被回收。

引用对比

2 垃圾回收算法

2.1 标记清除(Mark Sweep)

在这里插入图片描述

  1. 标记:标记哪些对象是垃圾
  2. 清除:释放垃圾占用的空间(把起始地址和结束地址放入空闲地址列表中)

优点:速度快(记录起始结束地址) 缺点:易产生内存碎片

2.2 标记整理(Mark Compact)

在这里插入图片描述

  1. 标记:标记哪些对象是垃圾
  2. 整理:紧凑技术整理碎片。获得连续内存空间。

优点:无内存碎片 缺点:速度慢

2.1 复制(Copy)

在这里插入图片描述

  1. 开辟两个区域,From有内存占用,To空闲
  2. 将非垃圾对象移动到To区,From区的垃圾回收
  3. 交换From和To

优点:不会产生碎片 缺点:占用双倍内存空间

3 分代回收

Java虚拟机会同时采取以上三种算法进行垃圾回收,区分方式为分代。
在这里插入图片描述

长时间使用的对象放在老年代,用完丢弃的放在新生代。根据对象生命周期的特点选择不同的垃圾回收算法,更有效的对垃圾回收进行管理。
在这里插入图片描述

  1. 刚创建的对象被分配在伊甸园区,新生代空间不足时,触发第一次垃圾回收Minor GC
  2. Minor GC 会引发 Stop the World,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
  3. 根据可达性分析算法找判断新生代空间的对象是否为垃圾,采用复制算法将存活对象复制到幸存区To,且寿命加1,From To交换,新生代中的垃圾回收。
  4. 创建的对象仍然被分配在伊甸园区。当伊甸园区满,再次触发垃圾回收Minor GC
  5. 根据可达性分析算法找判断新生代中的对象是否为垃圾,采用复制算法将存活对象复制到幸存区To,且寿命加1,From To交换,新生代中的垃圾回收。对象寿命超过阈值时,晋升
  6. 到老年代,最大寿命是15(4bit)。
  7. 当老年代空间不足,会先尝试触发Minor GC,如果之后空间仍不足,那么触发Full GC,STW的时间更长

3.1相关VM参数

在这里插入图片描述

3.2 GC分析

举例:我们为堆分配20M内存,其中新生代10M(伊甸园:From:To = 8:1:1),老年代10M,采用SerialGC垃圾回收器并打印垃圾回收信息。

 // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
void main(String[] args) throws InterruptedException {

            }

未放入ArrayList对象前,其堆内存分配如下:
在这里插入图片描述
分析上图:

  • to区是空白,因此新生代内存大小将其去除,新生代内存为9M
  • Java程序运行前需要载入一些类,因此伊甸园区初始时部分被占用

我们添加对象,模拟触发垃圾回收

 // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
void main(String[] args) throws InterruptedException {
	ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_7MB]);
            }

在这里插入图片描述
由于内存溢出,因此,触发了Minor GC,垃圾回收时,线程暂停,进行垃圾回收后,最终部分数据储存在From中,部分储存在伊甸园区。

对于大对象来说,新生代空间不够而老年代空间足够,则直接晋升老年代,不会触发垃圾回收。
在这里插入图片描述

4 垃圾回收器

SerialGC 串行 新生代内存不足 -minor GC 老年代内存不足 -full GC
ParallelGC 并行 新生代内存不足 -minor GC 老年代内存不足 -full GC
CMS 并发 新生代内存不足 -minor GC 老年代内存不足,并发失败(垃圾回收速度慢于垃圾产生速度) -full GC
G1 并发 新生代内存不足 -minor GC 老年代内存不足,垃圾回收速度慢于垃圾产生速度 -full GC

  • Partial GC:并不收集整个GC堆的模式
  • Young GC:只收集young gen的GC
  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

4.1 串行

适用场景:

  • 单线程
  • 堆内存较小,适合个人电脑
  • 新生代的垃圾回收器基于复制算法,老年代的基于标记整理算法

在这里插入图片描述

  1. 在安全点暂停正在工作的线程(垃圾回收可能改变对象地址)
  2. 单线程的垃圾回收线程工作,其余线程阻塞
  3. 垃圾回收线程结束后,其余线程恢复

4.2 吞吐量优先

适用场景:

  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW时间最短
  • 新生代的垃圾回收器基于复制算法,老年代的基于标记整理算法

VM参数:

  • -XX:+UseAdaptiveSizePolicy可动态调整新生代区比例
  • -XX:GCTimeRatio调整吞吐量的目标,ratio越大,垃圾回收的时间越少,吞吐量越高,当吞吐量无法满足要求时,增大堆大小(垃圾回收次数减少来减少回收时间)
  • -XX:MaxGCPauseMillis=ms最大暂停毫秒数,即每一次垃圾回收时间。与上一个参数是互相制衡的,堆增大了,单次回收时间也会增大,吞吐量减小。因此,二者需要折中。
    在这里插入图片描述
  1. 在安全点暂停正在工作的线程(垃圾回收可能改变对象地址)
  2. 垃圾回收器开启多个线程进行垃圾回收,多个垃圾回收器并行执行,线程个数取决于CPU核数(垃圾回收时,垃圾回收器的cpu占用率增高)
  3. 垃圾回收线程结束后,其余线程恢复

4.3 响应时间优先

适用场景:

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让单次STW时间最短
  • 并发标记和并发清理对吞吐量有影响
  • 会产生内存碎片,易造成并发失败,CMS退化为SerialOld清理碎片,此时垃圾回收时间变长。

VM参数:

  • ConcMarkSweepGC垃圾回收器工作在老年代,基于标记清除算法,与其他线程并发执行,不阻塞其他线程。若并发失败,切换到SerialOld垃圾回收器,基于标记整理算法。
  • ParNewGC工作在新生代,基于复制算法。
  • 线程数VM配置:-XX:ParallelGCThreads=n并行垃圾回收线程数,一般等于CPU核数。-XX:ConcGCThreads= threads 并发垃圾回收线程数,一般等于 CPU核数/4
  • 垃圾回收线程并发清理时会产生浮动垃圾,等到下次垃圾回收时清理。因此需要预留一定的空间存储浮动垃圾。-XX:InitiatingOccupancyFraction=percect决定CMS触发垃圾回收的时机,越小越早触发回收
  • -XX:CMSScavengeBeforeRemark在重新标记回收前先对新生代进行垃圾回收,以减少重新标记时的压力(因为重新标记时要依次扫描新生代和老年代,不清理会有重复无效工作)
    在这里插入图片描述
  1. 老年代内存不足,则在安全点暂停正在工作的线程
  2. 进行初始标记根对象,其余线程暂停阻塞(STW时间较短)
  3. 初始标记完成后,用户进程恢复,垃圾回收线程并发标记
  4. 并发标记结束后,阻塞用户线程进行重新标记(STW)
  5. 重新标记完成后,用户线程恢复,垃圾回收线程并发清理
  • 初始标记:仅标记GC root的直接关联对象,STW
  • 并发标记:使用GC root Tracing算法进行跟踪标记
  • 重新标记:并发标记时产生新垃圾,扫描整个堆进行重新标记,STW

4.4 G1(GarbageFirst)垃圾回收器

面向局部收集和基于Region的内存布局形式。

适用场景:

  • 同时注重吞吐量和低延迟,默认暂停目标是200ms
  • 超大堆内存,将堆划分为多个大小相等的Region
  • 整体上是标记整理算法,两个区域之间是复制算法

相关JVM参数:
-XX: +UseG1GC
-XX: G1HeapRegionSize=size
-XX: MaxGCPauseMillis=time

G1垃圾回收阶段
在这里插入图片描述

  • 新生代垃圾回收
  • 老年代内存超过阈值,在新生代垃圾回收的基础上并发标记
  • 对新生代和老年代都进行一次规模较大的收集,即混合回收
    以上三步循环执行。

新生代垃圾回收Young Collection
在这里插入图片描述
G1垃圾回收器把推内存划分为一个个区域Region,每个区域都可以单独作为伊甸园E、幸存区S和老年代O。当伊甸园区内存不足,会触发一次新生代垃圾回收,会STW。存活的对象复制到幸存区。
存活的对象复制到幸存区
幸存区对象寿命大或者幸存区对象较多达到阈值时,再次触发新生代垃圾回收。寿命大的对象晋升到老年区,不够寿命的复制到幸存区,伊甸园区存活的对象复制到幸存区。
在这里插入图片描述
新生代垃圾回收和并发标记Young Collection+CM

  • 在Young GC时会进行GC root的初始标记
  • 老年代占用堆空间的比例到达阈值时,进行并发标记(不会STW)
  • -XX:InitiatingOccupancyFraction=percect默认45%
    当老年代被占用45%时会触发并发标记
    在这里插入图片描述
    混合回收(Mixed Collection)
    对E S O进行全面垃圾回收
  • 最终标记(Remark)会STW(之前的并发标记阶段可能产生新的垃圾)
  • 拷贝存活(Evacuation)会STW
    在这里插入图片描述
    新生代的回收:E中的幸存对象、S中不够年龄的幸存对象会复制到S中;寿命高的对象晋升到老年代
    老年代的回收:经过并发标记得到待回收对象,根据最大暂停时间 -XX: MaxGCPauseMillis=ms 选择回收价值高的区进行垃圾回收(复制的区域少,可以达到目标)

接下来是一些细节的解释:
Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
新生代垃圾回收时,老年代的对象引用了新生代的对象,利用卡表技术将其标记为脏卡,减少搜索范围,提高扫描根对象的效率。

  • 卡表与Remembered Set对应
  • 引用变更时通过post-write barrier+dirty card queue异步实现
  • Concurrent refinement 线程更新Remember Set

在这里插入图片描述
重新标记Remark
我们记录一个并发标记的中间状态,黑色表示处理完成,灰色表示正在处理,白色表示未处理。最终的回收结果是黑色被保留,灰色和最下面的白色块由于强引用也被保留。而中间的白色块由于GCroot不可达,会被回收。
在这里插入图片描述
但是并发标记时,其引用状态可能被改变,需要重新标记Remark。采用了写屏障指令pre-write barrier+ satb_mark_queue。在并发阶段当引用发生变化时,将对象加入队列并改变状态为灰色。重新标记阶段将处理队列中的对象,会STW。
在这里插入图片描述
字符串去重
优点:节省大量内存
缺点:占用了cpu时间,新生代回收时间略增加
开关: -XX:+UseStringDeduplication

String s1 = new String("ABC"); // char[]{'A','B','C'}
String s2 = new String("ABC");// char[]{'A','B','C'}
  1. 将所有新分配的字符串放入一个队列
  2. 当新生代回收时,G1并发检查是否有重复字符串
  3. 如果值一样,引用同一个char[]即可

这里的去重与String.intern不同:String.intern关注的是字符串对象,利用的是串表原理,而字符串去重关注的是char[]。

并发标记类卸载
所有对象经过并发标记后,可以知道哪些类不再被使用,当一个类加载器(一般是自定义类加载器)的所有类都不再使用,则卸载它所加载的所有类。
-XX: +ClassUnloadingWithConcurrentMark

回收巨型对象

  • 大于Region一半的对象H
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑(回收价值高)
  • G1会跟踪老年代所有incoming引用(记录卡表引用),使得老年代incoming引用为0的巨型对象可以在新生代垃圾回收时处理掉。
    在这里插入图片描述
    动态调整并发标记起始时间
    并发标记必须发生在堆空间占满前,否则退化为fullGC
  1. 采用 -XX:InitiatingOccupancyFraction=percect 设置初始值
  2. 进行数据采样并动态调整
  3. 总会添加一个安全的空挡空间,容纳浮动垃圾

5 垃圾回收调优

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • IO

5.2确定目标

【低延迟】还是【高吞吐量】,选择合适的回收器

  • 低延迟:CMS,G1,ZGC,Zing
  • 高吞吐量:ParrallelGC

5.3最快的GC是不发生GC

查看FullGC前后的内存占用,考虑下面几个问题:

  • 数据是不是太多?
  • 数据表示是否太臃肿?
    对象图
    对象大小
  • 是否存在内存泄漏?
    内存泄漏: 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
    解决方案:软连接、硬连接、第三方缓存实现

5.4 新生代调优

新生代的特点:

  • 所有的new操作的内存分配非常廉价,效率高
    TLAB Thread-local allocation buffer 线程分配局部缓冲区,减少线程之间内存分配冲突。每个线程自己都有私有的伊甸园内存。
  • 死亡对象的回收代价是零
  • 大部分对象是用过即死
  • Minor GC的时间远远低于Full GC(相差一到两个数量级)

措施:
增大新生代大小,在新生代和吞吐量大小之间找到平衡点
新生代过小,吞吐量小;新生代越大,相对的老年代越小,老年代内存不足容易引发FullGC。新生代过大,垃圾回收时间变长导致吞吐量降低。一般新生代大小占整个堆的25%~50%。

新生代大小需要遵从的原则:

  • 新生代能容纳所有【并发量 * (请求-响应)】的数据
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让长时间存活对象尽快晋升

5.5 老年代调优

以 CMS 为例,

  • CMS 的老年代内存越大越好
  • 先不做调优,先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
    -XX:CMSInitiatingOccupancyFraction=percent

5.6 案例

案例1:Full GC 和 Minor GC频繁
原因: GC频繁说明空间不足,当业务高峰期时,新创建的对象会占满新生代内存,幸存区空间紧张导致对象的晋升阈值降低,从而导致生存期很短的对象晋升到老年代,进一步触发FullGC的发生。
解决方案: 增大新生代内存、增大幸存区空间、增大晋升阈值

案例2:请求高峰期发生 Full GC,单次暂停时间特别长 (业务实时性要求选择CMS)
原因: 查看GC日志,CMS垃圾回收器在重新标记时费时最长
解决方案: -XX:CMSScavengeBeforeRemark在重新标记回收前先对新生代进行垃圾回收,以减少重新标记时的压力

案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
原因: 永久代空间不足
解决方案: 增大永久代大小

六、 类加载与字节码技术

在这里插入图片描述

1 类文件结构

简单的HelloWorld.java

public class HelloWorld {
	public static void main(String[] args) {
	System.out.println("hello world");
	}
}

执行 javac -parameters -d . HellowWorld.java 编译为 HelloWorld.class

[root@localhost ~]# od -t xC HelloWorld.class
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

要看懂字节码文件,我们需要知道JVM规范,根据JVM规范,类文件结构为
在这里插入图片描述

1.1 魔数

头4个字节,表示是否是【class】类型的文件
在这里插入图片描述

1.2 版本

魔数后4个字节是版本信息,对应不同版本的JDK,图中表示类的版本 00 34(52) 表示是 Java 8
在这里插入图片描述

1.3 常量池

在这里插入图片描述
常量池是class文件中的资源仓库
在这里插入图片描述
表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值
我们着重分析一下第#1项,如下图。
在这里插入图片描述
查询上面的表,第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】。
在这里插入图片描述
我们以【所属类】为例,学习底层查找原理。
在常量池中查找第#6项
在这里插入图片描述
07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项
在常量池中查找第#28项
在这里插入图片描述
01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】

1.4 访问标识与继承信息

在这里插入图片描述
我们解读一下字节码
在这里插入图片描述

1.5 Field信息

在这里插入图片描述
在这里插入图片描述
表示成员变量数量,本类为

1.6 方法信息

方法由 访问修饰符、名称、参数描述、方法属性数量、方法属性 组成
在这里插入图片描述
表示方法数量,本类为 2(构造方法和main方法),下面是方法的具体组成
在这里插入图片描述
以上字节码的解读如下:

  • 红色代表访问修饰符(本类中是 public)
  • 蓝色代表引用了常量池 #07 项作为方法名称
  • 绿色代表引用了常量池 #08 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 1
  • 红色代表方法属性
    00 09 表示引用了常量池 #09 项,发现是【Code】属性
    00 00 00 2f 表示此属性的长度是 47
    00 01 表示【操作数栈】最大深度
    00 01 表示【局部变量表】最大槽(slot)数
    2a b7 00 01 b1 是字节码指令
    00 00 00 02 表示方法细节属性数量,本例是 2
00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
00 00 00 06 表示此属性的总长度,本例是 6
00 01 表示【LineNumberTable】长度
00 00 表示【字节码】行号 00 04 表示【java 源码】行号
00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
00 00 00 0c 表示此属性的总长度,本例是 12
00 01 表示【LocalVariableTable】长度
00 00 表示局部变量生命周期开始,相对于字节码的偏移量
00 05 表示局部变量覆盖的范围长度
00 0c 表示局部变量名称,本例引用了常量池 #12 项,是【this】
00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是
【Lcn/itcast/jvm/t5/HelloWorld;】
00 00 表示局部变量占有的槽位(slot)编号,本例是 0
在这里插入图片描述
以上字节码的解读如下:
  • 红色代表访问修饰符(本类中是 public static)
  • 蓝色代表引用了常量池 #14 项作为方法名称
  • 绿色代表引用了常量池 #15 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 2
  • 红色代表方法属性(属性1)
    00 09 表示引用了常量池 #09 项,发现是【Code】属性
    00 00 00 37 表示此属性的长度是 55
    00 02 表示【操作数栈】最大深度
    00 01 表示【局部变量表】最大槽(slot)数
    00 00 00 05 表示字节码长度,本例是 9
    b2 00 02 12 03 b6 00 04 b1 是字节码指令
    00 00 00 02 表示方法细节属性数量,本例是 2
00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
00 00 00 0a 表示此属性的总长度,本例是 10
00 02 表示【LineNumberTable】长度
00 00 表示【字节码】行号 00 06 表示【java 源码】行号
00 08 表示【字节码】行号 00 07 表示【java 源码】行号
00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
00 00 00 0c 表示此属性的总长度,本例是 12
00 01 表示【LocalVariableTable】长度
00 10 表示局部变量名称,本例引用了常量池 #16 项,是【args】
00 11 表示局部变量的类型,本例引用了常量池 #17 项,是【[Ljava/lang/String;】
00 00 表示局部变量占有的槽位(slot)编号,本例是 0
在这里插入图片描述
  • 红色代表方法属性(属性2)
    00 12 表示引用了常量池 #18 项,发现是【MethodParameters】属性
    00 00 00 05 表示此属性的总长度,本例是 5
    01 参数数量
    00 10 表示引用了常量池 #16 项,是【args】
    00 00 访问修饰符

1.7 附加属性

在这里插入图片描述

  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

2 字节码指令

2.1 简单举例

构造方法字节码指令

2a b7 00 01 b1
  • 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  • b7 => invokespecial 预备调用构造方法,哪个方法呢?]
  • 00 01 引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】
  • b1 表示返回

主方法字节码指令

b2 00 02 12 03 b6 00 04 b1
  • b2 => getstatic 用来加载静态变量到操作数栈,哪个静态变量呢?
  • 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  • 12 => ldc 加载参数,哪个参数呢?
  • 03 引用常量池中 #3 项,即 【String hello world】
  • b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  • 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  • b1 表示返回

2.2 javap工具

[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.class
	Last modified Jul 7, 2019; size 597 bytes
	MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
	Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
	minor version: 0
	major version: 52
	flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
	#1 = Methodref 		#6.#21 		// java/lang/Object."<init>":()V
	#2 = Fieldref 		#22.#23 	//java/lang/System.out:Ljava/io/PrintStream;
	#3 = String 		#24 		// hello world
	#4 = Methodref 		#25.#26 	// java/io/PrintStream.println:
(Ljava/lang/String;)V
	#5 = Class 			#27 		// cn/itcast/jvm/t5/HelloWorld
	#6 = Class 			#28 		// java/lang/Object
	#7 = Utf8 			<init>
	#8 = Utf8 			()V
	#9 = Utf8 			Code
	#10 = Utf8 			LineNumberTable
	#11 = Utf8 			LocalVariableTable
	#12 = Utf8 			this
	#13 = Utf8 			Lcn/itcast/jvm/t5/HelloWorld;
	#16 = Utf8 			args
	#17 = Utf8 			[Ljava/lang/String;
	#18 = Utf8 			MethodParameters
	#19 = Utf8 			SourceFile
	#20 = Utf8 			HelloWorld.java
	#21 = NameAndType 	#7:#8 		// "<init>":()V
	#22 = Class 		#29 		// java/lang/System
	#23 = NameAndType 	#30:#31 	// out:Ljava/io/PrintStream;
	#24 = Utf8 			hello world
	#25 = Class #32 				// java/io/PrintStream
	#26 = NameAndType 	#33:#34 	// println:(Ljava/lang/String;)V
	#27 = Utf8 			cn/itcast/jvm/t5/HelloWorld
	#28 = Utf8 			java/lang/Object
	#29 = Utf8 			java/lang/System
	#30 = Utf8 			out
	#31 = Utf8 			Ljava/io/PrintStream;
	#32 = Utf8 			java/io/PrintStream
	#33 = Utf8 			println
	#34 = Utf8 			(Ljava/lang/String;)V
	
{
public cn.itcast.jvm.t5.HelloWorld();
  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 4: 0
	LocalVariableTable:
		Start  Length  Slot  Name  Signature
			0      5 	  0  this  Lcn/itcast/jvm/t5/HelloWorld;
			
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
	stack=2, locals=1, args_size=1
		0: getstatic 		#2 		// Field
java/lang/System.out:Ljava/io/PrintStream;
		3: ldc 				#3 		// String hello world
		5: invokevirtual 	#4 		// Method
java/io/PrintStream.println:(Ljava/lang/String;)V
		8: return
	LineNumberTable:
		line 6: 0
		line 7: 8
	LocalVariableTable:
		Start  Length  Slot  Name  Signature
			0 		9 	  0  args [Ljava/lang/String;
  MethodParameters:
	Name 				Flags
	args
}

2.3 图解方法执行流程

演示字节码指令、操作数栈、常量池的关系
原始Java代码

public class Demo3_1 {
    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:/Java/code0416/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2021年7月21日; size 635 bytes
  SHA-256 checksum e5f598745edb48c064349f660c3b26885e91cbe04939445a6a1f9921c54c08ca
  Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #22                         // cn/itcast/jvm/t3/bytecode/Demo3_1
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // java/lang/Short
   #8 = Utf8               java/lang/Short
   #9 = Integer            32768
  #10 = Fieldref           #11.#12        // java/lang/System.out:Ljava/io/PrintStream;
  #11 = Class              #13            // java/lang/System
  #12 = NameAndType        #14:#15        // out:Ljava/io/PrintStream;
  #13 = Utf8               java/lang/System
  #14 = Utf8               out
  #15 = Utf8               Ljava/io/PrintStream;
  #16 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
  #17 = Class              #19            // java/io/PrintStream
  #18 = NameAndType        #20:#21        // println:(I)V
  #19 = Utf8               java/io/PrintStream
  #20 = Utf8               println
  #21 = Utf8               (I)V
  #22 = Class              #23            // cn/itcast/jvm/t3/bytecode/Demo3_1
  #23 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_1
  #24 = Utf8               Code
  #25 = Utf8               LineNumberTable
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_1;
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               args
  #32 = Utf8               [Ljava/lang/String;
  #33 = Utf8               a
  #34 = Utf8               I
  #35 = Utf8               b
  #36 = Utf8               c
  #37 = Utf8               SourceFile
  #38 = Utf8               Demo3_1.java
{
  public cn.itcast.jvm.t3.bytecode.Demo3_1();
    descriptor: ()V
    flags: (0x0001) 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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #9                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #16                 // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 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
}

常量池载入运行时常量池
在这里插入图片描述
将class文件中常量池的数据载入到运行时常量池,
方法字节码载入方法区
在这里插入图片描述
main线程开始运行,分配栈帧内存
在这里插入图片描述
绿色为局部变量区,蓝色为操作数栈,两者决定栈帧的大小。
执行引擎开始执行字节码

bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有:
sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
ldc 将一个 int 压入操作数栈
ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore_1
将操作数栈顶数据弹出,存入局部变量表的slot 1
在这里插入图片描述
在这里插入图片描述
lds #3
从常量池中加载#3数据到操作数栈
Short.MAX_VALUE = 32767,Short.MAX_VALUE+1 = 32768是编译期间算好的。
在这里插入图片描述
istore_2
将操作数栈顶数据弹出,存入局部变量表的slot 2
在这里插入图片描述
在这里插入图片描述
iload_1
将slot 1的数据压入操作数栈
在这里插入图片描述
iload_2
将slot 2的数据压入操作数栈
在这里插入图片描述
iadd
将数据弹出操作数栈,并把相加后的结果压入操作数栈
在这里插入图片描述
istore_3
将操作数栈顶数据弹出,存入局部变量表的slot 3
在这里插入图片描述
getstatic #4
在常量池中获取成员变量的引用,找到该成员变量并且把引用压入操作数栈
在这里插入图片描述
在这里插入图片描述
iload_3
将slot 3的数据压入操作数栈
在这里插入图片描述
在这里插入图片描述
invokevirtual #5
找到常量池 #5 项
定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码
在这里插入图片描述
执行完毕,弹出栈帧
清除 main 操作数栈内容
在这里插入图片描述
return
完成 main 方法调用,弹出 main 栈帧
程序结束

面试题 a++ 和 ++a

iinc 指令是直接在局部变量 slot 上进行运算
a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

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

答案是 11 34

2.4 字节码指令-条件判断

在这里插入图片描述

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

2.5 字节码指令-循环控制指令

public class Demo3_4 {
	public static void main(String[] args) {
		int a = 0;
		while (a < 10) {
		a++;
		}
	}
}

字节码文件

0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
public class Demo3_5 {
	public static void main(String[] args) {
		int a = 0;
		do {
			a++;
		} 
		while (a < 10);
	}
}
0: iconst_0
1: istore_1
2: iinc          1, 1
5: iload_1
6: bipush        10
8: if_icmplt     2
11: return
public class Demo3_6 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {

        }
    }
}
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
  • 比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归
public class Demo3_6_1 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x);
    }
}

输出结果为 0
x = x++; iload x innc x 1 istore 1

2.6 字节码指令-构造方法

< cinit >()V

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

编译器会按照从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法()V,()V 方法会在类加载的初始化阶段被调用

0: bipush 20
2: putstatic #2 // Field i:I
5: bipush 30
7: putstatic #2 // Field i:I
10: bipush 10
12: putstatic #2 // Field i:I
15: return

< init >()V

// 执行顺序: 静态代码块-非静态代码块-类的构造方法
public class Demo3_8_2 {
    private String a = "s1";
    {
        b = 20;
    }
    private int b = 10;
    {
        a = "s2";
    }
    public Demo3_8_2(String a, int b) {
        this.a = a;
        this.b = b;
    }
    public static void main(String[] args) {
        Demo3_8_2 d = new Demo3_8_2("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

补充:关于构造代码块

  • Java 编译器编译一个 Java 源文件的时候,会把成员变量的声明语句提前至类的最前端
  • 成员变量的初始化工作,其实都是在构造函数中执行的
  • Java 编译器编译后,构造代码块的语句体会被移动到构造函数(的最前端)中执行,构造函数中的语句体在构造代码块的语句体执行完毕后再执行,即构造代码块的代码优先于构造函数中的代码执行
  • 成员变量的显示初始化与构造代码块的代码是按照当前代码的先后顺序执行的
E:\Java\code0416\out\production\jvm\cn\itcast\jvm\t3\bytecode>Javap -v Demo3_8_2.class
Classfile /E:/Java/code0416/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_8_2.class
  Last modified 2021年7月21日; size 838 bytes
  SHA-256 checksum e0324ff9b173ab5ac9593de6f282ccb9959cce0f061a3ebabd60141e17e714b2
  Compiled from "Demo3_8_2.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_8_2
  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #10                         // cn/itcast/jvm/t3/bytecode/Demo3_8_2
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // s1
   #8 = Utf8               s1
   #9 = Fieldref           #10.#11        // cn/itcast/jvm/t3/bytecode/Demo3_8_2.a:Ljava/lang/String;
  #10 = Class              #12            // cn/itcast/jvm/t3/bytecode/Demo3_8_2
  #11 = NameAndType        #13:#14        // a:Ljava/lang/String;
  #12 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_8_2
  #13 = Utf8               a
  #14 = Utf8               Ljava/lang/String;
  #15 = Fieldref           #10.#16        // cn/itcast/jvm/t3/bytecode/Demo3_8_2.b:I
  #16 = NameAndType        #17:#18        // b:I
  #17 = Utf8               b
  #18 = Utf8               I
  #19 = String             #20            // s2
  #20 = Utf8               s2
  #21 = String             #22            // s3
  #22 = Utf8               s3
  #23 = Methodref          #10.#24        // cn/itcast/jvm/t3/bytecode/Demo3_8_2."<init>":(Ljava/lang/String;I)V
  #24 = NameAndType        #5:#25         // "<init>":(Ljava/lang/String;I)V
  #25 = Utf8               (Ljava/lang/String;I)V
  #26 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
  #27 = Class              #29            // java/lang/System
  #28 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #33 = Class              #35            // java/io/PrintStream
  #34 = NameAndType        #36:#37        // println:(Ljava/lang/String;)V
  #35 = Utf8               java/io/PrintStream
  #36 = Utf8               println
  #37 = Utf8               (Ljava/lang/String;)V
  #38 = Methodref          #33.#39        // java/io/PrintStream.println:(I)V
  #39 = NameAndType        #36:#40        // println:(I)V
  #40 = Utf8               (I)V
  #41 = Utf8               Code
  #42 = Utf8               LineNumberTable
  #43 = Utf8               LocalVariableTable
  #44 = Utf8               this
  #45 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
  #46 = Utf8               main
  #47 = Utf8               ([Ljava/lang/String;)V
  #48 = Utf8               args
  #49 = Utf8               [Ljava/lang/String;
  #50 = Utf8               d
  #51 = Utf8               SourceFile
  #52 = Utf8               Demo3_8_2.java
{
  public cn.itcast.jvm.t3.bytecode.Demo3_8_2(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: (0x0001) ACC_PUBLIC
    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           #7                  // String s1
         7: putfield      #9                  // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
        13: putfield      #15                 // Field b:I
        16: aload_0
        17: bipush        10
        19: putfield      #15                 // Field b:I
        22: aload_0
        23: ldc           #19                 // String s2
        25: putfield      #9                  // Field a:Ljava/lang/String;
        28: aload_0
        29: aload_1
        30: putfield      #9                  // Field a:Ljava/lang/String;
        33: aload_0
        34: iload_2
        35: putfield      #15                 // Field b:I
        38: return
      LineNumberTable:
        line 18: 0
        line 6: 4
        line 9: 10
        line 12: 16
        line 15: 22
        line 19: 28
        line 20: 33
        line 21: 38
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      39     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
            0      39     1     a   Ljava/lang/String;
            0      39     2     b   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: new           #10                 // class cn/itcast/jvm/t3/bytecode/Demo3_8_2
         3: dup
         4: ldc           #21                 // String s3
         6: bipush        30
         8: invokespecial #23                 // Method "<init>":(Ljava/lang/String;I)V
        11: astore_1
        12: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
        15: aload_1
        16: getfield      #9                  // Field a:Ljava/lang/String;
        19: invokevirtual #32                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
        25: aload_1
        26: getfield      #15                 // Field b:I
        29: invokevirtual #38                 // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 24: 0
        line 25: 12
        line 26: 22
        line 27: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
           12      21     1     d   Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
}

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

2.7 方法调用

public class Demo3_9 {
    public Demo3_9() { } //构造方法

    private void test1() { } // 私有方法

    private final void test2() { } // 私有+final

    public void test3() { } // 普通方法

    public static void test4() { } // 静态方法

    @Override
    public String toString() {
        return super.toString();
    }

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

字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #11                 // class cn/itcast/jvm/t3/bytecode/Demo3_9
         3: dup
         4: invokespecial #13                 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #14                 // Method test1:()V
        12: aload_1
        13: invokevirtual #17                 // Method test2:()V
        16: aload_1
        17: invokevirtual #20                 // Method test3:()V
        20: aload_1
        21: pop
        22: invokestatic  #23                 // Method test4:()V
        25: invokestatic  #23                 // Method test4:()V
        28: aload_1
        29: invokevirtual #26                 // Method toString:()Ljava/lang/String;
        32: pop
        33: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 12
        line 23: 16
        line 24: 20
        line 25: 25
        line 26: 28
        line 27: 33
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      34     0  args   [Ljava/lang/String;
            8      26     1     d   Lcn/itcast/jvm/t3/bytecode/Demo3_9;
  • 最终方法(final),私有方法(private),构造方法,super 调用父类方法都是由 invokespecial 指令来调用,属于静态绑定,性能高,直接绑定地址
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态,性能较低,需要多次寻址
  • 【new】做了两件事:一是给对象分配堆内存,二是内存分配成功后将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 < init >: ()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了。相当于浪费了两个指令。

2.8 多态的原理

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

2.9 异常处理

public class Demo3_11_1 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    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) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号。8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置。9行的意思是20 存入局部变量表slot 1的位置。

多个single-catch块

public class Demo3_11_2 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }
}
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        30
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        40
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        50
        25: istore_1
        26: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
	LocalVariableTable:
        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中的一个分支,局部变量表slot 2被共用(复用),节省栈帧占用的内存。
Multi-catch

public class Demo3_11_3 {

    public static void main(String[] args) {
        try {
            Method test = Demo3_11_3.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void test() {
        System.out.println("ok");
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #7                  // class cn/itcast/jvm/t3/bytecode/Demo3_11_3
         2: ldc           #9                  // String test
         4: iconst_0
         5: anewarray     #11                 // class java/lang/Class
         8: invokevirtual #13                 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        11: astore_1
        12: aload_1
        13: aconst_null
        14: iconst_0
        15: anewarray     #2                  // class java/lang/Object
        18: invokevirtual #17                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        21: pop
        22: goto          30
        25: astore_1
        26: aload_1
        27: invokevirtual #29                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
        30: return
      Exception table:
         from    to  target type
             0    22    25   Class java/lang/NoSuchMethodException
             0    22    25   Class java/lang/IllegalAccessException
             0    22    25   Class java/lang/reflect/InvocationTargetException
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12      10     1  test   Ljava/lang/reflect/Method;
           26       4     1     e   Ljava/lang/ReflectiveOperationException;
            0      31     0  args   [Ljava/lang/String;

finally块

public class Demo3_11_4 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I

  • finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程,保证一定会执行
  • 为了捕捉到与Exception同级的异常以及finally中的异常,会将这些异常的引用放入slot 3中,并在最后抛出,即在finally被复制的第三次中实现这个过程。

面试题

public class Demo3_12_1 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

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

输出结果 20

  public static int test();
    descriptor: ()I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: bipush        10
         2: istore_0
         3: bipush        20
         5: ireturn
         6: astore_1
         7: bipush        20
         9: ireturn
      Exception table:
         from    to  target type
             0     3     6   any
  • 若在finally中return,会使athrow丢失,即异常被吞。
  • 由于 finally 中的 ireturn 被插入到了所有可能的流程,因此返回结果肯定以 finally 的为准

finally对返回值的影响

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}
  public static int test();
    descriptor: ()I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    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_1
         9: ireturn
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I

在这个场景下,局部变量表中有一个slot 1用来暂存返回值,目的是为了固定返回值。需要返回时,先从局部变量表的slot 1中iload 1,再将操作数栈顶元素返回ireturn.

2.10 synchronized

public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // new Object
         3: dup
         4: invokespecial #1                  // invokespecial <init>:()V
         7: astore_1						  // 将对象引用存入局部变量表 lock引用 -> lock
         8: aload_1							  // <- lock(synchronized)
         9: dup								   
        10: astore_2						  // lock引用 -> slot 2
        11: monitorenter					  //monitorenter(lock引用)
        12: getstatic     #7                  // <- System.out
        15: ldc           #13                 // <- "ok"
        17: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2							  // <- lock2(lock引用)
        21: monitorexit						  // monitorexit(lock引用)
        22: goto          30
        25: astore_3						  // any -> slot 3
        26: aload_2						      // <- lock2(lock引用)
        27: monitorexit						  // monitorexit(lock引用)
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      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字节码的过程中,自动生成和转换的一些代码,主要目的是减轻程序员负担。

3.1 默认构造器

调用父类Object的无参构造方法。即调用java/lang/Object."< init >": ()V

public class Candy1 {
}

编译成class后:

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

3.2 自动拆装箱

public class Candy2{
	public static void main(String[] args){
		Integer x = 1; // 装箱
		int y = x; // 拆箱
	}
}
public class Candy2 {
	public static void main(String[] args) {
		Integer x = Integer.valueOf(1);
		int y = x.intValue();
	}
}

3.3 泛型-泛型擦除

public class Candy2{
	public static void main(String[] args){
		List<Integer> list = new ArrayList<>();
		list.add(10); // 实际调用的是 List.add(Object e)
		Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
	}
}
{
  public cn.itcast.jvm.t3.candy.Candy3();
    descriptor: ()V
    flags: (0x0001) 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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/candy/Candy3;

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=7, locals=12, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: ldc           #8                  // class cn/itcast/jvm/t3/candy/Candy3
        33: ldc           #9                  // String test
        35: iconst_2
        36: anewarray     #10                 // class java/lang/Class
        39: dup
        40: iconst_0
        41: ldc           #11                 // class java/util/List
        43: aastore
        44: dup
        45: iconst_1
        46: ldc           #12                 // class java/util/Map
        48: aastore
        49: invokevirtual #13                 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        52: astore_3
        53: aload_3
        54: invokevirtual #14                 // Method java/lang/reflect/Method.getGenericParameterTypes:()[Ljava/lang/reflect/Type;
        57: astore        4
        59: aload         4
        61: astore        5
        63: aload         5
        65: arraylength
        66: istore        6
        68: iconst_0
        69: istore        7
        71: iload         7
        73: iload         6
        75: if_icmpge     192
        78: aload         5
        80: iload         7
        82: aaload
        83: astore        8
        85: aload         8
        87: instanceof    #15                 // class java/lang/reflect/ParameterizedType
        90: ifeq          186
        93: aload         8
        95: checkcast     #15                 // class java/lang/reflect/ParameterizedType
        98: astore        9
       100: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       103: new           #17                 // class java/lang/StringBuilder
       106: dup
       107: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
       110: ldc           #19                 // String 原始类型 -
       112: invokevirtual #20                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       115: aload         9
       117: invokeinterface #21,  1           // InterfaceMethod java/lang/reflect/ParameterizedType.getRawType:()Ljava/lang/reflect/Type;
       122: invokevirtual #22                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
       125: invokevirtual #23                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       128: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       131: aload         9
       133: invokeinterface #25,  1           // InterfaceMethod java/lang/reflect/ParameterizedType.getActualTypeArguments:()[Ljava/lang/reflect/Type;
       138: astore        10
       140: iconst_0
       141: istore        11
       143: iload         11
       145: aload         10
       147: arraylength
       148: if_icmpge     186
       151: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       154: ldc           #26                 // String 泛型参数[%d] - %s\n
       156: iconst_2
       157: anewarray     #27                 // class java/lang/Object
       160: dup
       161: iconst_0
       162: iload         11
       164: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       167: aastore
       168: dup
       169: iconst_1
       170: aload         10
       172: iload         11
       174: aaload
       175: aastore
       176: invokevirtual #28                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
       179: pop
       180: iinc          11, 1
       183: goto          143
       186: iinc          7, 1
       189: goto          71
       192: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          143      43    11     i   I
          100      86     9 parameterizedType   Ljava/lang/reflect/ParameterizedType;
          140      46    10 arguments   [Ljava/lang/reflect/Type;
           85     101     8  type   Ljava/lang/reflect/Type;
            0     193     0  args   [Ljava/lang/String;
            8     185     1  list   Ljava/util/List;
           31     162     2     x   Ljava/lang/Integer;
           53     140     3  test   Ljava/lang/reflect/Method;
           59     134     4 types   [Ljava/lang/reflect/Type;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8     185     1  list   Ljava/util/List<Ljava/lang/Integer;>;

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息。
泛型信息在编译为字节码之后就丢失了,实际的类型都当作Object来处理。上述代码中,字节码不区分泛型信息,方法调用为List.add(Object o)和Object o = list.get(0); checkcast强制类型转换

3.4 泛型-泛型反射

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息,利用反射可以得到其泛型参数:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    }
}

Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始类型 - " + parameterizedType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

只能得到作为方法参数和返回值的泛型。

3.5 可变参数

public class Candy4{
	public static void main(String[] args){
		foo("Hello","World");
	}
	public static void foo(String... args){
		String[] arr = args;
		System.out.println(arr);
	}
}

可变参数String… args实际上是String[] args,上述代码在编译时会被变换为

public class Candy4{
	public static void main(String[] args){
		foo(new String[]{"Hello","World"});
	}
	public static void foo(String[] args){
		String[] arr = args;
		System.out.println(arr);
	}
}

如果调用了foo()则等价为foo(new String[]{}),创建了一个空数组,而不是传递null

3.6 for each

public class Candy5_1 {
	public static void main(String[] args){
		int[] arr = {1,2,3,4,5};
		for(int e : arr){
			System.out.println(e);
		}
	}
}

会被编译器转换为:

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

对于集合的遍历

public class Candy5_2 {
	public static void main(String[] args){
		List<Integer> list = Arrays.asList(1,2,3,4,5);
		for(Integer e : list){
			System.out.println(e);
		}
	}
}

会被编译器转换为:

public class Candy5_2 {
	public static void main(String[] args){
		List<Integer> list = Arrays.asList(1,2,3,4,5);
		Iterator iter = list.iterator();
		while(iterator.hasNext()){
			Integer e = (Integer)iter.next();
			System.out.println(e);
		}
	}
}

foreach循环能配合循环以及实现了Iterable接口的集合类使用,Iterable接口用来获取集合的迭代器iterator

3.6 条件编译-switch-string

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

会被编译器转换为:

public class Candy6_1 {
	public Candy6_1() {
	}
	public static void choose(String str){
		byte x = -1;
		switch(str.hashCode()) {
		case 99162322: // hello 的 hashCode
			if (str.equals("hello")) {
				x = 0;
			}
			break;
		case 113318802: // world 的 hashCode
			if (str.equals("world")) {					
			x = 1;
			}
		}
		switch(x) {
		case 0:				
			System.out.println("h");
			break;
		case 1:
			System.out.println("w");
		}
	}
}

一个switch分支转换为两个switch分支。第一次根据字符串的hashcode和equals将字符串转为相应的byte类型;第二次根据byte进行比较。
用hashcode进行比较能提高比较效率,减少可能的比较,而equals是为了防止哈希冲突。

3.7 条件编译-switch-enum

enum Sex {
    MALE, FEMALE;
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

转换后代码

/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
	// 数组大小即为枚举元素个数,里面存储case用来对比的数字
	static int[] map = new int[2];
	static {
		map[Sex.MALE.ordinal()] = 1;
		map[Sex.FEMALE.ordinal()] = 2;
	}
}
	public static void foo(Sex sex) {
		int x = $MAP.map[sex.ordinal()];
		switch (x) {
		case 1:
			System.out.println("男");
			break;
		case 2:
			System.out.println("女");
			break;
		}
	}
}

3.8 枚举类

普通类的实例对象是无限的,枚举类的实例对象是有限的。枚举类不能被继承

enum Sex {
    MALE, FEMALE;
}

转换后代码

public final class Sex extends Enum<Sex> {
	public static final Sex MALE;
	public static final Sex FEMALE;
	private static final Sex[] $VALUES;
	static {
		MALE = new Sex("MALE", 0);
		FEMALE = new Sex("FEMALE", 1);
		$VALUES = new Sex[]{MALE, FEMALE};
	}
	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);
	}
}

3.9 try-with-resources

对需要关闭的资源处理的特殊语法。资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等,使用twr可以不用写finally代码块,编译器会帮助生成关闭资源代码。

public class Candy9 {
	public static void main(String[] args){
		try(InputStream is = new FileInputStream("d:\\1.txt")){
			System.out.println(is);
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

会被转换为:

public class Candy9 {
	public Candy9() {
	}
	public static void main(String[] args) {
		try {
			InputStream is = new FileInputStream("d:\\1.txt");
			System.out.println(is);
			} catch (Throwable e1) {
			// t 是我们代码出现的异常
				t = e1;
				throw e1;
			} finally {
				// 判断了资源不为空
				if (is != null) {
					// 如果我们代码有异常
					if (t != null) {
						try {
							is.close();
						} catch (Throwable e2) {
							// 如果 close 出现异常,作为被压制异常添加
							t.addSuppressed(e2);
						}
					} else {
						// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
						is.close();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

设计addSuppressed(Throwable e) 被压制异常,防止异常信息的丢失

public class Test6 {
	public static void main(String[] args) {
		try (MyResource resource = new MyResource()) {
			int i = 1/0;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
class MyResource implements AutoCloseable {
	public void close() throws Exception {
		throw new Exception("close 异常");
	}
}

输出:

java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)

3.10 方法重写时的桥接方法

我们知道,方法重写时的返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值是父类返回值的子类
class A {
	public Number m() {
		return 1;
	}
}
class B extends A {
	@Override
	// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
	public Integer m() {
		return 2;
	}
}

对于子类,java 编译器会做如下处理:

class B extends A {
	public Integer m() {
		return 2;
	}
	// 此方法才是真正重写了父类 public Number m() 方法
	public synthetic bridge Number m() {
		// 调用 public Integer m()
		return m();
	}
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) {
	System.out.println(m);
}

输出结果:

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

3.11 匿名内部类

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

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {
	Candy11$1() {
	}
	public void run() {
	System.out.println("ok");
	}
}
public class Candy11 {
	public static void main(String[] args) {
		Runnable runnable = new Candy11$1();
	}
}

引用局部变量的匿名内部类

public class Candy11 {
	public static void test(final int x) {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("ok:" + x);
			}
		};
	}
}

转换后的代码

// 额外生成的类
final class Candy11$1 implements Runnable {
	int val$x;
	Candy11$1(int x) {
		this.val$x = x;
	}
	public void run() {
		System.out.println("ok:" + this.val$x);
	}
}
	public static void test(final int x) {
		Runnable runnable = new Candy11$1(x);
	}
}

匿名内部类引用的外部局部变量必须是final的:在创建对象时,将变量的值赋给了对象的val属性,若变量变化,val属性不能变化。

4 类加载阶段

4.1 加载

  • Java程序运行时,需要用类加载器将字节码文件载入方法区中,内部采用C++的instanceKlass描述java类,重要属性有:
    • _java_mirror java类的镜像,即*.class,klass和class互相持有对方的指针。通过对象不能直接访问instanceKlass,而是先访问镜像class,再访问instanceKlass,进而知道其属性。
    • _super 父类
    • _field 成员变量
    • _methods 方法
    • _constants 常量池
    • _class_loader 类加载器
    • _vtable 虚方法表
    • _itable 接口放发表
  • 若子类的父类还没有加载,先加载父类
  • 加载和链接是交替运行的
    在这里插入图片描述
    类的字节码被加载到元空间中,构成了instanceKlass数据结构,加载的同时,会在堆内存中生成一个镜像(类对象),存储着instanceKlass的指针地址。
  • instanceKlass 元数据存储在方法去的元空间内,_java_mirror存储在堆中,可通过jHSDB查看。
  • 实例对象根据对象头中的信息可以找到镜像进而找到instanceKlass 元数据,访问其属性。

4.2 连接-验证

验证类是否符合JVM规范,安全性检查。
文件格式、元数据、字节码、符号引用验证

4.3 连接-准备

正式为类中的变量(即静态变量)分配内存并设置类变量初始值。

  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段就能确定值,赋值在准备阶段完成
  • 如果static变量是final的引用类型,赋值在初始化阶段完成,属于cinit指令完成部分
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        // loadClass 方法不会导致类的解析和初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        //new C();
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {

}

4.4 连接-解析

将常量池中的符号引用解析为直接引用,直接引用是可以直接指向目标的指针,因此可以确切知道目标在内存中的位置。

4.5 初始化

调用< cinit >()V方法,虚拟机会保证这个类的【构造方法】的线程安全。

初始化发生的时机:类初始化是懒惰的

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

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)
  • 类对象.class(加载阶段生成mirror class)
  • 创建该类的数组A[0]
  • 类加载器的loadclass方法(只会导致类的加载,不会导致类的解析和初始化)
  • Class.forName的参数2为false时(初始化标志关闭)
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");
    }
}

验证实验

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 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.itcast.jvm.t3.load.B");
//        // 5. 不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


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

面试题

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

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 = "hello";
	public static final Integer c = 20;
}

懒惰初始化的单例模式
单例模式:保证类的实例对象只有一个,关键字:private修饰构造方法
懒惰:第一次使用类时再加载、连接、初始化

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

以上的实现特点是:

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

5 类加载器

在这里插入图片描述

5.1 启动类加载器 Bootstrap ClassLoader

用 Bootstrap 类加载器加载类:

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

执行

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}

输出:

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

5.2 扩展类加载器 Extension ClassLoader

public class G {
    static {
//        System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}

执行

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

再写一个同名的类,打包成jar后拷贝到…/lib/ext中,重新执行,输出:

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

5.3 双亲委派模式

调用类加载器的loadClass方法时,查找类的规则:(上级优先)

  • 检查该类是否已经加载,如果没有加载执行下面的步骤
  • 有上级的话,委派上级loadClass完成类加载
  • 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
  • 每一层都找不到,调用findClass方法(每个类加载器自己扩展)来加载
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 {
				if (parent != null) {
					// 2. 有上级的话,委派上级 loadClass
					c = parent.loadClass(name, false);
				} else {
					// 3. 如果没有上级了(ExtClassLoader),则委派
	BootstrapClassLoader
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
			}
			if (c == null) {
				long t1 = System.nanoTime();
				// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
				c = findClass(name);
				// 5. 记录耗时
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		if (resolve) {
			resolveClass(c);
		}
	return c;
	}
}
public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Load5_3.class.getClassLoader());
        Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());

    }
}

执行流程:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$ AppClassLoader // 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher $ ExtClassLoader // 4 处,调用自己的 findClass 方法,是在
    JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher$AppClassLoader的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

5.4 线程上下文类加载器

jdk需要在某些情况下打破双亲委派模式,否则,某些类(如jdbc.Driver)在lib路径下找不到。
以jdbc.Driver类为例,其源码如下:

在这里插入代码片

Service Provider Interface(SPI)
在这里插入图片描述
约定:在jar包的META-INF/services包下,以接口全限定名为文件,文件内容是实现类名称。可以使用如下规则代码得到实现类:

在这里插入代码片

体现面向接口编程+解耦的思想,一些框架中也运用了此思想:

  • JDBC
  • Servlet初始化容器
  • Spring容器
  • Duboo(扩展SPI)
    获取线程上下文类加载器
在这里插入代码片

线程上下文类加载器是当前线程使用的类加载器,默认为应用程序类加载器,内部是由Class.forName调用了上下文类加载器完成类加载

5.5 自定义类加载器

需要自定义类加载器的场景:

  • 加载非classpath随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 某些类希望予以隔离,不同应用的同类名可以加载不冲突,常见于tomcat容器

步骤:

  • 集成ClassLoader父类
  • 遵从双亲委派机制,重写findClass方法(不能重写loadClass方法,违背双亲委派机制)
  • 读取类文件的字节码
  • 调用父类的defineClass方法来加载类
  • 使用者调用该类加载器的loadClass方法
public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);

        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {

    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";

        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);

            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

6 运行期优化

6.1 即时编译

JVM将执行状态分为5个层次分层编译:

  • 0层解释执行Interpreter
  • 1层使用C1即时编译器编译执行(不带profiling)
  • 2层使用C1即时编译器编译执行(带基本的profiling)
  • 3层使用C1即时编译器编译执行(带完全的profiling)
  • 4层使用C2即时编译器编译执行

profiling指运行过程中收集一些程序执行状态的数据。如方法的调用次数、循环的回收次数等。

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

  • 解释器将字节码解释为机器码,下次遇到相同的字节码仍会重复解释;JIT将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码直接执行,无需再编译
  • 解释器将字节码解释为针对所有平台通用的机器码;JIT根据平台类型,生成平台特定的机器码

对于不常用的代码,解释执行;对于热点代码,JIT将其编译成机器码。效率Interpreter<C1<C2

逃逸分析(Escape Analysis)

public static void main(String[] args){
	for(int i; i < 1000; i++){
		
	}
}

分析对象的动态作用域,若对象不会逃逸到方法或者线程之外,或者逃逸程度低,可以进行优化。
方法内联(Inlining)

private static int square(final int i){
	return i*i;
}
public static void main(String[] args){
	System.out.println(square(9));
}

若发现square是热点方法,且长度较短,会进行内联,即把方法内的代码拷贝粘贴到调用者的位置

System.out.println(9*9);

常量折叠(constant folding)优化后

System.out.println(81);

字段优化
基于JMH创建maven工程

在这里插入代码片

6.2 反射优化

public class Reflect1 {

    public static void foo() {
        System.out.println("foo...");
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

通过查看 ReflectionFactory 源码可知:

  • sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阈值

七、内存模型(Java Memory Model)

1 Java内存模型

定义了一套在多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性和原则性的规则和保障

1.1原子性

public class Demo4_1 {
    static int i = 0;
    static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
                for (int j = 0; j < 50000; j++) {
                    i++;
                }
        });

        Thread t2 = new Thread(() -> {
                for (int j = 0; j < 50000; j++) {
                    i--;
                }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

i++字节码指令

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

i–字节码指令

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

Java中对静态变量的自增自减并不是原子操作,Java内存模型中,共享数据存在主内存中,多线程静态变量的自增自减操作需要在主存和线程内存中进行数据交换
在这里插入图片描述
多线程抢夺CPU资源(时间片)执行,存在交错执行的情况

解决方案

  • 语法:
    synchronized同步关键字保证原子性
    锁住同一个对象
synchronized(对象){
	原子操作代码
}
  • 解决并发问题
public class Demo4_1 {

    static int i = 0;

    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; j++) {
                    i++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; j++) {
                    i--;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

1.2 可见性

当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。具体实现:内存模型通过在变量修改后将新值同步回主内存,在读取变量前从主内存刷新变量值,即依赖主内存传递值

public class Demo4_2 {

    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(run){
                // ....
            }
        });
        t.start();

        Thread.sleep(1000);
        run = false; // 线程t不会如预想的停下来
    }
}

我们画图分析一下上述代码的执行流程:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
    在这里插入图片描述
  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
    在这里插入图片描述
  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述
    解决方案

语法:volatile 易变关键字

  • 用来修饰来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
  • 保证多个线程治安,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性。仅用在一个线程写,多个线程读的情况。

注意:synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低
如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,为什么?
println方法中也加入了同步关键字synchronized,防止当前线程从高速缓存中获取值

1.3 有序性

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
	if(ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
	num = 2;
	ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

  • 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
  • 情况4:线程2执行到 ready = true,线程1 执行,这回进入 if 分支,但此时线程2的num还没有赋值,结果为0
    这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化。
    解决方案

语法:volatile 易变关键字,禁用指令重排。

int num = 0;
volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
   if(ready) {
   	r.r1 = num + num;
   } else {
   	r.r1 = 1;
   }
}
// 线程2 执行此方法
public void actor2(I_Result r) {
   num = 2;
   ready = true;
}

在单线程中,指令重排不会影响结果;但在多线程中,指令重排可能会影响结果的正确性。
例如著名的 double-checkedlocking 模式实现单例

public final class Singleton {
	private Singleton() { }
	private static Singleton INSTANCE = null;
	public static Singleton getInstance() {
		// 实例没创建,才会进入内部的 synchronized代码块
		if (INSTANCE == null) {
			synchronized (Singleton.class) {
				// 也许有其它线程已经创建实例,所以再判断一次
				if (INSTANCE == null) {
					INSTANCE = new Singleton();
				}
			}
		}
		return INSTANCE;
	}
}

若多个线程同时调用getInstance方法会导致对象多次创建

双重锁定,以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 存在指令重排导致对象初始化未完成的情况

1.4 happens-before

规定了哪些写操作对其他线程的读操作可见,是可见性和有序性的一套规则总结:

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
	x = 10;
},"t1").start();
new Thread(()->{
	System.out.println(x);
},"t2").start();
  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
	synchronized(m) {
		x = 10;
	}
},"t1").start();
new Thread(()->{
	synchronized(m) {
		System.out.println(x);
	}
},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
	x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
	Thread t2 = new Thread(()->{
		while(true) {
			if(Thread.currentThread().isInterrupted()) {
				System.out.println(x);
				break;
			}
		}
	},"t2");
	t2.start();
	new Thread(()->{
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		x = 10;
		t2.interrupt();
	},"t1").start();
	while(!t2.isInterrupted()) {
		Thread.yield();
	}
	System.out.println(x);
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

2 CAS与原子类

2.1 CAS

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while(true) {
	int 旧值 = 共享变量 ; // 比如拿到了当前值 0
	int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
	/*
	这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
	compareAndSwap 返回 false,重新尝试,直到:
	compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
	*/
	if( compareAndSwap ( 旧值, 结果 )) {
		// 成功,退出循环
	}
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
  • CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令

2.2 乐观锁和悲观锁

  • 乐观锁CAS::最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • 悲观锁synchronized:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

2.3 原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

import java.util.concurrent.atomic.AtomicInteger;

public class Demo4_4 {
    // 创建原子整数对象
    private static AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                i.getAndIncrement();  // 获取并且自增  i++
//                i.incrementAndGet();  // 自增并且获取  ++i
            }
        });

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                i.getAndDecrement(); // 获取并且自减  i--
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

3 synchronized

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、 重量级锁指针 、 线程ID 等内容

3.1 轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。即线程是交替错开给资源加锁的。

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
在这里插入图片描述
在这里插入图片描述

3.2 锁膨胀

有竞争的情况下,轻量级锁进化为重量级锁。
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

public static void method1() {
	synchronized( obj ) {
		// 同步块
	}
}

在这里插入图片描述
在这里插入图片描述

3.3 重量锁-自旋

重量级锁竞争的时候,使用自旋来进行优化,访问同步块失败后不立即阻塞,而是自旋。如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋成功
在这里插入图片描述

自旋失败
在这里插入图片描述

  • 自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

3.4 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。用偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新 CAS.

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  • 访问对象的 hashCode 也会撤销偏向锁(加锁时对象头中的信息给了线程)
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 A
		method2();
	}
}
public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}

在这里插入图片描述

3.5 其他优化

减少上锁时间
同步代码块中尽量短

减少锁的粒度:将一个锁拆分为多个锁提高并发度

  • ConcurrentHashMap 数组的链表头加锁,HashMap读写完全互斥。而ConcurrentHashMap每次只锁一个链表,其余链表读写不受影响
  • LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
  • LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高

锁粗化

  • 多次循环进入同步块 不如 同步块内多次循环
  • JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)
new StringBuffer().append("a").append("b").append("c");

锁消除
JVM JIT会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。

读写分离

  • CopyOnWriteArrayList
  • ConyOnWriteSet
    读原数组,写在新复制的数组上。即读操作不需要同步,写操作需要同步。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值