垃圾回收
1 如何判断对象可以回收
1.1 引用计数法
顾名思义,每被引用一次就+1;
对于循环引用的情况,循环引用的对象就不会被回收,造成内存泄漏。
1.2 可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象;扫描队中的对象,看能否沿着GC Root对象(根对象)为起点的引用链找到该对象,找不到表示可以回收。即先确定根对象,由根对象间接或直接引用的都不能回收。
哪些对象可以作为根对象呢
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈中Native方法引用的对象
- 方法区中类静态属性引用的对象 以及 常量引用的对象
1.3 四种引用
1.强引用
- 通过new 关键字的即是强引用
- 只有 所有GCRoots对象 都不通过【强引用】引用该对象,该对象才能被垃圾回收
2.软引用(SoftReference)
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身
3.弱引用(WeakReference)
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身
4.虚引用(PhantomReference)
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由 Reference Handler线程调用虚引用相关方法释放直接内存 – 即调用Cleaner根据记录的直接内存地址来回收直接内存
5.终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
软引用应用
public static void soft() {
List<SoftReference<byte[]>> list= new ArrayList<>();
for(int i=0;i<5;i++){
SoftReferencec<byte[]> ref=new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.outprintln(list.size())
}
System.out.println("活环结束:"+list.size());
for(SoftReference<byte[]> ref :list){
System.out.println(ref.get());
}
}
使用引用队列清除无用的软引用
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++){
SoftReference<byte[]> ref=newSoftReference<>(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.outprintln(reference.get());
}
}
弱引用应用
public static void soft() {
List<WeakReference<byte[]>> list= new ArrayList<>();
for(int i=0;i<5;i++){
WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
for(WeakReference<byte[]> w :list){
System.out.println(w.get());
}
}
}
2 垃圾回收算法
2.1 标记清除
标记出所有需要回收的对象,回收被标记对象
2.2标记整理
标记出所有需要回收的对象,回收被标记对象,将未被标记对象的内存 整理成 连续的内存空间,但这个过程会使得对象地址改变,因而
2.3 复制
将内存划分为等大的两块,每次只使用其中的一块。当一快用完了触发垃圾回收:标记出所有需要回收的对象,将未被标记的对象复制到另一块内存空间,再一次性把现内存空间回收;下次触发垃圾回收时又将另一块存活的复制到这块,在一次性把另一块回收,循环往复。
- 不会有内存碎片
- 需要占用双倍内存空间,内存利用率不高
2.4 分代算法
将内存区域划分为新生代和老年代,针对不同区域采取不同算法:
- 新生代采取复制算法,老年代采取标记整理/标记清除算法
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发新生代回收(Minor GC),伊甸园存活的对象复制到幸存区中,存活的对象年龄加1;下次触发Minor GC时,伊甸园和幸存区存活的对象,存活的对象年龄加1,复制到另一个幸存区,循环往复
- From 与 To 可理解为从这个幸存区From复制到另一个幸存区To,因而幸存区名字并不固定,笔者认为只为方便记忆
- Minor gc会引发 stop the world(STW),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过一个阈值时,会晋升到老年代,最大寿命15(4bit)
- 当老年代空间不足,会先尝试触发新生代回收,如果之后空间仍不足,则会触发Full GC,STW时间更长
注意:
若其他线程的运行不会影响主线程的数据情况,则其他线程的内存溢出,不会影响主线程的正常运行
3 垃圾回收器
总的来说垃圾回收器可分为四种:串行、并行、并发、G1
3.1 串行
Serial收集器:对新生代采取复制(Serial),对老年代采取标记整理(Serial Old)
对应的JVM参数:-XX:+UseSerialGC
3.2 并行
- 多垃圾回收线程并行工作,会暂停其他所有线程
- 适用于弱交互的
ParNew收集器: 只针对新生代的回收,是Serial收集器新生代的并行多线程版本,同样对新生代采取复制。
对应的JVM参数:-XX:+UseParNewGC – ParNew + Serial Old的收集器组合
Parallel收集器:对新生代采取复制(Parallel Scavenge),对老年代采取标记整理(Parallel Old)。
-
Parallel Scavenge收集器:类似ParNew收集器也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。 吞吐量: 运行用户代码时间 占比 (运行用户代码时间 + 垃圾回收时间) 如程序运行10分钟, 垃圾回收5秒,吞吐量约为99% -
Parallel Old收集器:是SerialOld收集器老年代代的并行多线程版本
对应JVM参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC – Parallel Scanvenge + Parallel Old的收集器组合
ParNew收集器 与 Parallel Scavenge收集器 区别 – Parallel Scavenge收集器的自适应策略
- 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量
3.3 并发
CMS收集器 :Concurrent Mark Sweep: 并发标记清除 ,是一种以获取最短回收停顿时间为目标的收集器。
- 初始标记:标记根对象直接引用的对象,会STW即停止其他线程
- 并发标记:找出根对象简接引用的存活对象,其他线程并发执行
- 重新标记:修正并发标记期间因其他线程继续运行而导致标记产生变动的那一部分对象的标记记录,会STW
- 并发清除:回收将要回收对象
对应的JVM参数: -XX:+UseConcMarkSweepGC (自动将-XX:+UseParNewGC打开)– ParNew + CMS + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
注意:
- 无法处理浮动垃圾,在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,可能在清除之前该对象已经变成了垃圾对象,最终造成本该被回收但没有被回收,只能等待下一次GC再将该对象回收
- 要是CMS运行期间预留的内存无法满足程序的需要,就会出现Concurrent Mode Failure,降级临时启用Serial Old收集器进行老年代的垃圾收集,STW时间也就因此变长
3.4 G1
G1适用于全堆,既可以在新生代使用和老年代使用
G1收集器: 一个有整理内存过程的垃圾收集器,不会产生很多内存碎片;STW更可控,在停顿时间上加了预测机制,用户可以指定期望停顿时间。
可分为以下四个步骤:
-
初始标记:标记根对象直接引用的对象,会STW即停止其他线程; 并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。 每个 Region 记录着两个 top-at-mark-start (TAMS) 指针,分别为 prevTAMS 和 nextTAMS ,在 TAMS 以上的内存空间对应的对象就是新分配的 -
并发标记:找出根对象简接引用的存活对象,其他线程并发执行 -
最终标记:修正并发标记期间因其他线程继续运行而导致标记产生变动的那一部分对象的标记记录,会STW -
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,会STW
垃圾回收阶段
新生代回收:进行GC Root的初始标记,会STW;
新生代回收+并发标记:老年代占用堆空间比例达到阈值时,进行并发标记(不会STW);
混合回收:进行全面的回收,最终标记和拷贝存活,会STW。(在最大的停止时间内,为了垃圾回收后的释放的堆空间较多,会选择内存较大的空间
跨代引用
老年代引用新生代对象
垃圾回收时需要根据根对象进行可达性分析,而老年代可能也存有根对象,跨代引用可以不需要完全遍历老年代,而是通过遍历脏卡来得到根对象。
类卸载
所有对象都经过并发标记后,就能知道那些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
回收巨型对象
4 垃圾回收调优
调优领域
确定目标
低延迟还是高吞吐量,选择合适的回收器
- CMS G1 ZGC – 低延迟
- ParallelGC --高吞吐量
- Zing 虚拟机 – 低延迟
4.1 最快的GC是不发生GC
因为如果频繁的发生GC那就说明STW的时长越长,那么对程序的运行也是不利的。
-
查看FullGC前后的内存占用,考虑以下问题
-
数据是不是太多 如数据库操作结果集,多加载了些不必要的数据,可通过limit限制 -
数据表示是否太臃肿 -
是否存在内存泄漏 static Map map 这种长度存在的对象 软引用/弱引用 第三方缓存实现 如中间件 redis
4.2 新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常链家
- 死亡对象的回收代价是0
- 大部分对象用过即死
- 新生代回收Minor GC时间远远低于Full GC
新生代内存越大越好嘛:
- 小:就可能会频繁出发Minor GC;Oracle推荐占据堆内存的25%-50%;
- 太大:那么回收所需时间的就可能越长。
理想情况:
4.3 老年代调优
以CMS为例
CMS垃圾收集器有两点缺陷,其一是无法处理浮动垃圾,即在并发清理的过程中,用户线程又产生了新的垃圾,这些垃圾无法标记只能等到下次FullGC进行清理。所以老年代需要预留一定的空间装下浮动垃圾。在CMS过程中就可能导致并发失败,即CMS运行时内存空间无法满足,这时虚拟机才将Serial Old拿出来并且STW,进行串行的垃圾清理。
-
CMS的老年代内存越大越好,避免浮动垃圾引起的并发失败 -
先尝试不做调优,如果没有Full GC那么说明老年代没有内存空间不足,即使有发生Full GC,先尝试调优新生代 -
观察发生Full GC时,老年代内存占用,将老年代内存预设调大1/4 ~1/3,减少Full GC的发生 -XX:CMSInitiatingOccupancyFraction=percent 调整启动CMS时老年代已用内存占比值
注明:图片来源字母站JVM系列视频截图
|