11230 字
56 分钟
JVM学习思维笔记01
2026-06-20
无标签

JVM学习思维笔记01#

可以看别人的笔记: https://blog.csdn.net/2302_80653152/article/details/145166618?spm=1001.2014.3001.5501

1. 什么是JVM?#

JVM 指的是 Java Virtual Machine也就是Java虚拟机,本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件。

2. JVM的功能是什么?#

解释运行、内存管理、即时编译(Just-In-Time 简称JIT 用于性能优化)

3. Java相比于C/C++,需要先转成字节码再通过JVM解释成机器码,这会导致性能损失,那么Java是如何去优化性能的?#

JVM会将热点代码解释并优化然后保存到内存中,之后再次执行这段代码就不需要再次用JVM解释,只需要直接调用内存中的这段机器码即可。

4. JVM由什么组成?#

image.png

5. 字节码文件由什么组成?(5部分)#

基础信息-魔数、字节码文件对应的Java版本号访问标识(public final等等)父类和接口 常量池-保存了字符串常量、类或接口名、字段名主要在字节码指令中使用 字段-当前类或接口声明的字段信息 方法-当前类或接口声明的方法信息字节码指令 属性-类的属性,比如源码的文件名内部类的列表等

6. 常量池的作用是什么?#

避免相同内容重复,减少空间浪费。

7. int i = θ;i = i++;最终i的值是多少?(从字节码指令来分析)#

他的字节码是这样的: iconst_0->放0到操作数栈 istore_1->从操作数栈取值到局部变量表索引1的位置 iload_1->加载局部变量表1位置的值到操作数栈(此时值为0) iinc 1 by 1->在局部变量表1号位置增加1 istore_1->从操作数栈取值到局部变量表索引1的位置 return

最后因为 i++是先存储值到操作数栈上 再让局部变量表加上1 之后i=i++赋值时再从操作数栈取值到局部变量表中 导致存储到的是原来的值0.

答案是0,我通过分析字节码指令发现,i++先把0取出来放入临时的操作数栈中,接下来对i进行加1,i变成了1,最后再将之前保存的临时值0放入i,最后i就变成了0。

8. 如何查看字节码文件#

本地文件可以直接用jclasslib打开

服务器可以用javap -v 然后 > 标准输出管道符保存到文本中

运行中的jar包可以用arthas工具,使用dump导出字节码文件,或使用jad进行反编译查看源码。

Terminal window
dump -d 输出目录 类的名字
jad 类的名字

9. 类的生命周期有几个阶段?#

加载

  1. 加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中。
  3. 类加载器在加载完类之后,Java 虚拟机会将字节码中的信息保存到内存的方法区中。(生成一个InstanceKlass 对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。)
  4. 同时,Java 虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class 对象。(作用是在Java 代码中去获取类的信息以及存储静态字段的数据(JDK8 及之后))

链接 验证:校验字节码文件是否符合规范 准备:为静态变量(static)分配内存并设置初始值。准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。final 修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。 解析:解析阶段主要是将常量池中的符号引用替换为直接引用(地址),符号引用就是在字节码文件中使用编号来访问常量池中的内容。

初始化:初始化阶段会执行静态代码块中的代码,并为静态变量赋值。(初始化阶段会执行字节码文件中clinit 部分的字节码指令)

使用

卸载

10. 如何使用hsdb工具查看内存中的对象?#

  1. 进入到jdk安装路径的bin目录下,输入命令jhsdb hsdb 会打开一个gui界面 jdk1.8可以去lib目录下输入 java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
  2. 打开控制台 输入jps命令 显示所有运行的java进程和进程id
  3. 在gui工具左上角点击attach to hotspot process 输入对应id 打开
  4. Tools中有一个Object Historgram 对象直方图,可以查看对象占用内存。
  5. 输入全类名找对应对象名字的占用。

11. 什么是类加载器?#

类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。 类加载器只参与加载过程中的字节码获取并加载到内存这一部分。

image.png

