JVM中一个垃圾回收线程,它的优先级较低,正常情况下不会执行。JVM空闲或者当前内存不足时,才会触发垃圾回收线程执行,扫描内没有被引用的对象,将这些对象添加到要回收的集合中进行回收。

GC介绍

Garbage Collection 垃圾收集,监测对象是否可用进而实现自动回收对象的目的。

GC调用方法:System.gc() 或Runtime.getRuntime().gc() 。

GC作用:防止内存泄漏。

GC运行方式:作为优先级低的线程运行,不可预知的情况下清除/回收内存中已经死亡或长时间未使用的对象。程序员无法实时调用GC回收对象。

说明

-Xms | -Xmx | -Xmn

堆的初始大小 | 堆的最大值 | 堆中年轻代值

-XX:-DisableExplicitGC

禁用System.gc()

-XX:NewSize | -XX:MaxNewSize

新生代大小 | 新生代最大大小

-XX:NewRation

老生代与新生代的比例

-XX:InitialTenuringThreshold | -XX:MaxTenuringThreshold

老年代阀值 | 老年代最大值

GC触发条件

方法一:引用计数器

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;

方法二:可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

引用计数器 1.为对象创建一个引用计数器,当该对象被引用时,计数器就加1;当该对象的引用被释放后,计数器就减1。 2.当计数器为0时就表明该对象没有被引用,可以被GC回收。但是无法解决循环依赖的问题。

可达性分析法

  1. 可达性分析的操作,必须确保一致性的快照中进行,保证所有引用在此期间不存在变化的情况。
  2. 可达性分析的操作会触发所有用户线程暂停(挂起),官方称为STW。

GC何时调用

  1. 对象没有引用(引用计数器=0,没有可达GC Root对象)
  2. 作用域捕获异常
  3. 程序意外终止 (例如:kill -9)
  4. 执行System.exit()
  5. 程序正常执行结束

GC如何调用

  • System.gc()
  • Runtime.getRuntime().gc()

GC 降低开销

  • 少用静态变量,静态变量属于全局变量,不会被GC回收,一直占用内存。
  • 尽量使用基本类型,而不是包装类(int / Integer),基本类型占用的资源少。
  • StringBuffer替代String来增加字符串。
  • 对象尽量不显示设为null,对象=null,会被作为垃圾处理。将不用的对象设置为null,有利于GC回收。
  • 尽量不显示调用System.gc()。System.gc()不会立即触发Full GC,但是大多时候会触发GC,从而增加了GC频率、增加了STW的次数、影响系统性能。

GC算法

标记-清除算法

第一步,标记需要回收的对象;

第二步,清除回收。

优点:操作简单方便、无需移动对象。

缺点:造成空间碎片,提高垃圾回收的频率。

Java - GC 垃圾回收_jvm

 复制算法

按容量划分为2个大小相等的内存区域,当其中一块用完时将存活的对象复制到另一块中,将已使用的内存空间一次清理掉。

优点:不会产生内存碎片。

缺点:降低了内存空间的使用率,生存周期长达的对象容易被重复复制,浪费CPU资源。

标记-整理算法

标记无用对象,将所有可用的对象移动到一端,直接除掉边界外的对象。

优点:不会存在内存碎片

缺点:需要局部移动,会降低效率。

Java - GC 垃圾回收_java_02

分代算法

根据对象存活周期的不同,将内存分为年轻代、老年代、永久代。

新生代基本采用复制算法,老年代采用标记-整理算。

GC Collector

描述

Serial

最早的单线程串行垃圾回收器

新生代

Serial Old

Serial 垃圾回收器的老年版本,也是单线程的

老年代

ParNew

Serial 的多线程版本

新生代

Parallel

吞吐量优先的收集器(多线程),牺牲等待时间换取吞吐量

新生代

Parallel Old

Parallel 老生代版本。采用标记-整理算法

老年代

CMS

以获得最短停顿时间为目标的收集器,适用 B/S 系统。

老年代

G1

兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项

整体

  • 串行 (必须掌握)
  • 并行(必须掌握)
  • 并发 CMS(必须掌握)
  • G1(必须掌握)

Java - GC 垃圾回收_java_03

Java - GC 垃圾回收_java_04

JDK1.7默认:Parallel Scavenge(新生代) + Parallel Old(老年代)

JDK1.8默认:Parallel Scavenge(新生代) + Parallel Old(老年代)

JDK1.9默认:G1

串行垃圾回收器

比喻:顾客在吃饭,服务员让其起来离开位置,需要打扫,顾客吃饭的行为被中断。

  • 单线程环境,只运行一个线程回收垃圾。
  • 回收时会暂停所有的用户线程 STW

Java - GC 垃圾回收_老年代_05

并行垃圾回收器

比喻:客在吃饭,出现几个清洁工打扫卫生,顾客先出去,一会回来用餐。

  • 多个垃圾收集线程执行垃圾收集工作
  • 用户线程暂停 STW

CMS 并发垃圾回收器

比喻:顾客在吃饭,出现几个清洁工打扫卫生,顾客先去1号卓用餐,一会回到原位置。

  • 垃圾收集与用户线程同时运行

Java - GC 垃圾回收_算法_06

 串行垃圾回收器

  • JVM 开启串行垃圾回收器:-XX:+UseSerialGC
  • JVM开启串行垃圾回收器之后,新生代、老年代会使用相关的组合
  • 新生代:Serial,使用复制算法
  • 老年代:Serial Old,使用标记-整理算法

并行垃圾回收器

新生代并行收集器,是Serial收集器的多线程版本,在多核cpu环境下使用。

  • JVM开启并行垃圾回收器:-XX:+UseParNewGC
  • JVM开启并行垃圾回收器之后,新生代、老年代会使用相关的组合
  • 新生代:ParNew,使用复制算法
  • 老年代:Serial Old,使用标记-整理算法

