JVM相关整理(二)--垃圾回收器
一、垃圾回收对象
JVM垃圾回收主要针对三块内存区:新生代(Young Generation)、老年代(Old Generation)、
持久代(Permanent Generation)。
在Sun HotSpot JVM 中新生代+老年代 == 堆,持久代 == 方法区。
内存区模型如下:
新生代:分为 Eden区 和 Survivor区,Survivor区又分为大小相同的两部分:From Space 和 To Space。新建的对象在新生代分配内存,Eden区不足时,会把存活对象移到Suvivor区中,新生代大小可以通过 -Xmn 来控制,新生代中,Eden区 和 Suvivor区的比例可以通过 -XX:SurvivorRatio 来控制。
老年代:用于存放新生代中多次GC后仍然存活的对象,老年代占用大小为 -Xmx 值减去 -Xmn 对应的值。
持久代:在Sun HotSpot JVM中即方法区,某些JVM 没有持久代。主要存放常量及类的一些信息,默认最小值为16MB,最大值为64MB,可通过 -XX:PermSize 及 -XX:MaxPermSize 设置最小值和最大值。
二、确定垃圾对象
- 引用计数法
在JDK1.2之前,使用的是引用计数法。
当类被加载到内存之后,就会产生方法区、堆、栈、程序计数器等一系列信息,当创建对象时,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为0时,标志着该对象无引用,可回收。
存在问题:
ObjA.obj = ObjB;
ObjB.obj = ObjA;
循环引用时,引用计数不为0,导致对象无法被GC。
- 可达性分析
通过一系列的称为GC Roots的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链(Peference Chain),当一个对象到GC Roots没有任何引用链相连时,即对象不可达,也就说明此对象是不可用的,如图:
Object5、6、7虽然互有关联,但它们到GC Root是不可达的,因此也会被判定为可回收对象。
在Java 中,可作为GC Roots的对象包括:
- 方法区:类静态属性引用的对象、常量引用的对象;
- 虚拟机栈(本地变量表)中引用的对象;
- 本地方法栈JNI(Native方法)中引用的对象。
注: 在可达性分析算法中不可达对象,不会立即被JVM回收掉,因为要真正宣告一个对象死亡,至少要经历两次标记过程:第一次是在可达性分析后发现没有与GC Roots相连接的引用链,第二次是GC 对在F-Queue 执行队列中的对象进行的小规模标记(对象需要覆盖finalize() 方法且没被调用过)。
三、垃圾回收算法
参考:https://www.cnblogs.com/cielosun/p/6674431.html#24%E5%88%86%E4%BB%A3%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95generational-collection
- 标记-清除算法(Mark-Sweep)
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
- 复制算法(Coping)
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
- 标记-整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。
然后清除端边界外的对象。如图:
- 分代收集算法(Generational Com)
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。
另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老生代。当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space无法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中。
四、垃圾收集器
参考:
http://www.importnew.com/23035.html
新生代
- Serial
虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.
- ParNew
-XX: +UseConcMarkSweepGC
的默认新生代收集器).由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用
-XX:ParallelGCThreads
参数控制GC线程数).- Parallel Scavenge
与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:
停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:
Parallel Scavenge参数 | 描述 |
---|---|
MaxGCPauseMillis |
(毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加. |
GCTimeRatio |
(整数:0 < GCTimeRatio < 100 ) 是垃圾收集时间占总时间的比率 |
-XX:+UseAdaptiveSizePolicy |
启用GC自适应的调节策略: 不再需要手工指定-Xmn 、-XX:SurvivorRatio 、-XX:PretenureSizeThreshold 等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量 |
老年代
- Serial Old
Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:
- Serial Old应用场景如下:
- JDK 1.5之前与Parallel Scavenge收集器搭配使用;
- 作为CMS收集器的后备预案, 在并发收集发生
Concurrent Mode Failure
时启用(见下:CMS收集器).
- Parallel Old
- CMS
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店).
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤:
1. 初始标记(CMS initial mark)
2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
3. 重新标记(CMS remark)
4. 并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩)
其中两个加粗的步骤(初始标记、重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间.
(由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的.
由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿的优势, 但它也并非十分完美, 其存在缺点及解决策略如下:
-
CMS默认启动的回收线程数=(CPU数目+3)4
当CPU数>4时, GC线程最多占用不超过
25%
的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低. - 无法处理浮动垃圾, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了
-XX:CMSInitiatingOccupancyFraction
参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly
来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了). - 最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了
-XX:+UseCMSCompactAtFullCollection
开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).
- G1
G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗CPU的服务器治理大内存.
- G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
--XX:+UseG1GC
启用G1收集器.
与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.
每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.
新生代收集
G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.
- G1的新生代收集特点如下:
- 一整块堆内存被分为多个Regions.
- 存活对象被拷贝到新的Survivor区或老年代.
- 年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.
- Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停.
- 多线程并发GC.
老年代收集
G1老年代GC会执行以下阶段:
注: 一下有些阶段也是年轻代垃圾收集的一部分.
index | Phase | Description |
---|---|---|
(1) | 初始标记 (Initial Mark: Stop the World Event) | 在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions. |
(2) | 扫描根区域 (Root Region Scanning: 与应用程序并发执行) | 扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完. |
(3) | 并发标记 (Concurrent Marking : 与应用程序并发执行) | 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断. |
(4) | 重新标记 (Remark : Stop the World Event) | 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度). |
(5) | 清理 (Cleanup : Stop the World Event and Concurrent) | 见下 5-1、2、3 |
5-1 (Stop the world) | 在含有存活对象和完全空闲的区域上进行统计 | |
5-2 (Stop the world) | 擦除Remembered Sets. | |
5-3 (Concurrent) | 重置空regions并将他们返还给空闲列表(free list) | |
(*) | Copying/Cleanup (Stop the World Event) | 选择”活跃度”最低的区域(这些区域可以最快的完成回收). 拷贝/转移存活的对象到新的尚未使用的regions. 该阶段会被记录在gc-log内(只发生年轻代[GC pause (young)] , 与老年代一起执行则被记录为[GC Pause (mixed)] . |
- G1老年代GC特点如下:
- 并发标记阶段(index 3)
- 在与应用程序并发执行的过程中会计算活跃度信息.
- 这些活跃度信息标识出那些regions最适合在STW期间回收(which regions will be best to reclaim during an evacuation pause).
- 不像CMS有清理阶段.
- 再次标记阶段(index 4)
- 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.
- 空region直接被回收.
- 拷贝/清理阶段(Copying/Cleanup Phase)
- 年轻代与老年代同时回收.
- 老年代内存回收会基于他的活跃度信息.
- 并发标记阶段(index 3)
补充: 关于Remembered Set
G1收集器中, Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免扫描全堆. G1中每个Region都有一个与之对应的Remembered Set, VM发现程序对Reference类型数据进行写操作时, 会产生一个Write Barrier暂时中断写操作, 检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中. 当内存回收时, 在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏.
JVM小工具
在${JAVA_HOME}/bin/目录下Sun/Oracle给我们提供了一些处理应用程序性能问题、定位故障的工具, 包含
bin | 描述 | 功能 |
---|---|---|
jps | 打印Hotspot VM进程 | VMID、JVM参数、main() 函数参数、主类名/Jar路径 |
jstat | 查看Hotspot VM 运行时信息 | 类加载、内存、GC[可分代查看]、JIT编译 |
jinfo | 查看和修改虚拟机各项配置 | -flag name=value |
jmap | heapdump: 生成VM堆转储快照、查询finalize执行队列、Java堆和永久代详细信息 | jmap -dump:live,format=b,file=heap.bin [VMID] |
jstack | 查看VM当前时刻的线程快照: 当前VM内每一条线程正在执行的方法堆栈集合 |
Thread.getAllStackTraces() 提供了类似的功能 |
javap | 查看经javac之后产生的JVM字节码代码 | 自动解析.class 文件, 避免了去理解class文件格式以及手动解析class文件内容 |
jcmd | 一个多功能工具, 可以用来导出堆, 查看Java进程、导出线程信息、 执行GC、查看性能相关数据等 | 几乎集合了jps、jstat、jinfo、jmap、jstack所有功能 |
jconsole | 基于JMX的可视化监视、管理工具 | 可以查看内存、线程、类、CPU信息, 以及对JMX MBean进行管理 |
jvisualvm | JDK中最强大运行监视和故障处理工具 | 可以监控内存泄露、跟踪垃圾回收、执行时内存分析、CPU分析、线程分析… |
JVM常用参数整理
参数 | 描述 |
---|---|
-Xms |
最小堆大小 |
-Xmx |
最大堆大小 |
-Xmn |
新生代大小 |
-XX:PermSize |
永久代大小 |
-XX:MaxPermSize |
永久代最大大小 |
-XX:+PrintGC |
输出GC日志 |
-verbose:gc |
- |
-XX:+PrintGCDetails |
输出GC的详细日志 |
-XX:+PrintGCTimeStamps |
输出GC时间戳(以基准时间的形式) |
-XX:+PrintHeapAtGC |
在进行GC的前后打印出堆的信息 |
-Xloggc:/path/gc.log |
日志文件的输出路径 |
-XX:+PrintGCApplicationStoppedTime |
打印由GC产生的停顿时间 |