12. Jdk8及以前 主要有哪几种类加载器?#

JDK8 及之前的版本中默认的类加载器有如下几种:

  1. 启动类加载器(bootstrap)用于加载Java中最核心的类 由C++实现。
  2. 扩展类加载器(extension)允许扩展 Java 中比较通用的类;由Java实现。
  3. 应用程序类加载器(application)用于加载应用使用的类,例如自定义的学生业务类,或者从 Maven 中导入的包;由Java实现。

13. 类的双亲委派机制有什么用?#

  1. 保证类加载的安全性 通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang·String,确保核心类库的完整性和安全性

  2. 避免重复加载 双亲委派机制可以避免同一个类被多次加载

14. 什么是类加载器的双亲委派机制?#

1、当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载

2、应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。

3、双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。

以下是详情:

双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。

image.png

向上查找如果已经加载过,就直接返回Class对象,加载过程结束。这样就能避免一个类重复加载。

image.png

如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下尝试加载。

image.png

第二次再去加载相同的类,仍然会向上进行委派,如果某个类加载器加载过就会直接返回。

向下委派加载起到了一个加载优先级的作用。

15. 如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?#

启动类加载器来加载,根据双亲委派机制,它的优先级最高

16. 在自己项目中创建一个java.lang.String,会被加载吗?#

不会。由双亲委派机制,首先会先向上查找这个类是否加载过。向上查找的时候,发现启动类加载器已经加载过这个String类了,于是就会返回启动类加载器加载在rt.jar包中的String类

17. 打破双亲委派机制的方式有哪些?#

  1. 自定义类加载器 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除 Tomcat通过这种方式实现应用之间类隔离

  2. 线程上下文类加载器 JNDI、JDBC、JCE、JAXB和JBI等框架使用了SPI机制+线程上下文类加载器。

  3. OSGi实现了一整套类加载机制,允许同级类加载器之间互相调用。(了解即可)

18. 怎么使用阿里arthas不停机解决线上问题?#

  1. 在出问题的服务器上部署一个 arthas,并启动。
  2. jad —source-only 类全限定名 > 目录/文件名.java (jad 命令反编译,然后可以用其它编译器,比如 vim 来修改源码)
  3. mc -c 类加载器的hashcode 目录/文件名.java -d 输出目录 (mc命令用来编译修改过的代码 类加载器的类名使用 sc -d 类全限定名 来得到)
  4. retransform class文件所在目录/xxx.class (用retransform 命令加载新的字节码)

注意事项: 1、程序重启之后,字节码文件会恢复,除非将class文件放入jar包中进行更新。 2、使用retransform不能添加方法或者字段,也不能更新正在执行中的方法。

只能用于应急使用!

19. 运行时数据区指的是什么?#

Java虚拟机在运行Java程序过程中管理的内存区域称之为运行时数据区(即JVM管理的内存)

20. 运行时数据区有什么?#

《Java虚拟机规范》中规定了每一部分的作用。

按两类去分:

  1. ==线程不共享==

    1. 程序计数器
      1. 在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令。
      2. 在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继续解释运行。
    2. Java虚拟机栈(有可能内存溢出 递归调用过多导致)
      1. Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。
      2. Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。
    3. 本地方法栈
  2. ==线程共享==

    1. 方法区(有可能出现内存溢出)
      1. 主要存放类的元信息,还保存了常量池
    2. (有可能出现内存溢出)
      1. 存放创建出来的对象

21. Java虚拟机栈存储了哪些内容?#

存储了方法的栈帧

栈帧由三部分组成:

  1. 局部变量表:局部变量表的作用是在运行过程中存放所有的局部变量
  2. 操作数栈:操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
  3. 帧数据:帧数据主要包含动态链接、方法出口、异常表的引用

Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。

Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。

22. 以下代码的局部变量表会占用几个槽?#

public void test(int k,int m){
{
int a = 1;
int b = 2;
}
{
int c = 1;
}
int i = 0;
int j = 1;
}

答案是6个:为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

23. 方法区中存储了什么?#