并发垃圾回收器

又称为吞吐量收集器(Throughout Collector),追求高吞吐量、高效运行CPU。

吞吐量=用户线程时间/(用户线程时间+垃圾回收时间)

使用场景:订单处理、工资支付等

  • -XX:+UseParallelGC:开启此模式,使用ParallelScavenge + Serial Old组合进行垃圾回收。
  • -XX:+UseParallelOldGC: 开启此模式,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收。

CMS

老年代并行收集器,追求最短卡顿时间。

CMS执行步骤

  1. 初始标记 STW ①
  2. 并发标记 ②
  3. 预清理
  4. 重新标记 STW ③
  5. 并发清除 ④
  6. 重置

1. 初始标记 STW
标记从GC Roots直接可达的老年代对象、新生代引用的老年代对象。
整个过程是单线程,其他线程会被暂停执行。

2. 并发标记
开始tracing,标记可达对象。
垃圾回收线程与应用程序并行执行,GC Roots对象会发生改变(例如:对象从新生代进入老年代,老年代中的引用发生改变,之前的引用被标记为dirty)。

3. 预清理

4. 重新标记 STW
暂停所有应用线程,重新标记并发标记遗漏的对象。

5. 并发清除
激活应用程序线程,将未被标记为存活的对象记为不可达。

6. 重置
CMS内部重置回收器状态,准备进入下一个回收周期。

CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。

Java - GC 垃圾回收_老年代_07

G1特点

  • 不产生内存碎片。G1会压缩空闲内存使之足够紧凑,做法是用regions代替细粒度的空闲列表进行分配,减少内存碎片的产生。
  • 可精确控制停顿时间。G1的STW更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间,避免同一时刻回收垃圾过多,造成雪崩。

G1开启

  • 第一步:开启G1垃圾收集器 -XX:+UseG1GC
  • 第二步:设置堆的最大内存 -XX Xms 32g
  • 第三步:设置最大停顿时间 -XX:MaxGCPauseMillis=100

G1 Region

G1取消了年轻代、老年代的物理划分,将堆划分为若干个region,这些region包含了逻辑上的年轻代、老年代区域。

 

Java - GC 垃圾回收_jvm_08

Humongous 巨大: 对象占用空间超过分区容量50%,G1认为这个对象是一个巨型对象。Humongous区专门存放巨型对象,一个H区装不下一个巨型对象,G1会寻找连续的H区来存储。

寻找连续的H区时,会触发Full GC。

G1全局并发标记

第一步:初始标记 (initial mark, STW):标记从根节点直接可达的对象,该阶段执行一次年轻代GC,产生STW。

第二步:根区域扫描(root region scan)

  • 在初始标记活动区扫描对老年代的引用,标记被引用的对象。
  • 该阶段与程序同时运行,只有完成该阶段,才能开始下一次STW年轻代垃圾回收。

第三步:并发标记(Cocurrent Marking):G1 GC在整个堆中查找可访问(存活的)的对象,该阶段与程序同时运行,可以被STW年轻代垃圾回收中断。

第四步:重新标记(Remark, STW):产生STW,针对上一次的标记进行修正。

第五步:清除垃圾(Cleanup,STW):清点和重置标记状态,产生STW。该阶段实际不会做GC,等待evacuation阶段来回收。

G1优化

G1 GC吞吐量目标:90%应用程序时间 + 10% 垃圾回收时间

G1模式

1. Young GC

Eden区使用达到阀值,并且无法申请足够内存时会触发Young GC。

每次Young GC会回收所有Eden和Survior区,将存活对象复制到老年代和Survior区。

2. Mixed GC

收集整个Young Gen和部分Old Gen。G1独有的模式

MixedGC是G1GC特有的,跟FullGC不同的是MixedGC只回收部分老年代的Region。

MixedGC一般会发生在一次YoungGC后面,为了提高效率,MixedGC会复用YoungGC的全局的根扫描结果,因为这个STW过程是必须的,整体上来说缩短了暂停时间。

GC 练习

public class Math {
public static final int initData = 666;
public static User user = new User(); // 实际在堆中分配内存空间,元空间中user指向堆中user的实际地址。
public int math() {
int a = 1;
int b = 2;
int c = (a+b) * 10;
return c;
}
public static void main(String[] args) {
Math demo1 = new Math(); // 实际在堆中分配地址,局部变量表中math是堆中math的存储地址的引用。
demo1.math();
}
}
class User{}

// math()对应的汇编代码如下
public int math();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn

Math.java 操作数栈过程

Java - GC 垃圾回收_G1_09

 Math.java内存分配过程

Java - GC 垃圾回收_老年代_10

Math.class 加载过程

0.加载:将bin-JVM-Math.class加载至JVM内存区域

加载方式:懒加载

war包有1万个文件,实际使用不足10%。实际,90%的文件不会被加载(未使用)。

Math.class加载至JVM内存之前有一些列步骤:

1. 验证

Math.class第1行:cafe babe 0000 0034 0029 0700 0201 0008

如果修改内容:cafe babe 0000 0034 0029 0700 0201 1118

新的字节码文件不符合格式,验证格式错误。则不加载Math.class

2.准备

将静态变量赋予默认值(JVM规定:boolean->false, Integer->0, Object->null 等)

3.解析

3.1 静态链接:符号引号替换为直接引用。(main被称为符号,直接引用就是内存地址。由方法名获取变为直接通过物理地址获取。)
3.2 动态链接:在程序运行期间完成的将符号引用替换为直接引用。

4.初始化

实际赋值,initData 从0变为666。