垃圾回收基础
判断对象存活算法(哪些对象需要回收?)
引用计数法
给对象中添加一个引用计数器,每当一个地方引用它时,计数器的值就+1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:简单高效
缺点:很难解决出现两个对象之间循环引用的问题。
可达性分析算法
基本思路:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,此时这个对象时不可用的。

可视为GC Roots的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象
- 被synchronized关键字持有的对象
引用类型
- 强引用:Object obj = new Object()这类都属于强引用,只要强引用在垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:如果内存不足,会被垃圾回收器回收
- 弱引用:当垃圾收集器工作时,无轮内存是否充足,都会回收弱引用对象。
- 虚引用:也叫幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响。
什么时候回收?
真正要宣告一个对象死亡,需要经历两次标记过程。
- 如果没有发现对象和GC Roots相连,它会被第一次标记并进行一次筛选
当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行,直接回收
有必要执行时,则会将对象放入一个F-Queue的队列中去执行,随后就会进行第二次的小规模标记,还是不可达就进行回收。
回收方法区
《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集(通常在方法区手机的“性价比”比较低,JDK11中的ZGC不支持类卸载。)
在大量使用反射、动态代理、CGLib等字节码框架,需要使用插件机制、JSP、OSGi等频繁自定义类加载器的场景中,需要垃圾收集器具备类卸载的能力。
永久代的垃圾收集主要回收两部分内容:
- 废弃常量
- 无用的类(通过类加载机制,如加载后失效的插件)
如何判定无用类:
- 该类所有实例已经被回收。
- 加载该类的ClassLoader已经被回收(如OSGi、JSP、自己实现的插件)
- 该类对应的Class对象没有再任何地方被引用,无法再任何地方通过反射访问该类的方法。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法(如何回收?)
分代收集理论
当前商业虚拟机的垃圾收集,大多遵循了**“分代收集”的理论**进行设计,它建立在两个假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭
- 强分代假说:熬过越多垃圾收集过程的对象越难以消亡。
将堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
- 新生代:每次垃圾收集都有大批对象死去,少量存活,适合复制算法。
- 老年代:对象存活率高,没有额外空间进行分配担保,适合使用“标记-清理”或者“标记-整理”算法*。
标记清除算法(Mark-Sweep)
最基础的垃圾收集算法,主要不足有两个:
- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题 标记清除之后会产生大量不连续的空间碎片,碎片太多会导致jvm创建大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

复制算法
为解决效率问题,出现了此方法,代价是将内存缩小到了原来的一半。此方法一般用来回收新生代。
复制算法缺点:
- 对象存活率较高时会进行较多复制操作,效率变低
- 浪费50%空间
所以这种垃圾收集算法不适用老年代。

标记-整理算法(Mark-Compact)
算法简介:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
算法缺点:
- 需要移动对象,这是个极为负重的操作

经典垃圾收集器
HotSpot虚拟机的垃圾收集器
Serial收集器
Serial是最基本、发展历史最悠久的收集器,单线程的收集器,它进行垃圾收集时,必须暂停其他所有工作线程,直到它运行结束(Stop The World)。采用复制算法。
优点:在单核CPU下,Serial收集器没有线程交互的开销,比其他收集器更高效。
ParNew收集器
ParNew收集器是Serial收集器的多线程版本。
特点:
- 除了Serial收集器外,只有它能与CMS收集器配合工作
- 多核环境下效率比Serial高,单核下比不过。
- 默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾回收线程数。
- 当选择了CMS收集器后,ParNew是默认新生代收集器,也可以用-XX:+UserParNewGC强制指定。
垃圾收集中并行与并发
- 并行(Parallel):多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态。
- 并发(Concurrent):用户线程和垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
ParNew收集器运行示意图
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代垃圾收集器,也被称为“吞吐量优先”收集器。
其目标是达到一个可控制的吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- -XX:MaxGCPauseMillis参数:大于0的毫秒数,收集器将尽可能保证内存回收时间不超过设定值。
- -XX:GCTimeRatio参数:大于0小于100的整数,垃圾收集时间占总时间的比率,默认99
- -XX:+UseAdaptiveSizePollicy参数:打开这个参数后不需要手工制定新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:ServivorRatio)、晋升老年代对象年龄等细节了,虚拟机会根据系统运行情况动态调整这些参数。这种调节方式称为GC自适应的调节策略,这也是和ParNew收集器的重要区别。
Serial Old 收集器
Serial Old 收集器是Serial收集器的老年代版本,单线程,使用 标记-整理 算法。
两大用途如下:
- 在JDK1.5之前与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Serial/Serial Old 收集器运行示意图如下:

Parallel Old 收集器
Parallel Old 收集器 是Parallel Scavenge 收集器的老年代版本,使用多线程和 标记-整理 算法,JDK1.6之后提供。
在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge + Parallel Old 收集器。
Parallel Old/Parallel Scavenge 工作原理如下:

ps: java8 默认是用+UseParallelGC 即Parallel Scavenge + Parallel Old 作为垃圾回收器
CMS收集器
CMS(Concurrent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法(当full GC一定次数后会用标记整理算法)。运行步骤如下:
- 初始标记(CMS initial mark,需要Stop The World):仅标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记(CMS concurrent mark):进行GC Roots Tracking的过程。
- 重新标记(CMS remark,需要Stop The World):为了修正并发标记期间,因用户程序继续运转而导致标记产生变动的那一部分对象的标记记录。(时间比初始标记稍长,远比并发标记短)
- 并发清除(CMS concurrent sweep)
整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以和用户线程一起工作,总体上来说CMS收集器的内存回收过程和用户线程一起并发执行的。
CMS收集器运行示意图:

CMS收集器优点如下:
- 并发收集
- 低停顿
缺点如下:
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
- 基于标记-清除算法,产生大量空间碎片。
G1收集器
java9中 已经将G1作为默认垃圾收集器,CMS被标记为弃用。
G1目标就是为了替换掉CMS收集器,G1具备以下特点:
- 并行与并发:充分利用多CPU、多核环境下的硬件优势,部分其他垃圾收集器原本需要停顿java线程执行的GC动作,G1可以通过并发的方式让Java程序继续运行。
- 分代收集:能独立管理整个GC堆,不需要其他垃圾回收器配合
- 空间整合:G1整体上看是基于标记-整理算法实现的收集器,从局部(两个Region之间)看是基于复制算法实现的。
- 可预测的停顿:这是G1相比CMS的另一大优势。G1可以建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不超过N毫秒。
G1收集器运行示意图:
垃圾收集器总结

rawdata 用的垃圾回收器是CMS+ParNewGC :原因是当时G1还不成熟,java8才出来不久,项目比较急着上线,优先使用成熟的垃圾回收器方案,避免踩坑。
线上rawdata配置
JAVA_OPTS=”-server -Xms4g -Xmx4g -Xss256k -Xmn2g -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=68 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/gxb/log/gc.log -XX:GCLogFileSize=20M -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=20 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/gxb/dump -Djava.util.Arrays.useLegacyMergeSort=true”
理解GC日志

- 出现Full GC(System) 调用了System.gc()
- 出现Full 表示发生了STW,并不表示发生在新生代或者老年代
- DefNew “Default New Generation” Serial收集器 新生代
- ParNew “Parallel New Generation” ParNew收集器
- PSYoungGen Parallel Scavenge收集器
内存分配与回收策略
对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在Eden区上。
如果开启了本地线程分配缓冲,将按线程优先在TLAB上分配。
大对象,可能会直接在老年代中分配。
GC常见名词解释
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC(部分收集)
- Minor GC/Young GC 新生代收集 回收速度快
- Major GC/Old GC 老年代收集,回收速度比MinorGc慢10倍以上
- Mixed GC 混合收集,整个新生代和部分老年代,只有G1收集器有这种行为
- Full GC 整堆收集,收集整个Java堆和方法区
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。
新生代垃圾回收过程
- 将内存分为大块的Eden空间 和两块较小的 Survivor 空间,每次使用Eden和其中一块Survivor。
- 当垃圾回收时,将Eden和Survivor中还存活的对象,一次性复制到另一块Survivor,
- 最后清理掉Eden和使用过的Survivor空间。
- 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
大对象直接进入老年代
所谓大对象是指,需要连续内存空间的java对象,典型的大对象就是很长的字符串以及数组。避免Eden和两个Survivor区之间发生大量内存复制。
如何判断一个对象是大对象?
- -XX:PretenureSizeThreshold参数可以令内存大于这个值的对象直接在老年代分配,如果这个值设置为3m 必须写成3145728 不能直接写3m。(对ParallGC无效,只对Serial和ParNew有效)
- 对象size大于Eden区,比如Eden区大小为8MB,对象需要分配9MB的空间。
长期存活的对象将进入老年代
如果对象在Eden出生并经过第一次MinorGc后仍然存活,并且被Survivor容纳的话,将被移动到Survivor区,并且对象年龄设置为1,每熬过一次Minor GC 年龄就增加1岁。年龄到一定程度(默认15)会被晋升到老年代中。
-XX:MaxTenuringThreshold 参数可以用来设置对象到老年代的年龄阈值。
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需到指定年龄。
空间分配担保
进行Minor GC之前,虚拟机需要先检查老年代的空间是否足够,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间或者历次晋升的平均大小 就会进行MinorGC,否则进行FullGC。(JDK6 Update24之后)
1 | public class MinorGC { |
GC触发条件
最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
- young GC:当young gen中的eden区分配满的时候触发。
- full GC:
- 当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC)
- 要在perm gen(永久代)分配空间但已经没有足够空间时,也要触发一次full GC;
- System.gc()、heap dump带GC,默认也是触发full GC。
并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。
System.gc()详解
规范层面:**JVM规范没规定实现要使用怎样的自动内存管理;Java的标准库API里System.gc()的规定说它只是一个提示,不保证有什么作用或者何时起作用。**规定说:”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.” 注意“best effort”,其实意思是“尽力呗,不过什么都不干也行”。
实现层面:**HotSpot VM和很多其它JVM一样,其实默认是会在用户调用System.gc()的时候马上执行GC,并且等到GC完成才返回的。只有使用CMS或G1时,配置-XX:+ExplicitGCInvokesConcurrent,调用System.gc()才会在触发了并发GC后就返回。**其中CMS版跟G1版的行为还略微不一样,这里就不展开说了。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/38551124/answer/77215101
来源:知乎
R大是真的牛逼
性能调优
- 调整内存设置控制垃圾收集频率
- 选择合适的收集器降低延迟
- 排查代码问题导致频繁full GC
- 为避免内存扩展带来的性能浪费,-Xmx -Xms参数保持一致
- 使用参数-XX:DisableExplicitGC 屏蔽掉代码里的System.gc()
- java并发内存越大越好,堆越大(12G),导致Full GC时间过长。