方法区是存放基础信息的位置,线程共享,主要包含三部分内容:

  1. 类的元信息:保存了所有类的基本信息
  2. 运行时常量池:保存了字节码文件中的常量池内容
  3. 字符串常量池:保存了字符串常量

image.png

24. 方法区存在哪?#

JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。

JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。

image.png

image.png

25. 字符串常量池是什么?#

堆内存(Heap)中的一个特殊区域(在JDK7及之后)。它专门用来缓存所有通过String.intern()方法显式加入的、或者由字面量创建(String s = "abc")的字符串对象的引用(注意:是引用,而不是对象本身)。

  • 关键位置变迁

    • JDK 6及以前:字符串常量池在永久代中。
    • JDK 7及以后:字符串常量池被移到了中。这是一个非常重要的优化,因为永久代的GC效率低,而堆的GC(尤其是Young GC)频率高、效率高,能及时清理不再被引用的字符串。
  • 有什么用

    • 避免重复创建:JVM通过复用字符串常量池中的引用,极大地减少了内存中相同字符串对象的数量,从而节省内存。比如,String s1 = "hello"; String s2 = "hello"; 实际上 s1 和 s2 指向的是堆中同一个String对象(通过字符串常量池的引用获取)。

26. 静态变量存储在哪?#

JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。

JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。

27. 字符串常量池的工作机制是?#

  • 工作机制
    • 用双引号直接赋值(字面量):JVM会去字符串常量池中查找。如果找到,直接返回该对象的引用;如果没找到,则在堆中创建一个String对象,并在常量池中存入它的引用,然后返回。
    • new String("hello"):则至少创建两个对象。一个是new在堆中(非池中)的String对象,另一个是字符串常量池中的那个引用(如果之前没有的话)。
    • 调用intern():如果常量池中还没有该字符串,则会在常量池中创建(或尝试创建)该字符串的引用,并返回常量池中的引用。

28. 不同JDK版本之间运行时数据区域的区别是什么?#

JDK6: image.png

JDK7: image.png

JDK8: image.png

29. 内存泄漏指的是什么?#

内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出

30. 方法区中类的回收需要满足什么条件?(了解即可)#

方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:

  1. 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
  2. 加载该类的类加载器已经被回收。
  3. 该类对应的java.lang.Class 对象没有在任何地方被引用。

开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中。每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

31. 如何判断堆上的对象可以回收?#

Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。(不完全准确)

32. 如图,如果在main方法中最后执行 a1 =nul,b1 =null,是否能回收A和B对象呢?#

image.png

能回收,方法中已经没有办法使用引用去访问A和B对象。

面试题循环依赖怎么解决?

33. 如何判断堆上的对象没有被引用?#

常见的有两种判断方法:引用计数法和可达性分析法。 引用计数法:为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。

可达性分析将对象分为两类:垃圾回收的根对象(GCRoot)和普通对象,对象与对象之间存在引用关系。 下图中A到B再到C和D,形成了一个引用链,可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。

image.png

Java使用的是可达性分析算法来判断对象是否可以被回收。

34. 可达性分析算法是什么样的?#

可达性分析将对象分为两类:垃圾回收的根对象(GCRoot)和普通对象,对象与对象之间存在引用关系。 下图中A到B再到C和D,形成了一个引用链,可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。

image.png

35. 可达性分析算法如何判断哪些对象为GC root对象?#

以下四种:

  1. 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。 image.png

  2. 系统类加载器加载的java.lang.Class对象,引用类中的静态变量。 image.png

  3. 监视器对象,用来保存同步锁synchronized关键字持有的对象。 image.png

  4. 本地方法调用时使用的全局对象。

36. 有哪几种常见的对象引用?#

可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式:

  1. 软引用
  2. 弱引用
  3. 虚引用
  4. 终结器引用

37. 什么是软引用?#

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。

简单来说,当内存不足的时候,强引用引用的对象会保存,而软引用引用的对象会被回收。

实现软引用: 在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。

