在 jvm 调优中一个离不开的重点是垃圾回收, 当垃圾回收成为系统达到更高并发量的瓶颈时, 我们就需要对 jvm 中如果进行 "自动化" 垃圾回收技术实施必要的监控和调节.
对于调优之前, 我们必须要了解其运行原理, java 的垃圾收集 Garbage Collection 通常被称为 "GC", 它诞生于 1960 年 MIT 的 Lisp 语言, 经过半个多世纪, 目前已经十分成熟了. 因此本篇主要从这三个方面来了解:
1. 哪些对象需要被回收?
2. 什么时候回收?
3. 如何回收?
一, 谁要被回收
java 虚拟机在执行 java 程序的过程中会把它所管理的内存划分为若干个不同是数据区域, 这些区域有各自各自的用途. 主要包含以下几个部分组成:
1, 程序计数器
程序计数器占用的内存空间我们可以忽略不计, 它是每个线程所执行的字节码的行号指示器.
2, 虚拟机栈
java 的虚拟机栈是线程私有的, 生命周期和线程相同. 它描述的是方法执行的内存模型. 同时用于存储局部变量, 操作数栈, 动态链接, 方法出口等.
3, 本地方法栈
本地方法栈, 类似虚拟机栈, 它调用的是是 native 方法.
4, 堆
堆是 jvm 中管理内存中最大一块. 它是被共享, 存放对象实例. 也被称为 "gc 堆". 垃圾回收的主要管理区域
5, 方法区
方法区也是共享的内存区域. 它主要存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译器 (jit) 编译后的代码数据.
以上就是 jvm 在运行时期主要的内存组成, 我们看到常见的内存使用不但存在于堆中, 还会存在于其他区域, 虽然堆的管理对程序的管理至关重要, 但我们不能只局限于这一个区域, 特别是当出现内存泄露的时候, 我们除了要排查堆内存的情况, 还得考虑虚拟机栈的以及方法区域的情况.
知道了要对谁以及那些区域进行内存管理, 我还需要知道什么时候对这些区域进行垃圾回收.
二, 什么时候回收
在垃圾回收之前, 我们必须确定的一件事就是对象是否存活? 这就牵扯到了判断对象是否存活的算法了.
引用计数算法:
给对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器 + 1, 当引用失效, 计数器 - 1. 任何时刻计数器为 0 的对象就是不可能再被使用的.
优点: 实现简单, 判定效率高效, 被 actionscript3 和 python 中广泛应用.
缺点: 无法解决对象之间的相互引用问题. java 没有采纳
可达性分析算法:
通过一系列称为 "GC Roots" 的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链, 当一个对象到 GCRoots 没有任何引用链相连的时候, 则证明此对象是不可用的.
比如如下, 右侧的对象是到 GCRoot 时不可达的, 可以判定为可回收对象.
在 java 中, 可以作为 GCRoot 的对象包括以下几种:
* 虚拟机栈中引用的对象.
* 方法区中静态属性引用的对象.
* 方法区中常量引用的对象.
* 本地方法中 JNI 引用的对象.
基于以上, 我们可以知道, 当当前对象到 GCRoot 中不可达时候, 即会满足被垃圾回收的可能.
那么是不是这些对象就非死不可, 也不一定, 此时只能宣判它们存在于一种 "缓刑" 的阶段, 要真正的宣告一个对象死亡. 至少要经历两次标记:
第一次: 对象可达性分析之后, 发现没有与 GCRoots 相连接, 此时会被第一次标记并筛选.
第二次: 对象没有覆盖 finalize()方法, 或者 finalize()方法已经被虚拟机调用过, 此时会被认定为没必要执行.
三, 如何回收
上述的两点讲解之后, 我们大概明白了, 哪些对象会被回收, 以及回收的依据是什么, 但回收的这个工作实现起来并不简单, 首先它需要扫描所有的对象, 鉴别谁能够被回收, 其次在扫描期间需要 "stop the world" 对象能被冻结, 不然你刚扫描, 他的引用信息有变化, 你就等于白做了.
分代回收
我们从一个 object1 来说明其在分代垃圾回收算法中的回收轨迹.
1,object1 新建, 出生于新生代的 Eden 区域.
2,minor GC,object1 还存活, 移动到 Fromsuvivor 空间, 此时还在新生代.
3,minor GC,object1 仍然存活, 此时会通过复制算法, 将 object1 移动到 ToSuv 区域, 此时 object1 的年龄 age+1.
4,minor GC,object1 仍然存活, 此时 survivor 中和 object1 同龄的对象并没有达到 survivor 的一半, 所以此时通过复制算法, 将 fromSuv 和 Tosuv 区域进行互换, 存活的对象被移动到了 Tosuv.
5,minor GC,object1 仍然存活, 此时 survivor 中和 object1 同龄的对象已经达到 survivor 的一半以上(toSuv 的区域已经满了),object1 被移动到了老年代区域.
6,object1 存活一段时间后, 发现此时 object1 不可达 GcRoots, 而且此时老年代空间比率已经超过了阈值, 触发了 majorGC(也可以认为是 fullGC, 但具体需要垃圾收集器来联系), 此时 object1 被回收了. fullGC 会触发 stop the world.
在以上的新生代中, 我们有提到对象的 age, 对象存活于 survivor 状态下, 不会立即晋升为老生代对象, 以避免给老生代造成过大的影响, 它们必须要满足以下条件才可以晋升:
1,minor gc 之后, 存活于 survivor 区域的对象的 age 会 + 1, 当超过(默认)15 的时候, 转移到老年代.
2, 动态对象, 如果 survivor 空间中相同年龄所有的对象大小的综合和大于 survivor 空间的一半, 年级大于或等于该年级的对象就可以直接进入老年代.
以上采用分代垃圾收集的思想, 对一个对象从存活到死亡所经历的历程. 期间, 在新生代的时刻, 会用到复制算法, 在老年代时, 有可能会用到标记 - 清楚算法 (mark-sweep) 算法或者标记 - 整理算法, 这些都是垃圾回收算法基于不同区域的实现, 我们看下这几种回收算法的实现原理.
垃圾回收算法
标记清除法(Mark-Sweep)
标记清除法是垃圾回收算法的思想基础. 标记清除算法将垃圾分为两个阶段: 标记阶段和清除阶段.
标记阶段, 通过根节点, 标记所有从根节点开始的可达对象, 未标记过的对象就是未被引用的垃圾对象.
清除阶段, 清除所有未被标记的对象.
复制算法(Copying)
复制算法是, 将原有的内存空间分为两块, 每次只使用其中一块, 在垃圾回收时, 将正在适用的内存中存活对象复制到未使用的内存块, 然后清除使用的内存块中所有的对象.
标记压缩算法(Mark-Compact)
标记压缩算法是一种老年代的回收算法.
标记阶段和标记清除算法一致, 对可达对象做一次标记.
清理阶段, 为了避免内存碎片产生, 将所有的存活对象压缩到内存的一端.
四, 垃圾收集器
垃圾收集器是内存回收的具体实现, 不同的厂商提供的垃圾收集器有很大的差别, 一般的垃圾收集器都会作用于不同的分代, 需要搭配使用. 以下是各种垃圾收集器的组合方式:
如果说收集算法是内存回收的方法论, 垃圾收集器就是内存回收的具体实现
Serial 收集器
串行收集器是最古老, 最稳定以及效率高的收集器, 可能会产生较长的停顿, 只使用一个线程去回收. 新生代, 老年代使用串行回收; 新生代复制算法, 老年代标记 - 压缩; 垃圾收集的过程中会 Stop The World(服务暂停)
参数控制:-XX:+UseSerialGC 串行收集器
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本. 新生代并行, 老年代串行; 新生代复制算法, 老年代标记 - 压缩
参数控制:-XX:+UseParNewGC ParNew 收集器
-XX:ParallelGCThreads 限制线程数量
Parallel 收集器
Parallel Scavenge 收集器类似 ParNew 收集器, Parallel 收集器更关注系统的吞吐量. 可以通过参数来打开自适应调节策略, 虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量; 也可以通过参数控制 GC 的时间不大于多少毫秒或者比例; 新生代复制算法, 老年代标记 - 压缩
参数控制:-XX:+UseParallelGC 使用 Parallel 收集器 + 老年代串行
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多线程和 "标记 - 整理" 算法. 这个收集器是在 JDK 1.6 中才开始提供
参数控制: -XX:+UseParallelOldGC 使用 Parallel 收集器 + 老年代并行
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器. 目前很大一部分的 Java 应用都集中在互联网站或 B/S 系统的服务端上, 这类应用尤其重视服务的响应速度, 希望系统停顿时间最短, 以给用户带来较好的体验.
从名字 (包含 "Mark Sweep") 上就可以看出 CMS 收集器是基于 "标记 - 清除" 算法实现的, 它的运作过程相对于前面几种收集器来说要更复杂一些, 整个过程分为 4 个步骤, 包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记, 重新标记这两个步骤仍然需要 "Stop The World". 初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象, 速度很快, 并发标记阶段就是进行 GC Roots Tracing 的过程, 而重新标记阶段则是为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短.
由于整个过程中耗时最长的并发标记和并发清除过程中, 收集器线程都可以与用户线程一起工作, 所以总体上来说, CMS 收集器的内存回收过程是与用户线程一起并发地执行. 老年代收集器(新生代使用 ParNew)
优点: 并发收集, 低停顿
缺点: 产生大量空间碎片, 并发阶段会降低吞吐量
参数控制:-XX:+UseConcMarkSweepGC 使用 CMS 收集器
-XX:+ UseCMSCompactAtFullCollection Full GC 后, 进行一次碎片整理; 整理过程是独占的, 会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次 Full GC 后, 进行一次碎片整理
-XX:ParallelCMSThreads 设定 CMS 的线程数量(一般情况约等于可用 CPU 数量)
G1 收集器
G1 是目前技术发展的最前沿成果之一, HotSpot 开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器. 与 CMS 收集器相比 G1 收集器有以下特点:
1. 空间整合, G1 收集器采用标记整理算法, 不会产生内存空间碎片. 分配大对象时不会因为无法找到连续空间而提前触发下一次 GC.
2. 可预测停顿, 这是 G1 的另一大优势, 降低停顿时间是 G1 和 CMS 的共同关注点, 但 G1 除了追求低停顿外, 还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为 N 毫秒的时间片段内, 消耗在垃圾收集上的时间不得超过 N 毫秒, 这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了.
上面提到的垃圾收集器, 收集的范围都是整个新生代或者老年代, 而 G1 不再是这样. 使用 G1 收集器时, Java 堆的内存布局与其他收集器有很大差别, 它将整个 Java 堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔阂了, 它们都是一部分(可以不连续)Region 的集合.
G1 的新生代收集跟 ParNew 类似, 当新生代占用达到一定比例的时候, 开始出发收集. 和 CMS 类似, G1 收集器收集老年代对象会有短暂停顿.
收集步骤:
1, 标记阶段, 首先初始标记(Initial-Mark), 这个阶段是停顿的(Stop the World Event), 并且会触发一次普通 Mintor GC. 对应 GC log:GC pause (young) (inital-mark)
2,Root Region Scanning, 程序运行过程中会回收 survivor 区(存活到老年代), 这一过程必须在 young GC 之前完成.
3,Concurrent Marking, 在整个堆中进行并发标记(和应用程序并发执行), 此过程可能被 young GC 中断. 在并发标记阶段, 若发现区域对象中的所有对象都是垃圾, 那个这个区域会被立即回收(图中打 X). 同时, 并发标记过程中, 会计算每个区域的对象活性(区域中存活对象的比例).
4,Remark, 再标记, 会有短暂停顿(STW). 再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1 中采用了比 CMS 更快的初始快照算法: snapshot-at-the-beginning (SATB).
5,Copy/Clean up, 多线程清除失活对象, 会有 STW.G1 将回收区域的存活对象拷贝到新区域, 清除 Remember Sets, 并发清空回收区域并把它返回到空闲区域链表中.
6, 复制 / 清除过程后. 回收区域的活性对象已经被集中回收到深蓝色和深绿色区域.
各种组合的优缺点:
| 新生代 GC 策略 | 年老代 GC 策略 | 说明 |
组合 1 | Serial | Serial Old | Serial 和 Serial Old 都是单线程进行 GC,特点就是 GC 时暂停所有应用线程。 |
组合 2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发 GC,实现 GC 线程和应用线程并发工作,不需要暂停所有应用线程。另外,当 CMS 进行 GC 失败时,会自动使用 Serial Old 策略进行 GC。 |
组合 3 | ParNew | CMS | 使用 - XX:+UseParNewGC 选项来开启。ParNew 是 Serial 的并行版本,可以指定 GC 线程数,默认 GC 线程数为 CPU 的数量。可以使用 - XX:ParallelGCThreads 选项指定 GC 的线程数。 如果指定了选项 - XX:+UseConcMarkSweepGC 选项,则新生代默认使用 ParNew GC 策略。 |
组合 4 | ParNew | Serial Old | 使用 - XX:+UseParNewGC 选项来开启。新生代使用 ParNew GC 策略,年老代默认使用 Serial Old GC 策略。 |
组合 5 | Parallel Scavenge | Serial Old | Parallel Scavenge 策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC 时间),可见这会使得 CPU 的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 |
组合 6 | Parallel Scavenge | Parallel Old | Parallel Old 是 Serial Old 的并行版本
|
组合 7 | G1GC | G1GC | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC # 开启 -XX:MaxGCPauseMillis =50 # 暂停时间目标 -XX:GCPauseIntervalMillis =200 # 暂停间隔目标 -XX:+G1YoungGenSize=512m # 年轻代大小 -XX:SurvivorRatio=6 # 幸存区比例 |
以上优缺点来自: http://www.importnew.com/23752.html
来源: http://www.bubuko.com/infodetail-2988979.html