1. 如何确定垃圾
1.1. 引用计数法
- java中如果操作对象,必须获取对象的引用。
- 为一个对象添加引用时,引用计数+1
- 为对象删除一个引用时,引用计数-1
- 如果一个对象引用计数为0,说明没有被引用,可以回收
- 如果两个对象相互引用,容易产生循环引用的问题
1.2. 可达性分析
- 首先定义一些GC Root对象,作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
- 那么那些点可以作为GC Roots呢?一般来说,如下情况的对象可以作为GC Roots:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(Native方法)的引用的对象
- 由系统类加载器(system class loader)加载的对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是root。
2. java中常用的垃圾回收算法
2.1. 标记清除算法
- 标记阶段
- 清除阶段
在清理对象所占用的内存空间后并没有整理可用的内存空间,会引起内存碎片化的问题,继而引起大对象无法获得连续可用内存空间。
2.2. 复制算法
- 首先将内存划分为两块大小相等的内存区域,新生代的对象都被存放在区域1。
- 在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对全部复制到区域2,这时区域1不存在任何存活的对象,直接清理整个区域1。
内存清理效率高易于实现,但同一时刻只有一半内存区域可用,大量浪费内存。如果内存中有大量长时间存在的对象,来回复制会影响效率。
适合朝生夕死的对象。
2.3. 标记整理算法
- 标记阶段,标记后将存活的对象移到内存的另一端
- 清除阶段
2.4. 分代收集算法
针对不同的对象类型,jvm采用不同的垃圾回收算法,称为分代收集算法。
- 新生代:
- 存放新生成的对象
- 特点:对象数量多,生命周期短
- 目前,新生代采用复制算法,原因:
- 新生代每次有大量的对象被回收
- 需要复制的对象少,不存在大量的对象在内存中被来回复制的现象
- 分区:
- Eden区
- 新创建的对象首先放在这里,如果是大对象直接分配到老年代
- Eden区内存不足时会触发MinorGC
- SurvivorTo区
- 保留上一次MinorGC时的幸存者
- SurvivorFrom区
- 将上一次MinorGC时的幸存者作为MinorGC的被扫描者
- 新生代GC叫做MinorGC,采用 复制算法 实现,Eden区和SurvivorFrom区内存不足触发。
- 把Eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区。
- 如果符合老年代的标准,则复制到老年代(年龄达到15),同时把这些对象的年龄+1。
- 如果SurvivorTo的内存空间不够,则直接复制到老年代
- 如果属于大对象(-XX:PretenureSizeThreshold),则直接复制到老年代
- 清空Eden区和SurvivorFrom区的对象
- 将SurvivorTo区SurvivorFrom区互换,原来的SurvivorTo区成为下一次SurvivorFrom区
- 老年代
- 存放大对象和生命周期长的对象
- 目前,老年代采用标记清除算法
- 永久代
- 存储Class类、常量、方法描述
- 主要回收废弃的常量和无用的类
3.HotSpot垃圾收集器
新生代垃圾收集器
Serial 垃圾收集器(单线程)
只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)。
一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿。因此 Serial 垃圾收集器适合客户端使用。
由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高效。
ParNew 垃圾收集器(多线程)
ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。
Parallel Scavenge 垃圾收集器(多线程)
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:
- Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
- ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。
- 通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。
- 通过参数 -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。
- 通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄,以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。
老年代垃圾收集器
Serial Old 垃圾收集器(单线程)
Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。
Parallel Old 垃圾收集器(多线程)
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 垃圾收集器
CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的缺点:
- 吞吐量低
- 无法处理浮动垃圾,导致频繁 Full GC
- 使用“标记-清除”算法产生碎片空间
对于产生碎片空间的问题,可以通过开启 -XX:+UseCMSCompactAtFullCollection,在每次 Full GC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction 告诉 CMS,经过了 N 次 Full GC 之后再进行一次内存整理。
G1 通用垃圾收集器
G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
这里抛个问题 ????
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
- 最终标记:Stop The World,使用多条标记线程并发执行。
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。
触发条件
GC触发条件
- 各种Young GC的触发原因都是eden区满了;
- Serial Old GC/PS MarkSweep GC/Parallel Old GC的触发则是在要执行Young GC时候预测其promote的object的总size超过老生代剩余size;
- CMS GC的initial marking的触发条件是老生代使用比率超过某值;
- G1 GC的initial marking的触发条件是Heap使用比率超过某值;
- Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显,就是3 和 4 的fancy算法不赶趟了,只能全局范围大搞一次GC;
- PS MarkSweep GC/Parallel Old GC(Full GC)之前会跑一次Parallel Young GC;
Full GC
- System.gc() 方法的调用 此方法的调用是建议 JVM 进行 Full GC,注意这只是建议而非一定,但在很多情况下它会触发 Full GC,从而增加 Full GC 的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc()。
- 老年代空间不足 老年代空间不足会触发 Full GC 操作,若进行该操作后空间依然不足,则会抛出如下错误:java.lang.OutOfMemoryError: Java heap space
- 永久代空间不足 JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代(Permanet Generation),存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
- CMS GC 时出现 promotion failed 和 concurrent mode failure promotion failed,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
- 统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间。
空间分配担保
JDK 6 Update 24 之前的规则是这样的:
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立,Minor GC 可以确保是安全的; 如果不成立,则虚拟机会查看 HandlePromotionFailure 值是否设置为允许担保失败, 如果是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的; 如果小于,或者 HandlePromotionFailure 设置不允许冒险,那此时也要改为进行一次 Full GC。
JDK 6 Update 24 之后的规则变为:
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。
这个过程就是分配担保。
4.JVM参数
先来看看堆区以及各个分代设置大小的参数:
参数 |
作用 |
说明 |
-Xms |
初始堆大小 |
|
-Xmx |
最大堆大小 |
|
-Xmn |
年轻代大小 |
|
-XX:SurvivorRatio |
Eden区与Survivor区(也就是咱们上文说的From 与To)的大小比值 |
默认值是8:1:1 |
-XX:MaxTenuringThreshold |
就是躲过几次YoungGC后会被分到老年代 |
默认是15 ,也就是在Survivor 区来回挪腾了15次会被送到老年代去 |
再来看下关于JVM虚拟机栈的一些参数:
参数 |
作用 |
-Xss |
栈空间大小,这个一般我们会设置为1m |
再来看下关于永久代的参数设置:
参数 |
作用 |
-XX:PermSize |
永久代初始大小 |
-XX:MaxPermSize |
永久代最大大小 |
然后我们再来,了解下CMS一些参数的设置,毕竟我们时候CMS回收器,它有自己的一些特殊参数:
参数 |
作用 |
-XX:+UseConcMarkSweepGC |
表示使用CMS回收器 |
-XX+UseCMSCompactAtFullCollection |
FullGC过后,开启对老年代的内存压缩,我们知道CMS使用的标记清除算法,会产生内存碎片,所以需要内存压缩 |
-XX:CMSFullGCsBeforeCompaction |
经过几次FullGC后进行内存压缩,这个总不能每次FullGC后都要内存压缩一次,这样就太费资源了 |
最后我们再来补充几个关于GC日志的参数,因为我们要观察系统的GC情况就需要查看GC日志:
参数 |
作用 |
-XX:+PrintGC |
开启打印GC日志 |
-XX:+PrintGCDetails |
打印详细的GC日志 |
-XX:+PrintGCTimeStamps |
打印GC时间 |
-Xloggc:filename |
这就是把GC日志输出到文件中 ,这个filename就是文件 |
持续不断的Full GC
- 先下线机器
- jinfo -flag +HeapDumpBeforeFullGC 等待dump结束
- 重启机器,上线机器
- 分析dump