软引用的执行过程如下: 1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。 2.内存不足时,虚拟机尝试进行垃圾回收。 3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。 4.如果依然内存不足,拋出OutOfMemory异常。

image.png

caffeine中可以讲Value值设置成soft:

Cache<Object,Object> build = Caffeine.newBuilder().softValues().build();

设置为soft后每一个值都会用softReference去封装,当内存不足的时候就会自己释放掉。

如果不设置的话,默认使用的是强引用

38. 软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些SoftReference对象需要回收呢?#

JVM 不需要去“知道哪些 SoftReference 对象需要被回收”。它通过全局的 Reference 链表来管理所有 Reference 对象。

  1. 全局链表:JVM 内部维护了四个全局链表(由 G1、ZGC、Shenandoah 等不同 GC 实现,但概念相同):SoftReferencesWeakReferencesFinalReferencesPhantomReferences
  2. 遍历与检查:在每次 GC 的标记阶段,GC 会遍历这些链表上的所有 Reference 对象。对于链表上的每一个 SoftReference 对象,GC 会检查它的 referent 字段指向的对象是否还存活(即是否被强引用链可达)。
  3. 决策:如果 referent 不存活了(只能通过软引用到达),并且当前内存压力大(SoftReference 有特殊的回收策略,例如最近一次 GC 后存活了多久,和上次 GC 之间的时间间隔等),则 GC 决定回收该 referent。
  4. 入队:如果回收了 referent,并且该 SoftReference 关联了 ReferenceQueue,GC 会将该 SoftReference 从全局链表中移除,并放入到它的 ReferenceQueue 中。

结论:GC 不会主动回收 SoftReference 对象本身(除非它从队列中取出后不再被引用)。它通过全局链表的遍历 来找到所有软引用,判断其 referent 是否需要回收,然后通过入队 这种方式通知你哪些 SoftReference 的 referent 已经被回收了,由你来决定何时清理这些 SoftReference 对象本身(从你的数据结构中移除)。

39. 什么是弱引用?#

弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收

在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。

弱引用对象本身也可以使用引用队列进行回收。

40. 什么是虚引用,作用是什么?(了解即可)#

虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

41. 什么是终结器引用?(了解即可)#

终结器引用(FinalReference) 是 JVM 内部用于支持 Object.finalize() 方法的一种特殊引用。它的作用是垃圾回收器在回收一个对象之前,先确保该对象的 finalize() 方法被调用一次,从而给对象一个“临终自救”或清理资源的机会。

但实际工程中,它极为不推荐使用,甚至被认为是 Java 语言设计的一个“坑”。

终结器引用的本质#

  • 定义:在 JVM 内部,如果一个类重写了 Object.finalize() 方法(且非空),那么这个类的实例被创建时,JVM 会在其内部数据结构中创建一个 FinalReference 对象指向该实例。
  • 优先级:它的优先级低于软引用(SoftReference)和弱引用(WeakReference),但高于虚引用(PhantomReference)。
  • 生命周期:它并不是一种“经典”的引用(像强、软、弱那样直接影响可达性),更像是一种 “逃逸机制”

终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。

42. Java如何实现垃圾回收?#

简单来说,垃圾回收要做的有两件事: 1、找到内存中存活的对象 2、释放不再存活对象的内存,使得程序能再次利用这部分空间

43. 常见的垃圾回收算法有哪些?#

  • 1960年John McCarthy发布了第一个GC算法:标记-清除算法。
  • 1963年Marvin L. Minsky 发布了复制算法。 本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来。
  1. 标记-清除算法(Mark Sweep GC)
  2. 复制算法(Copying GC)
  3. 标记-整理算法(Mark Compact GC)
  4. 分代GC(Generational GC)

Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。

image.png

44. 垃圾回收算法的评价标准通过哪几个方面来考虑?#

  1. 吞吐量 吞吐量指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,即吞吐量= 执行用户代码时间/(执行用户代码时间+GC时间)。吞吐量数值越高,垃圾回收的效率就越高。 image.png

  2. 最大暂停时间 最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短。 image.png

