一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情。
如何判断对象可以回收
引用计数法
引用计数的缺陷是无法回收循环引用的对象,可能会发生内存泄漏。Java 中没有使用这种方法来判断对象是否可以回收。
可达性分析
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象- 扫描堆中的对象,看是否能够沿着
GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root?
GC Root是一系列对象,指的是引用的对象,并不是引用变量。
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 演示GC Roots
*/
public class GCRoots {
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();
list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}
}
jpsjmap -dump:format=b,live,file=1.bin 21384format=b表示二进制文件live表示只转储存活对象,所以在转储之前会进行一次垃圾回收file用于指定要转储的文件
- 可以使用
Memory Analyzer(MAT)工具来分析。
五种引用
五种引用:
- 强引用:被强引用引用的对象,不会被垃圾回收。
- 软引用
SoftReference:只被软引用引用的对象,在内存不足的时候,会被Full GC垃圾回收。软引用本身也是一个对象,如果在创建软引用对象时分配了引用队列,在软引用引用的对象被垃圾回收时,会把软引用对象本身放入引用队列。 - 弱引用
WeakReference:只被弱引用引用的对象,不管内存是否充足,都会被Full GC垃圾回收。弱引用本身也是一个对象,如果在创建弱引用对象时分配了引用队列,在弱引用引用的对象被垃圾回收时,会把弱引用对象本身放入引用队列。
因为软引用和弱引用也占用内存,如果想释放它们占用的内存,可以借助引用队列来处理。
- 虚引用
PhantomReference:虚引用的作用主要是为了释放直接内存,防止内存泄漏。直接内存不在JVM中,无法被垃圾回收。因此使用虚引用对象Cleaner存放直接内存地址,当ByteBuffer对象被回收时,虚引用对象会被放入引用队列,ReferenceHandler线程会查看引用队列,如果发现有新入队的Cleaner,就会调用它的clean()方法,在这个方法里,会调用Unsafe对象的freeMemory()方法释放直接内存,以避免直接内存的内存泄漏问题。 - 终结器引用
FinalReference:无需手动编码。所有的Java对象都会继承Object父类,其中有一个finalize()终结方法,当对象重写了finalize()方法,我们希望该对象在垃圾回收的时候会执行该方法。当对象没有强引用引用时,JVM会创建与之对应的终结器引用,并放入引用队列。由一个优先级很低的Finalizer线程查看引用队列中是否有终结器引用,如果有,会根据该引用找到刚才被垃圾回收的对象,调用该对象的finalize()方法,调用完之后,在下一次垃圾回收时就可以真正回收该对象占用的内存空间了。
虚引用和终结器引用必须配合引用队列来使用,它们创建的时候都会关联一个引用队列。
软引用演示
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class SoftRefDemo {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
/*List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();*/
soft();
}
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
软引用配合引用队列可以清理软引用
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示软引用, 配合引用队列
*/
public class RefQueueDemo {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
弱引用演示
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class WeakRefDemo {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}
垃圾回收算法
标记清除
标记清除 Mark Sweep 分为两个阶段:
- 标记
- 清除:清除阶段不会将内存清零,而是会记录空闲内存的起始地址与结束地址,放到空闲地址列表中就可以了。这样下次分配内存的时候就可以查看该列表分配空间。
优缺点:
- 速度快
- 会产生内存碎片
标记整理
标记整理 Mark Compact:
- 标记
- 整理
优缺点:
- 速度慢
- 没有内存碎片
复制
复制 Copy:
- 不会有内存碎片
- 需要占用双倍内存空间
分代垃圾回收
Eden区与Survivor From和Survivor To占用的内存比例默认为8:1:1。
- 新创建的对象被放在
Eden区。 Eden空间不足时,进行Minor GC,又称为Young GC,指发生在新生代的垃圾收集动作。新生代垃圾回收使用复制算法。- 将
Eden的存活对象移动到Survivor To区域,并设置它们的年龄为1 - 将
Survivor From区域的存活对象年龄+1,如果小于等于15,则复制到Survivor To区域,如果大于15则放到老年代。 - 交换
Survivor From和Survivor To区域的指针,从而保证Minor GC后,Survivor To区域是空的。 - 如果
Survivor To放不下,就放到老年代,老年代还放不下,进行Major GC,又称作Full GC。老年代垃圾回收采用的是标记-整理算法。 GC过程中会STW,暂停用户线程。
在什么情况下会把新生代的对象移动到老年代?
Survivor To放不下了- 存活对象年龄
+1后到达年龄阈值。年龄阈值默认为15,对象头中占用4bit,可以通过虚拟机参数-XX:MaxTenuringThreshold设置。
对象每次发生
Eden、Survivor From和Survivor To之间的跨区移动,年龄都会+1。
大对象直接进入老年代
- 大对象指的是占用内存较大的对象,这个策略的作用是避免大内存复制。
- 可以通过虚拟机参数
-XX:PretenureSizeThreshold来设置,如果对象占用的内存大于该阈值,直接进入老年代。 - 该参数默认值为
0,表示不论对象有多大,总是优先放入Eden区。
动态对象年龄判定
- 如果
Survivor区域中,同年对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代
老年代空间分配担保
Minor GC前,检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,则进行Minor GC- 如果老年代空间不够,检查是否允许晋升失败,也就是虚拟机选项
HandlePromotionFailure,如果允许晋升失败,则进行Minor GC - 如果
HandlePromotionFailure不允许晋升失败,也就是说设置成了-XX:-HandlePromotionFailure,检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试Minor GC。否则,进行Full GC - 推荐开启
HandlePromotionFailure,也就是允许晋升失败,能用Minor GC解决的,就不用Full GC,减少Full GC的频率有助于提高性能
相关虚拟机参数
| 含义 | 参数 |
|---|---|
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio,默认为 8,指的是 Eden 区域占比 |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
演示代码
import java.util.ArrayList;
/**
* 演示内存的分配策略
*/
public class GCDemo {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
- 当一个线程抛出
OOM异常后,它所占据的内存资源会全部释放,从而不会影响其他线程的运行。
垃圾回收器
垃圾回收器主要分为三种:
- 串行
- 吞吐量优先
- 响应时间优先
串行 Serial 与 Serial Old
- 单线程
- 适用于堆内存较小,适合个人电脑这种
CPU核心数少的工作环境
串行垃圾回收器参数
-XX:+UseSerialGCSerialGC = Serial + SerialOld- 其中
Serial工作在新生代,采用复制算法 SerialOld工作在老年代,采用标记-整理算法
吞吐量优先 Parallel Scavenge 与 ParallelOld
- 多线程
- 适用于堆内存较大,多核
CPU的工作环境 - 让单位时间内,
STW时长最短。比如一秒钟垃圾回收了两次,每次STW都是0.2s。
吞吐量优先垃圾回收器参数
-XX:+UseParallelGC和-XX:+UseParallelOldGC,它们在JDK 1.8默认开启。这两个开关只要开启一个,另外一个自动连带开启。ParallelGC采用复制算法,用于新生代;ParallelOldGC采用标记-整理算法,用于老年代- 当
CPU核心数较少时,垃圾回收线程个数默认与CPU核心数相等。垃圾回收过程中,CPU使用率会大幅上升。垃圾回收线程数可以使用-XX:ParallelGCThreads=n来指定。 -XX:+UseAdaptiveSizePolicy采用自适应大小策略,用于调整Eden与Survivor比例,包括堆大小、晋升阈值都会受到影响。-XX:GCTimeRatio=ratio:GC时间占比。计算公式为1 / (1 + ratio),ratio默认为99,也就是说垃圾回收的时间不能占比超过总时间的1%。垃圾回收器会调整堆的大小,一般会增大堆,以达到期望目标。一般会设置为19,占比5%,因为设置成99很难达到。-XX:MaxGCPauseMillis=ms:最大GC暂停毫秒数,默认为200ms。与上面的参数冲突,因为上面的冲突会增加堆大小,堆大小一单增大,每次垃圾回收的用时就会增加。
响应时间优先 Concurrent Mark Sweep
- 多线程
- 适用于堆内存较大,多核
CPU的工作环境 - 让单次
STW时长最短。比如一秒钟垃圾回收了五次,每次STW都是0.1s。 CMS用于老年代,新生代配合ParNew使用,后者是Serial的多线程并行版本
垃圾收集器的并行与并发:与传统意义上的并行、并发不一样
- 并行:用户线程暂停,多个垃圾回收线程同时运行
- 并发:用户线程和垃圾回收线程同时运行
响应时间优先垃圾回收器参数
-XX:+UseConcMarkSweepGC、-XX:UseParNewGC,如果并发失败,老年代会从CMS退化成SerialOld- 分为四步:初始标记、并发标记、重新标记、并发清除,初始标记和重新标记需要
STW -XX:ParallelGCThreads=n,并行垃圾回收线程数,一般与CPU核数相同-XX:ConcGCThreads=threads,并发垃圾回收线程数,建议设置为并行线程数的1/4- 并发标记和并发清除的时候,用户线程会产生新的垃圾,被称作浮动垃圾,会在下次
GC的时候清除。为了预留一部分内存供用户线程使用,可以使用-XX:CMSInitiatingOccupancyFraction=percent控制执行CMS垃圾回收的内存占比。JDK 5默认为68%,JDK 6默认为92%。该参数如果设置的太高,会导致并发失败,将冻结用户线程,启用SerialOld进行老年代的垃圾收集。 -XX:+CMSScavengeBeforeRemark:在重新标记前,扫描新生代,回收垃圾。这样可以减少重新标记阶段老年代的标记压力。
G1
Garbage FirstJDK 7u4官方支持,可以使用-XX:+UseG1GC启用JDK 9默认
适用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是
200ms - 也具有分代思想,将新生代和老年代化整为零
- 将堆划分成多个大小相等的
Region,根据回收价值优先级回收 - 超过
Region容量一半的对象被认为是大对象,会被放到Humongous区域,一般被当作老年代处理 - 整体上采用标记-整理算法,两个区域之间采用的是复制算法
G1垃圾回收阶段
G1 垃圾回收阶段一:Young Collection
Eden满了会进行新生代的垃圾回收,此时会STW- 采用复制算法将新生代拷贝到幸存区
- 幸存区对象较多,或者超过年龄,晋升到老年代
G1 垃圾回收阶段二:Young Collection + CM
- 在
Young GC的时候会进行GC Roots的初始标记,任务是找到根对象。初始标记在Young Collection的时候已经完成了。 - 并发标记的任务是从根对象出发,顺着引用链标记其他对象
- 老年代占用堆内存空间比例达到阈值时,进行并发标记(不会
STW),由下面的JVM参数决定 -XX:InitiatingHeapOccupancyPercent=percent,默认45%
G1 垃圾回收阶段三:Mixed Gollection
- 对
E、S、O进行全面垃圾回收 - 最终标记
Final Marking会STW,用于处理并发标记阶段留下来的SATB记录。 - 筛选回收会
STW,把回收价值高的Region中的存活对象复制到空的Region中。
Young Collection 跨代引用
- 记忆集用于记录跨代引用,从而避免在
Minor GC的时候扫描整个老年代,它的一个实现是卡表。 - 采用卡表,将老年代
Region细分为卡片,每个占据约512kb。 - 如果引用了新生代对象,对应的卡片标记为脏卡,这样在遍历老年代的时候就不用遍历整个老年代了。
- 在引用变更时,通过写屏障来维护卡表,把更新脏卡的指令放到脏卡队列中,由一个线程完成脏卡更新操作。
并发标记
三色标记法:
- 黑
- 白
- 灰
G1 垃圾收集器需要借助写前屏障和 SATB 标记队列 satb_mark_queue 来跟踪并发时的指针变化情况。