Java的GC
不断更新, 预计还需要1个月完成。
Java GC概念
什么是垃圾?
在程序运行中,没有任何指针(引用)指向的对象。
An object is considered garbage when it can no longer be reached from any pointer in the running program.
为什么需要GC?
一个基本认知,是如何不进行GC,内存尽早被消耗殆尽。
如果不及时清理,这些垃圾对象占用的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,而且会产生大量的不连续内存空间,影响大对象的存储,甚至导致内存溢出。
表示数据已不可见,对用户没有价值。
什么时机GC?
怎样GC? 标记 + 清理。
早期在C、C++时代, GC是人工进行的,开发人员使用new进行内存申请,使用Delete关键字内存释放。
虽然灵活,但频繁操作带来管理负担。
新语言的出现,发现了这个问题,因此提出了自动化内存分配和GC的理念,目前多数新语言都是采用自动化GC。
在有了自动GC机制后(如Java),开发人员可以解释了手脚,专注于开发核心流程,而减少对GC的关注。
识别垃圾的方法: 引用计数与可达性分析
- 引用计数。
一个对象A被引用了,它的计数器加个1,当引用失效就减1,当它的计数器为零时,表示这个对象A就是垃圾了。
优点:实现简单,便于标识,判定效率高,回收没有延迟性。
缺点:需要单独的字段存储计数器,占用存储空间;时间开销,每次引用变化的维护。
最为严重的缺点,无法解决循环引用的GC场景,这是一条致命缺陷,导致在Java中没有使用这种方法。
(Python用引用计数法, 使用弱引用。)
- 可达性分析(GC Roots)
Java,C# 使用。
GC Roots 根集合是一组活跃的引用对象,由它出发直接或间接访问到的对象就不是垃圾。
也称作追踪性垃圾回收(Tracing Garbage Collection)
-
虚拟机栈中引用的对象引用
-
本地方法栈内JNI引用对象引用
-
方法区中静态属性引用的对象引用
-
方法区中常量引用的对象引用
-
所有被同步锁持有的对象引用
-
虚拟机内部的引用
有哪几种GC算法
- Mark-Sweep (标记清除算法)
1960年, J. McCarthy提出,应用于Lisp语言。
堆中有效内存空间(available memory)被耗尽时,会停止整个程序(STW)。
进行两项工作,标记,清除。
标记: 从引用根节点(Root)开始遍历,标记所有被引用对象, 在对象的Header记录为可达对象。
清除: Collector 对堆内存从头到尾线性扫描,发现没有标记可达,则视作为垃圾。 并不是真清空,而是把对象地址保存在空闲的地址列表里。 下次有新对象需要加载时,判断垃圾的空间是否足够,如果足够,就直接覆盖。
缺点, 两次对全局对象的全遍历效率不高; 有STW,影响正常的工作流程;
以及比较大的问题(第三点) —— 产生不连续的内存空间(碎片),影响后续对象的分配。
需要维护一个空闲列表。
- Copy (复制算法)
在内存开辟A区和B区,在A区标记为正常对象和垃圾对象,将正常对象都复制到B区。
优点和缺点比较明显: 用空间换时间的理念,换来的是运行高效,复制过去也能保证空间连续性。
局限性
系统中垃圾对象很多,复制算法需要复制的存活对象不能太多
在分代Young-old中的S1和S0就是采用的这个方法。
- Mark-Compact(标记-整理算法,标记-压缩算法)
第一阶段和Mark-Sweep相同。
第二阶段(Compact), 将所有存活对象压缩到内存的一端,按顺序排放。 之后清理边界外所有空间,腾挪出所有空间。
和MS区别, MS是一种非移动式的回收算法。
MC是移动式的,效率要比MS低。 移动过程中也会STW。
System.gc() vs Runtime.gc() vs System.runFinalization
System.gc()
首先看下System.gc方法的源码声明:
Runs the garbage collector.
Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects in order to make the memory they currently occupy available for quick reuse. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all discarded objects.
The call System.gc() is effectively equivalent to the call:
Runtime.getRuntime().gc()
See Also: Runtime.gc()
**建议(提醒)**JVM调用gc方法,以回收垃圾对象。
这里的"suggest" 有一些传神,因为方法不确定是否马上执行gc。
System.runFinalization
强制执行对象的finalize()方法
Java中的Garbage Collector(回收器)
按线程数分,可分为串行垃圾回收器和并行垃圾回收器。
串行,同一时间段只允许有一个CPU用于执行GC,此时STW,直到GC结束。
与串行相反, 并行GC运用多个CPU同时执行GC,提升应用的吞吐量,不过并行与串行回收一样,采用独占式,使用STW机制。
GC的迭代过程,
1999年, JDK1.3.1 Serial GC 串行,第一款GC; ParNew 是其多线程版本
2002年, JDK1.4.2, 推出Parallel GC和 CMS(Concurrent Mark Sweep GC). 在JDK6之后, Parallel GC成为HotSpot默认GC
2012年, JDK1.7u4, G1推出
2017年, JDK9中 G1成为默认GC
2018年,JDK11, 引入 Epsilon GC (No-op 无操作), 同时引入ZGC
2019年, JDK12 优化G1, 引入 Shenandoah GC
2019年, JDK13发布,增强ZGC
2020年,JDK14, 删除CMS,扩展ZGC
七款经典回收器:
串行回收: Serial , Serial Old
并行: ParNew, Parallel Scavenge, Parallel Old
并发: CMS, G1
按代际划分:
新生代: Serial, ParNew, PS
老年代:Serial Old, Parallel Old, CMS
整体: G1
垃圾收集器的组合
JVM堆空间分代,不同代有各自的回收器。
老年代来说: Serial Old 都能组合;
Parallel Old 只能和PS; CMS 不能要PS
(JDK10 CMS 删除)
(JDK 14版 弃用了上面的红线; 绿线标记为Deprecated)
G1, 既能回收新生代,也能回收老年代。
如果是目前的视角, 七种经典回收器的可用组合就是3种了
- Serial GC + Serial Old
- PS + Parallel Old
- G1
回头看对象的引用
强引用,软引用,弱引用,虚引用有什么区别?(Strong/Soft/Weak/Phantom Reference)
强软弱虚 4种引用强度依次递减的引用类型。
除强引用, 剩余三种在java.lang.ref包。
- new对象产生的引用对象便是强引用。 关键字: 宁肯OOM也不回收。
- 软引用,内存溢出之前,将这弱引用对象进行二次回收。 关键字,内存不够时,回收。
- 弱引用,不管内存够不够,只要发现弱引用,就回收。
- 虚引用, 唯一目的是在这个对象被gc时收到一个系统通知。
应用场景,大多数情况我们使用的都是强引用,在使用缓存时会用软引用和弱引用。