image.png

  1. 堆使用效率 不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。

image.png

上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。一般来说,堆内存越大,最大暂停时间就越长。想要减少最大暂停时间,就会降低吞吐量。不同的垃圾回收算法,适用于不同的场景。

45. 垃圾回收算法-标记清除算法是什么样的?#

标记清除算法的核心思想分为两个阶段:

1.标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。

2.清除阶段,从内存中删除没有被标记也就是非存活对象。

image.png

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。 缺点:

  1. 碎片化问题。由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
  2. 分配速度慢。由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。

46. 垃圾回收算法-复制算法的核心思想是什么?#

  1. 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
  2. 在垃圾回收GC阶段,将From中存活对象复制到To空间。
  3. 将两块空间的From和To名字互换。

完整的复制算法的例子:

  1. 将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象。 image.png

  2. GC阶段开始,将GC Root搬运到To空间

  3. 将GC Root关联的对象,搬运到To空间 image.png

  4. 理From空间,并把名称互换 image.png

优点: 吞吐量高 复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动 不会发生碎片化 复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。

缺点: 内存使用效率低 每次只能让一半的内存空间来为创建对象使用

47. 垃圾回收算法-标记整理算法的核心思想是什么?#

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。核心思想分为两个阶段:

  1. 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。
  2. 整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。 image.png

优点: 内存使用效率高 整个堆内存都可以使用,不会像复制算法只能使用半个堆内存 不会发生碎片化 在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间

缺点: 整理阶段的效率不高 整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-Finger、表格算法、ImmixGc等高效的整理算法优化此阶段的性能

48. 垃圾回收算法-分代GC的核心思想是什么?#

分代GC垃圾回收算法是Java虚拟机(JVM)等现代垃圾回收器中最核心、最实用的一种设计思想。它并非一种孤立的算法,而是一种策略,它结合了前面提到的复制算法、标记-清除算法和标记-整理算法,并根据对象存活周期的不同特点将它们应用于不同的内存区域,以达到最优的性能。

核心思想:围绕“弱代假说”#

分代算法的理论基础是弱代假说

  1. 绝大多数对象都是“朝生夕死”的:程序员创建的大量对象(如循环中的临时变量、方法内部的局部对象)在创建后很快就会变成垃圾,不再被引用。
  2. 熬过越多次垃圾回收的对象,越难以消亡:那些长时间存活的对象(如缓存、单例、连接池对象)往往会一直存活下去。

基于这个观察,JVM将堆内存划分成几个不同的代(Generation),并对不同的代采用最适合其对象特点的垃圾回收算法。

内存分代结构(以HotSpot JVM为例)#

典型的JVM堆被分为三个主要区域:

  1. 年轻代(Young Generation)

    • 存放生命周期短的对象。
    • 特点:空间小,垃圾回收频率高,回收速度快。
    • 算法:主要采用复制算法(Copying)。因为每次回收都有大量对象死亡,只有少量存活,复制的成本很低。
  2. 老年代(Old Generation)

    • 存放经过多次回收仍然存活的对象,以及从年轻代晋升过来的大对象。
    • 特点:空间大,垃圾回收频率低,但每次回收耗时较长。
    • 算法:主要采用标记-清除(Mark-Sweep) 或 标记-整理(Mark-Compact) 算法。因为对象存活率高,复制算法代价太大。
  3. 元空间/永久代(Metaspace / Permanent Generation)

    • 存放类信息、方法、常量、静态变量等元数据。在JDK 8之后,本地内存(Native Memory)取代了永久代。
    • 回收:主要发生在发生Full GC时,回收卸载的类信息和常量,条件苛刻。

工作流程详解:三步走#

分代GC的回收过程可以分为三个主要的 “事件”

