封面 《Golden Marriage》
判断对象是否可以回收
引用计数算法 (Reference Counting Collector)
通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收
- 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
- 缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以 Java 语言并没有选择这种算法进行垃圾回收。
下面是一个循环引用的例子,在这个例子中使用引用计数法导致两个对象虽然被设置为 null 但是各自引用对方导致引用计数器无法被设置 0,从而导致无法被回收。
1 | public class ReferenceCountingGC { |
可达性分析算法 (Reachability Analysis)
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
可以作为 GC ROOT 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- JVM 内部的引用
- 所有被同步锁(synchronized 关键字)持有的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
此时的 s,即为 GC Root,当 s 置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。
1 | public class StackLocalParameter { |
方法区中类静态属性引用的对象
s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。
1 | public class MethodAreaStaicProperties { |
方法区中常量引用的对象
m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。
1 | public class MethodAreaStaicProperties { |
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
四种引用状态
- 强引用 (StrongReference): 普通的引用都是强引用. 如果一个对象具有强引用,那垃圾回收器不会回收
- 软引用 (SoftReference): 描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java 中的类 SoftReference 表示软引用。
- 弱引用 (WeakReference): 描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 Java 中的类 WeakReference 表示弱引用。
- 虚引用 (PhantomReference): 这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。Java 中的类 PhantomReference 表示虚引用。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
垃圾回收算法
标记 - 清除法 (Mark-Sweep)
先采用标记确定可回收的对象,然后再根据标记清除内容
- 优点:速度快。
- 缺点:容易产生大量的内存碎片,无法满足大对象的内存分配。
复制算法 (Coping)
将内存分为等大小的两个区域,每一次只使用一块区域。当进行垃圾回收时,将不被回收的对象复制到另一块内存区域,清扫当前区域
- 优点:不会产生内存碎片
- 缺点:占用双倍内存,可用内存减半
标记 - 整理法 (Mark-Compact)
先进行标记,将不被回收的内存空间进行移动排序。
- 优点:不会产生内存碎片。
- 缺点:效率低,要进行内存的移动,且要进行程序内存地址的改变。
分代收集算法 (Generational Collection)
将内存分为新生代和老年代
新生代与老年代区域的比为 1:2
新生代分为三个区域分别是 Eden、S0、S1,他们的比例是 8:1:1
为什么要分代收集呢,这是因为在 JVM 中使用标记整理法效率较低,而根据分析大多数时候许多对象的存活的时间较少
JVM 虚拟机的堆结构如下图所示
- 新生代:新分配内存的对象,当年轻代满了会引起 minor gc,minor gc 发生的比较频繁
- STOP THE WORLD EVENT (STW): 所有的 minor gc 都是 STOP THE WORLD EVENT,所有的应用线程都会停止直到操作完成
- 老年代:老年代存一些长期存活的对象,当老年代需要被垃圾回收的时候会触发 major gc 事件
- 永久代:存放 JVM 超参数,classes 和 method 等
算法流程
- 所有新对象被分配的时候都放入 eden 区,两个 survivor 区在开始的时候都为空
- 当 eden 区满的时候触发一次 minor gc
- 仍然有被引用的对象放入第一个 survivor 区 S0,eden 区内无被引用的对象被清空
- 下一次 minor gc 时,eden 区做法相同,但是此时仍然有被引用的对象移动到第二个 survivor 区 S1,此外 S0 内无被引用的对象删除,仍有被引用的对象放到 S1 区。此时 S0 和 eden 区都是空的,S1 区中的一些对象有不同的年龄 (age)
- 下一次 minor gc 时,重复同样的操作,不过此时互换两个 survivor 区,被引用的对象移动区域同时增加年龄
- 在多次 minor gc 后,当一个对象的年龄到达阈值 (假设是 8),此时将这个对象从年轻代移动到老年代
- minor gc 会把一些对象放到老年代的区域
- 老年代快满的时候会发起 major gc,对老年代进行清理并重组。老年代使用的是标记重组法
老年代
以下几种情况会进入老年代
大对象
大对象指需要大量连续内存空间的对象,即使 eden 为空也会进入老年代,这样做主要是为了避免在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。
长期存活对象
对象在 Eden,Survivor 区中每经历一次 Minor GC,年龄就增加 1 岁。当年龄增加到 15 岁时,这时候就会被转移到老年代。这里的 15 可以调整。(Marked Word 里面有一个字段存储 age,为 4 位)
动态对象年龄
如果 Survivor 空间中相同年龄所有对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你 “成年”。
垃圾回收器
垃圾回收器类型
- Serial Garbage Collector
- Parallel Garbage Collector
- CMS Garbage Collector
- G1 Garbage Collector
- Z Garbage Collector
串行垃圾回收器 (Serial Garbage Collector)
- 单线程,采用复制算法。
- 收集器进行垃圾回收时,必须暂停其他所有的工作线程(STW),并采用单独的的线程进行 GC
- 适用于单 CPU、并且对应用程序的暂停时间要求不高的情况,所以不太适合当前的生产环境。
1 | java -XX:+UseSerialGC -jar Application.java |
并行垃圾回收器 (Parallel Garbage Collector)
- 多线程
- 相较于串行垃圾回收器而言性能稍有提升,它也是需要 STW
- 并行垃圾回收器适用于多 CPU 的服务器、并且能接受短暂的应用暂停的程序。
1 | java -XX:+UseParallelGC -jar Application.java |
CMS 收集器
一种以获取最短响应时间为目标的老年代收集器。CMS 收集器的内存回收过程是与用户线程一起并发执行的
- 特点
基于标记 - 清除算法实现。并发收集、低停顿,但是会产生内存碎片
- 应用场景
适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务。
- 运行过程
- 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题
- 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
- 并发清除:对标记的对象进行清除回收(标记清除算法)
1 | # java9标为deprecad java14被完全删除 |
G1 收集器 (Garbage First Garbage Collector)
jdk1.7 以后才引入,替代了 CMS
G1 并没有将内存进行物理划分,它只是将堆内存划分为一个个等大的 Region
- 适用场景
- 同时注重吞吐量和低延迟(响应时间)
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域(Region),每个区域都可以作为 Eden、Survivor、Old、H
- 区域内部是标记 - 整理算法,两个区域之间是复制算法
- G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region (这也就是它的名字 Garbage-First 的由来)
1 | java -XX:+UseG1GC -jar Application.java |
Z 收集器 (Z Garbage Collectors)
Z 收集器是一个低延时收集器,在 java11 中作为实验性质引入
- 应用线程停止时间不超过 10ms
1 | # java15以前 |