目录
0垃圾的定义
1.什么是垃圾
2.如何定位垃圾对象
引用计数法:
根可达算法:
什么对象是根对象?
线程变量:
静态变量:
常量池:
JNI指针 :
1.GC 简介
1.1. 引言
1.2. 何为 GC
1.2.1. 手动 GC
1.2.2. 自动 GC
引用计数法
可达性分析
2.GC入门分析
2.1.碎片整理
1)对象创建时,执行写入操作越来越耗时
2)内存分配错误
2.2. 分代设想
2.3. 对象分配
对象内存分配过程
2.4. GC 模式分析
3.GC 算法基础
3.1. 标记可达对象
3.2. 移除不可达对象
标记-清除(Mark-Sweep)
标记-清除-整理(Mark-Sweep-Compact)
标记-复制 (Mark and Copy)
4.GC 算法实现
4.1. GC 算法实现简介
4.2. GC 收集器应用分析
4.2.1. Serial 收集器应用分析
4.2.2. Parallel 收集器应用分析
4.2.3. CMS 收集器应用分析
4.2.4. G1 收集器应用分析
没有任何引用指向的一个对象或者多个对象
对于某个对象而言,只要应用程序中持有对该对象的引用,就说明该对象不是垃圾,否则,如果一个对象都没有引用该对象,认为该对象就是垃圾。
弊端:如果A,B两个对象相互持有对方的引用,而没有其他对象来持有A,B的引用,也就是说这来那个对象本质上是垃圾,但是永远不能被回收。
通过GC的root对象,开始时乡下寻找,看某个对象是否可达,遍历到的对象,就不是垃圾,否则就是垃圾。
Java 程序运行时会启动一个线程栈,栈里会有main方法里边的马上调用执行的这些开对象,被称为根对象。从类型上分,主要有 线程栈变量,静态变量,常量池,JNI指针。
是不是根对象,首先得看是不是main栈帧里边的对象。
main线程中用到的线程变量
class load到内存够厚哦,马上会对静态变量赋默认值,初始化,所以main线程中的静态变量算根对象。
main方法中用到的其他类变量,这些个对象的创建要去常量池中看他的类结构,所以这些个对象也算一个。
main方法 中如果用到C++\C写的本地方法,这些本地方法用到的类或者对象,也是根对象
在理解 GC 之前,先回顾一下 JVM 体系结构,如下图所示:
int send_request() { size_t n = read_size(); int *elements = malloc(n * sizeof(int)); if(read_elements(n, elements) < n) { // elements not freed! return -1; } // … free(elements) return 0; }手动 GC 时忘记释放内存是相当容易的。这样会直接导致内存泄漏。
JAVA 中堆内存的内存结构如下图所示:
备注:
什么样的对象在栈上分配? 满足: 1.线程私有的小对象 2.没有逃逸: 线程私有,其他线程没有调用该对象 3.支持标量替换:用对象中的普通属性代替整个对象,如类 T中就两个成员变量 int a ;int b ; 那么完全可以用这两个属性代替T这个类;没必要在构建T这个对象; 4.无需调整 什么对应会进行线程本地分配TLAB(Thread local allocation buffer)? 设置TLAB的背景: 因为eden是多线程进程的场所,所以就会存在多线程间的同步问题,效率就会相对降低,为了提高效率,产生了TLAB. TLAB: 1.每个线程默认可以在 eden中取1%的空间,这个空间是该线程独有的 2.每个线程再分配对象时,优先在该线程独有的这块空间分配,这里不会和其他线程产生争抢,所以提高了效率 3.一般无需调整这块的大小
首先,GC 遍历(traverses)内存中整体的对象关系图(object graph)确定根对象,那什么样的对象可作为根对象呢?GC 规范中指出根对象可以是:
1) 栈中变量直接引用的对象 2) 常量池中引用的对象 3) … 其次,确定了根对象以后,进而从根对象开始进行依赖查找,所有可访问到的对 象都认为是存活对象,然后进行标记( mark )。 说明:标记可达对象需要暂停所有应用线程 , 以确定对象的引用关系。其暂停的 时间 , 与堆内存大小、对象的总数没有直接关系 , 而是由存活对象 (aliveobjects) 的数量来决定。移除不可达对象(Removing Unused Objects)时会因 GC 算法的不同而不同, 但是大部分的 GC 操作一般都可大致分为三类:
标记清除(Mark-Sweep),标记清除整理(Mark-Sweep-Compact),标记复制(Mark-Copy).
优点:算法相对简单 缺点: 1.算放上要进行扫描两遍,效率偏低。 2.容易产生内存碎片 适应场景:存活对象比较多,即垃圾较少的对象下效率较高,因为第二次遍历(清理垃圾)时,就快一些。比如Eden中垃圾较多,就不适合这种算法。 两次扫描:第一遍找出所有存活的对象,第二遍清理垃圾对象对于标记清除算法(Mark and Sweep algorithms)应用相对简单,但内存会产生大量的碎片,这样再创建大对象时,假如内存没有足够连续的内存空间可能会 出现 OutOfMemoryError 。
优点: 清理了垃圾,不产生内存碎片,不造成内存的浪费 缺点: 1.算法设计上要进行两次扫描,效率偏低 2.在压缩(整理)的过程中需要调整对象的引用,效率偏低, 适应场景: 对GC停顿时间要求不太严格的场景
优点: 1.只扫描1次堆内存,提高了效率 2.没有产生内存碎片 缺点: 1.空间浪费严重 2.移动复制对象的过程中需要调整对象的引用 适用场景: 因为它是一次性清除垃圾,所以更适应于 存活对象较少,即垃圾较多的场景,因为一次性就能删掉很多的垃圾,比如Eden的垃圾较多时,就可以使用标记复制算法。
Young | Tenured | JVM options |
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
其中: 1) 年轻代和老年代的串行收集器: ( Serial GC) 2) 年轻代和老年代的并行收集器: (Parallel GC) 3) 年轻代的并行收集器 (Parallel New) + 老年代的并发收集器CMS(Concurrent Mark and Sweep) 4) 年轻代和老年代的 G1 收集器 , 负责回收年轻代和老年代说明:除了以上几种组合方式外,其它的组合方式要么现在已经不支持,要么不推荐。如何对这些组合进行选择,要结合系统的特点。例如系统是追求高吞吐量 还是响应时间,还是两者都要兼顾。总之,对于 GC 组合的选择没有最好,只有 更好。知己知彼,才能百战不殆。结合当前系统的环境配置,性能指标以及 GC 器特点,不断进行 GC 日志分析,定位系统问题,才是一般是选择哪种 GC 的关 键。
JDK的诞生时serial垃圾回收器就跟随而生,它是单线的,能够满足当时几十兆的内存情形。一开始很好用。伴随着JVM对内存的需求越来越大,诞生了多线程的parallel scavenge (ps)和 parallel old (po) 垃圾回收器。一开始的时候内存也就几G,用的挺好。后来内存需求扩展到几十个G,ps,po的stw问题越来越突出。 为了降低STW,诞生了CMS,为了配合CMS的使用,出现了paralle new ;当然CMS时代也就是针对基本上小于32G的内存。
CMS是里程碑式的GC,它开启了并发回收的先河,但是CMS的问题也比较多,因此目前没有一个JDK的默认垃圾回收器是CMS。目前G1逐渐成熟起来,JDK9默认就是G1。
其应用参数配置: -XX:+UseSerialGC 总之, Serial GC 一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有 的工作线程。适合单 CPU 小应用,实时性要求不是那么高场景。一般在 JVM 的 客户端模式下应用比较好。备注: savepoint 用户线程停止的安全点,程序不是说停止就能立刻停止的,比如说对加了锁的对象得unlock后才能停。savepoint 是STW的开始点
1) -XX:ParallelGCThreads=20:设置并行收集器的线程数,即:同时多少个 线程一起进行垃圾回收。此值最好配置与处理器数目相等。 2) -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果 无法满足此时间, JVM 会自动调整年轻代大小,以满足此值。 3) -XX:+UseAdaptiveSizePolicy 设置并行收集器自动选择年轻代区大小和 相应的 Survivor 区比例,以达到目标系统规定的最低响应时间或者收集频 率等,此值建议使用并行收集器时,一直打开。 4) -XX:GCTimeRatio=99 ,设置吞吐量大小,默认值就是 99,也就是将垃圾回 收的时间设置成了总时间的 1% 。它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n ,那么系统将花费不超过 1/(1+n) 的时间用于垃 圾收集。 总之, Parallel GC 是一种并行收集器,可利用多 CPU 优势,执行并行 GC 操 作,吞吐量较高,并可有效降低工作线程暂停时长。但是因为垃圾收集的所有阶 段都不能被打断,所以 Parallel GC 还是有可能导致长时间的应用暂停。所以 Parallel GC 适合于需要高吞吐量而对暂停时间不敏感的场合,比如批处理任 务。 说明:所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞 吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
CMS产生的原因是无法忍受STW了。
CMS 的官方名称为 “ Mostly Concurrent Mark and Sweep Garbage Collector” ,其设计目标是追求更快的响应时间。 CMS (并发收集器)应用特点: 1) 使用空闲列表 (free-lists)管理内存空间的回收,不对老年代进行碎片整理,减少用户线程暂停时间。 2) 在标记 - 清除阶段的大部分工作和用户线程一起并发执行。 3) 最大优点是可减少停顿时间(可提高服务的响应速度) ,最大缺陷是老年代的内存碎片 CMS (并发收集器)场景应用: 1) 应用于多个或多核处理器,目标降低延迟,缩短停顿时间 , 响应时间优先 . 2) CPU 受限场景下,因与用户线程竞争 CPU ,吞吐量会减少。 CMS (并发收集器)算法应用: 1) 年轻代采用并行方式的 mark-copy ( 标记 - 复制 ) 算法。 2) 老年代主要使用并发 mark-sweep ( 标记 - 清除 ) 算法。 CMS (并发收集器)关键步骤分析: 1) 初始标记( initial mark )此阶段标记一下 GC Roots 能直接关联到的对象,速度很快,产生stw的时间很小。 2) 并发标记(concurrent mark)此阶段就是进行 GC Roots Tracing 的过程,从直接关联对象遍历所有可达对象,然后进行标记。 (IBM以前做过统计,80%的GC时间都用在并发标记上,而CMS的并发设计,让用后线程和标记线程同时执行,客户会感觉慢了点,但是至少程序没有暂停,较好的解决了STW问题) 3) 重新标记( final remark)此阶段要修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 (由于并发标记过程中用用线程也不会太多,标记时间也不会用的太久,产生的新的对象和变动了标记的对象也不会太多,重新标记时又是多线程并行标记,所以这个过程很快,产生的stw也比较少) 4) 并发清除( concurrent sweep )此阶段与应用程序并发执行 , 不需要 STW 停顿。目的是删除未使用的对象 , 并收回他们占用的空间。 5) 并发重置 (concurrent Reset) 此阶段与应用程序并发执行 , 重置 CMS 算法相关的内部数据,同时 GC 线程切换到用户线程。 (并发清理的过程中产生的垃圾,没办法 ,只能下一次再清理,这些垃圾被称为浮动垃圾) CMS (并发收集器)实践应用: 使 用 CMS 的 参 数 配 置 :-XX:+UseConcMarkSweepGC , 默 认 开 启 - XX:+UseParNewGC其它参数配置: 1) -XX:+ UseCMSCompactAtFullCollection 执行 Full GC 后,进行一次碎 片整理;整理过程是独占的,会引起停顿时间变长 2) -XX:+CMSFullGCsBeforeCompaction 设置进行几次 Full GC 后,进行一次碎片整理 3) -XX:ParallelCMSThreads 设定 CMS 的线程数量(一般情况约等于可用CPU 数量)总之,
优点:并发收集,低停顿
缺点:
1.CMS设计的初衷是为了减少STW,但是由于本身算法的限制(标记清除算法),天然的会产生内存碎片,一旦碎片一多,yong区的大对象进入old区时,一旦找不到足够的空间,就会导致promotionFaild(升级失败),这个时候CMS的解决办法就是调用serialOld垃圾回收器来做标记清除整理回收。这个老太太是单线程的也是针对几十M内存设计的回收器。所以面对大内存的CMS 时代,回收效率会特别慢,可能会产生几个小时到1天的STW。
2.并发清理会产生浮动垃圾
解决办法:
可以降低CMS FGC的阈值,默认是92%才FGC,可以调低一些,比如 60%时就进行FGC,这样old区的垃圾就及时清理了。留出来的空间就多了一些,能够很好的容纳Yong区的垃圾了。–XX:CMSInitiatingOccupancyFraction 92%
PS 和 PN区别的延伸阅读: https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-3D0BB91E-9BFF-4EBB-B523-14493A860E73
1) -XX:MaxGCPauseMillis=200 - 设置最大 GC 停顿时间(GC pause time) 指标 (target). 这是一个软性指标 (soft goal), JVM 会尽力去达成这个 目标 . 所以有时候这个目标并不能达成 . 默认值为 200 毫秒 . 2) -XX:InitiatingHeapOccupancyPercent=45 - 启动并发 GC 时的堆内存 占用百分比 . G1 用它来触发并发 GC 周期 , 基于整个堆的使用率 ,而不只是 某一代内存的使用比例。值为 0 则表示“一直执行 GC 循环 )'. 默认值为 45 ( 表示堆使用了 45%). 总之: G1 是 HotSpot 中最先进的准产品级 (production-ready)垃圾收集器。重要的 是 , HotSpot 工程师的主要精力都放在不断改进 G1 上面 , 在新的 java 版本 中 , 将会带来新的功能和优化。
-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
-XX:+UseParNewGC = ParNew + SerialOld
这个组合已经很少用(在某些版本中已经废弃)
java - Why Remove support for ParNew+SerialOld andDefNew+CMS in the future? - Stack Overflow
-XX:+UseConcMarkSweepGC = ParNew + CMS + Serial Old(CMS一旦产生promotionFailed后才使用)
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-XX:+UseG1GC = G1
Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
java +XX:+PrintCommandLineFlags -version
通过GC的日志来分辨
Linux下1.8版本默认的垃圾回收器到底是什么?
1.8.0_181 默认(看不出来)Copy MarkCompact
1.8.0_222 默认 PS + PO
JVM的命令行参数参考:百度安全验证https://baijiahao.baidu.com/s?id=1782119405007668815&wfr=spider&for=pc
HotSpot参数分类
标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消
java -version
java -X
import java.util.List; import java.util.LinkedList; public class HelloGC { public static void main(String[] args) { System.out.println("HelloGC!"); List list = new LinkedList(); for(;;) { byte[] b = new byte[1024*1024];// 每次1M list.add(b); } } }