1. Minor GC (Young GC)

  • 发生地点:年轻代。
  • 触发时机:当年轻代的Eden区空间不足时。
  • 过程
    • 初始阶段:新对象默认分配到Eden区。大对象可能会直接进入老年代。
    • 回收动作:当Eden区满时,触发Minor GC。
    • 复制与晋升:使用复制算法。将Eden区和一个存活对象较少的Survivor区(From)中存活的对象复制到另一个空的Survivor区(To)。如果对象年龄超过阈值(默认15次),就将其晋升(Promotion)到老年代。
    • 清理:清空Eden区和原From区。然后,To区变成新的From区,另一个区清空待用。
  • 特点:速度非常快,暂停时间短。它会暂停所有用户线程(Stop-The-World),但由于年轻代小,暂停时间通常很短。

2. Major GC / Full GC (Old GC)

  • 发生地点:老年代(Major GC通常指老年代GC,Full GC则包含年轻代、老年代和元空间/永久代)。
  • 触发时机
    • 老年代空间不足。
    • Minor GC晋升的对象大小超过老年代剩余空间。
    • 调用 System.gc() 显式请求。
    • JDK 17之前的CMS失败转Serial Old等。
  • 过程:会对整个堆(或老年代)进行回收。通常使用标记-清除标记-整理算法。
  • 特点:速度慢,暂停时间长。通常是系统性能瓶颈的主要来源。

为什么分代GC很高效?#

  1. 降低整体暂停时间:因为绝大多数GC工作都集中在对象快速死亡的年轻代,而年轻代的Minor GC很快。老年代GC次数少,影响可控。
  2. 空间利用率高:复制算法在年轻代中避免了内存碎片(将存活对象紧凑复制)。标记-整理算法在老年代也能消除碎片。
  3. 算法选择最优:在不同区域使用最适合其对象存活率的算法,避免了“一刀切”的低效。

49. 为什么分代GC算法要把堆分成年轻代和老年代?#

● 系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。 ● 老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。 ● 在虚拟机的默认设置中,新生代大小要远小于老年代的大小。

分代GC算法将堆分成年轻代和老年代主要原因有: 1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。 2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。 3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(fulgc),STW时间就会减少。

50. JVM中有哪些常见的垃圾回收器?#

一、经典垃圾回收器(JDK 8及之前为主)#

1. Serial GC(串行收集器)#

  • 工作方式:单线程进行垃圾回收,回收时所有用户线程暂停(Stop-The-World, STW)。
  • 适用场景:单核CPU、内存较小的客户端模式应用(如桌面应用)。
  • 特点:简单高效,对于单核环境,停顿时间可控。
  • 启用参数-XX:+UseSerialGC

2. ParNew GC(并行年轻代收集器)#

  • 工作方式:Serial收集器的多线程版本,只在年轻代使用多线程进行复制算法。
  • 适用场景:配合CMS老年代收集器使用(CMS只能与ParNew或Serial配合)。
  • 特点:多核环境下比Serial快,但依然会STW。
  • 启用参数-XX:+UseParNewGC

3. Parallel Scavenge GC(并行年轻代收集器)#

  • 工作方式:多线程并行回收年轻代,使用复制算法。
  • 与ParNew的区别:更关注吞吐量(CPU用于运行用户代码的时间比例),提供自适应调节功能(-XX:+UseAdaptiveSizePolicy)。
  • 适用场景:后台计算、批处理等对吞吐量要求高的场景。
  • 启用参数-XX:+UseParallelGC(此时老年代为Parallel Old)

4. Serial Old GC(串行老年代收集器)#

  • 工作方式:单线程,使用标记-整理算法对老年代进行回收。
  • 适用场景:作为Serial收集器的老年代搭档,或者作为CMS失败的后备预案。
  • 启用参数-XX:+UseSerialOldGC(通常由JVM自动选择)

5. Parallel Old GC(并行老年代收集器)#

  • 工作方式:多线程并行回收老年代,使用标记-整理算法。
  • 适用场景:与Parallel Scavenge配合,追求高吞吐量。
  • 启用参数-XX:+UseParallelOldGC

6. CMS(Concurrent Mark Sweep,并发标记清除)收集器#

  • 工作方式:以获取最短回收停顿时间为目标,多数阶段与用户线程并发执行。
    • 阶段:初始标记(STW,短)→ 并发标记 → 重新标记(STW,短)→ 并发清除
  • 适用场景:对响应时间敏感的Web服务、交互式应用。
  • 优点:低停顿。
  • 缺点
    • 对CPU资源敏感(并发阶段占用CPU)
    • 无法处理浮动垃圾(可能导致Concurrent Mode Failure)
    • 使用标记-清除算法,会产生内存碎片
  • 启用参数-XX:+UseConcMarkSweepGC

二、现代垃圾回收器(JDK 9及以后为主推)#

7. G1 GC(Garbage-First,垃圾优先)收集器#

  • 定位:面向服务端、替代CMS的全功能垃圾回收器。
  • 核心设计
    • 将堆划分为多个大小相等的Region(区域),逻辑上仍分年轻代和老年代,但物理不连续
    • 优先回收垃圾最多的Region(Garbage-First)
    • 能够可预测地控制停顿时间-XX:MaxGCPauseMillis,默认200ms)
  • 工作流程
    • 年轻代GC:并行复制,STW
    • 并发标记阶段:与用户线程并发进行全局标记
    • 混合回收(Mixed GC):同时回收年轻代和部分老年代Region
  • 特点
    • 整体上属于标记-整理,不会产生大量内存碎片
    • 大对象(Humongous)直接分配到单独Region
    • JDK 9起成为默认垃圾回收器
  • 启用参数-XX:+UseG1GC

三、超低延迟垃圾回收器(JDK 11及以后)#

8. ZGC(Z Garbage Collector)#

  • 目标超低停顿(<10ms),且不随堆大小增长而增加停顿时间。
  • 核心技术
    • 染色指针:在对象指针中编码元数据,实现并发整理
    • 读屏障:访问对象时进行并发修正
    • 几乎所有的回收阶段(标记、转移、重定位)都是并发
  • 特点
    • 支持TB级别的堆内存
    • 停顿时间极短,适合大内存、低延迟应用(如金融交易系统)
    • JDK 15起正式生产就绪
  • 启用参数-XX:+UseZGC

9. Shenandoah GC#

  • 目标:同样追求超低停顿(<10ms),与ZGC类似但实现路线不同。
  • 核心技术
    • 使用Brooks Pointer(转发指针)实现并发压缩
    • 所有的GC阶段(包括对象整理)都与用户线程并发
  • 特点
    • Red Hat主导开发
    • JDK 12起加入,但需要额外启用
  • 启用参数-XX:+UseShenandoahGC

50. G1垃圾回收器完整运行流程是什么样的?#

  1. 正常阶段:不断执行年轻代GC,回收Eden区中新死亡的对象
  2. 触发点:当堆使用率超过阈值后,启动并发标记周期
  3. 并发标记:通过SATB并发标记出老年代中所有存活对象,识别出垃圾最多的Region
  4. 混合GC:经过多次STW暂停,逐步回收垃圾最多的老年代Region
  5. 循环往复:混合GC完成后,重新进入年轻代GC周期
  6. 兜底:如果上述机制来不及回收,触发Full GC(应尽量避免)

G1的GC流程不是孤立的,而是一个包含多个子周期的循环过程。我们可以把它的生命周期理解为一连串的周期交替:

阶段0:整体周期示意图#

时间轴 → ┌─────────────────────────────────────────────────────────┐ │ 年轻代GC → 年轻代GC → … → 并发标记周期 → 混合GC → … │ │ (循环多次) (触发条件满足) (多个混合GC) │ └─────────────────────────────────────────────────────────┘

阶段1:年轻代GC(Young GC)#

触发条件#

  • Eden区被填满,没有足够的空白Region来分配新对象

步骤1:选择要回收的Region集合(CSet,Collection Set) → 选择所有的Eden Region + 部分Survivor Region

步骤2:STW暂停当前应用线程 → 进行根扫描(线程栈、寄存器、全局变量等)

步骤3:使用复制算法将存活对象从CSet复制到空闲Region → Eden中的存活对象 → 复制到Survivor → Survivor中的老年对象(年龄+1)→ 达到阈值则晋升到Old

步骤4:更新RSet(记忆集)

步骤5:清空原Region,重置为空白Region

特点#

  • STW暂停,但通常很短(几十毫秒)
  • 使用复制算法,无碎片
  • 吞吐量高,适合大部分对象快速死亡的情况

阶段2:并发标记周期(Concurrent Marking Cycle)#

触发条件#

  • 堆内存使用率达到阈值(-XX:InitiatingHeapOccupancyPercent,默认45%)

子步骤详解#

子步骤A:初始标记(Initial Mark,STW)

  1. 暂停所有用户线程(STW)
  2. 标记所有GC Roots直接引用的对象
  3. 标记当前活跃的年轻代Region中的对象(因为后面需要)
  4. 这个阶段非常快

子步骤B:根区域扫描(Root Region Scanning,并发)

  1. 与用户线程并发执行
  2. 扫描Survivor Region中对象的引用,找出它们是否引用了老年代对象
  3. 更新RSet
  4. 这个阶段完成后,后续的年轻代GC(如果有)可以正常执行

子步骤C:并发标记(Concurrent Marking,并发)

  1. 与用户线程并发执行
  2. 从初始标记的根对象出发,遍历整个对象图
  3. 标记所有可达对象(使用三色标记法)
  4. 利用SATB(Snapshot-At-The-Beginning)处理并发期间的引用变化
  5. 这个阶段耗时最长,但不会暂停应用

子步骤D:重新标记(Remark,STW)

  1. 暂停所有用户线程(STW)
  2. 处理SATB队列中剩余的引用更新
  3. 最终确定哪些对象是存活的
  4. 这个阶段比较短

子步骤E:清理(Cleanup,STW + 并发)

  1. STW部分:计算每个Region中存活对象的数量和空间
  2. STW部分:识别出完全空闲的Region(存活对象=0),可以立即回收
  3. 并发部分:更新RSet,准备后续的混合回收
  4. 这个阶段结束后,G1就知道了哪些Region垃圾最多

并发标记的结果#

  • 每个Region的存活对象数量已经确定
  • G1知道哪些Region是“最值得回收的”(垃圾最多的)

阶段3:混合回收(Mixed GC)#

触发条件#

  • 并发标记完成后,堆使用率仍然较高
  • 或者年轻代GC无法满足停顿时间目标

执行步骤#

步骤1:选择CSet(Collection Set) → 包含所有的Eden Region + 部分老年代Region → 老年代Region的选择依据:优先选垃圾最多的Region(Garbage-First原则)

步骤2:STW暂停,进行回收 → 使用复制算法将存活对象从CSet复制到空闲Region → 年轻代对象复制到Survivor → 老年代对象复制到另一个空闲Region(压缩整理)

步骤3:更新RSet,清空原Region

特点#

  • 分多次执行(一般3~5次),每次只回收一部分老年代Region
  • 目标是控制每次暂停时间在预测范围内
  • 通过多次混合GC,逐步清理老年代中的垃圾

阶段4:Full GC(兜底机制)#

触发条件#

  • 混合GC过程中,没有足够的空白Region用于复制存活对象
  • 或者堆内存严重不足

执行过程#

  • 单线程(Serial Old)或并行(取决于配置)的标记-整理算法
  • 对整个堆进行全堆扫描和压缩
  • 这是最慢的GC,STW时间长

预防措施#

  • 调高 -XX:InitiatingHeapOccupancyPercent(触发并发标记的阈值)
  • 增加堆内存大小
  • 调整 -XX:G1ReservePercent(预留空间比例,默认10%)
JVM学习思维笔记01
http://www.shineacz.top/posts/jvm学习思维笔记01/
作者
shineAcZ
发布于
2026-06-20
许可协议
CC BY 4.0