Java垃圾回收和内存分配策略

1 篇文章 0 订阅
1 篇文章 0 订阅

本文主要是总结了《深入理解Java虚拟机》、《码出高效》以及网上的一些资料。

概念

  • Full GC、Major GC、Minor GC
    在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。

针对 HotSpot VM的实现,它里面的GC其实准确分类有两种:

  • Partial GC(局部 GC): 并不收集整个 GC 堆的模式
    • Young GC: 只收集young gen的GC,Young GC还有种说法就叫做 "Minor GC"
    • Old GC: 只收集old gen的GC。只有垃圾收集器CMS的concurrent collection 是这个模式
    • Mixed GC: 收集整个young gen 以及部分old gen的GC。只有垃圾收集器 G1有这个模式
  • Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式

参考:JVM 系列文章之 Full GC 和 Minor GC

  • STW
    JVM在运行过程中,许多事件都可能造成Stop The World,常见的有GC、JIT、取消偏向锁(RevokeBias)、RedefineClasses(AOP)等等,其中GC停顿对应用程序的影响最大。参考: https://www.javatt.com/p/42495

JVM内存布局(虚拟机运行时数据区)

JVM内存布局如下图所示:
在这里插入图片描述

下面对各个区域的存放内容进行简单说明

区域存放内容异常子线程共享
堆区所有实例对象OOM
虚拟机栈运行环境,由栈帧组成,每个栈帧包括:局部变量表、操作栈、动态连接、方法返回地址SOF
元数据区类元信息、字段、静态属性、方法、常量
程序计数器存放执行指令的偏移量和信号指示器,用于线程执行和恢复
本地方法栈栈执行内部Java,该区域通过JNI执行Native方法

JKD8方法区(Perm区)中字符串常量移动到堆区,其他内容包括类元信息、字段、静态属性、方法、常量移动到元数据区。

内存申请过程

申请过程如下图所示:

  1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;当Eden空间足够时,内存申请结束。否则到下一步;
  2. JVM试图释放在Eden中所有不活跃的对象(YGC),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
  3. Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
  4. 当old区空间不够时,JVM会在old区进行FGC;
  5. 垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"OOM"错误;堆内存出现 OOM 的概率是所有内存耗尽 异常中最高的。出错时的堆内信息对解决问题非常有帮助,所以给 NM 设置运行参数 -XX:+HeapDumpOnOutOfMemoryError,让JVM遇到 OOM 异常时能输出堆内信息, 特别是对相隔数月才出现的 OOM 尤为重要。
    image22.png

对象存活判断

对象存活判断算法

引用计数算法

  • 基本思路:给每个对象添加一个计数器,当有一个地方引用它时计数+1,引用失效计数-1。当对象的计数为0是,则对象不可用。
  • 存在问题:很难解决对象之间互相引用的问题。

可达性分析算法

  • 基本思路:将 GC Roots 对象作为起始点,向下搜索,搜索走过的路径成为引用链;当一个对象到GC Roots没有任何引用链时,则该对象不可用。
  • 可作为GC Roots 的对象包括:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。(执行方法的过程中)
    • 方法区(JDK8成为元空间)中类静态属性或者常量引用的对象。
    • 本地方法栈中JNI引用的对象。

HotSpot可达性分析算法的实现

  1. GC Roots节点查找引用链:使用OopMap存放所有引用的存放信息,map的key为引用,value为引用的存放信息,包括栈和寄存器的位置。
  2. 安全点:在特定的位置记录了OopMap信息,并且只能在这些位置STW,这些位置称之为安全点。安全点包括:方法调用、循环跳转、异常跳转等“长时间执行”点。

Stop-The-World

  • 解释:可达性分析必须在一个能确保一致性的快照中进行,这里的一致性指的是整个分析期间,执行系统看起来就行被冻结在某个时间点上,引用关系不能在分析过程中变化。这个过程中,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。其中,所有Java代码停止,native代码可以执行,但不能和JVM交互。
  • 那个阶段发生:Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
  • 产生停顿的原因:当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。
  • 危害
    • 长时间服务停止,没有响应
    • 遇到HA系统,可能引起主备切换,严重危害生产环境。
    • 新生代的gc时间比较短,危害小。老年代的gc有时候时间短,但是有时候比较长几秒甚至100秒–几十分钟都有。
    • 一般情况下,堆越大花的时间越长。

引用

引用类型描述回收时机
强引用程序代码中普遍存在的引用。不回收。
软引用
还有用但非必需的对象。OOM前回收。
弱引用非必须的对象。下次GC回收。
虚引用不影响对象生存时间。不影响回收。

垃圾回收算法

算法说明优点缺点场景
复制
1. 两块区域from和to,一次只用一块
1. 用完后,from–>to区域,同时清空from
1. 默认eden:from:to=8:1:1

1. 无内存碎片
1. 运行高效

1. 浪费空间
1. 存活率高时,效率低
新生代
标记-清除
1. 标记所有需要回收对象
1. 回收

1. 效率不高
1. 内存碎片
老年代
标记-整理
1. 标记所有需要回收对象
1. 所有存活对象移向一端
老年代

说明:

  • IBM研究表明,98%的对象时“朝生夕死”的
  • 虚拟机的垃圾回收期都采用“分代收集”算法

垃圾回收器

如下图,有7种收集器,分为上下两部分,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。新生代的收集器使用复制算法,老年代使用并发标记清除(CMS)或标记-整理算法。
image.png

垃圾收集器对比:

收集器范围算法执行类型
Serial新生代复制单线程
ParNew新生代复制多线程并行
Parallel新生代复制多线程并行
Serial Old老年代标记整理单线程
CMS老年代标记清除多线程并发
Parallel Old老年代标记整理多线程
G1全部复制算法
标记整理
多线程

ParNew

如下图,可多线程收集垃圾image.png

  • 定义:ParNew收集器是JAVA虚拟机中垃圾收集器的一种。它是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器一致。

  • 应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

  • 设置参数:
    “-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
    "-XX:+UseParNewGC":强制指定使用ParNew;
    "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

  • ParNew收集器和Serial收集器的差异
    ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证能超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

  • 并行和并发的区别

    • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序继续运行,而垃圾收集程序运行于另一个CPU上。

[GC [ParNew 表示新生代使用的是ParNew收集器。
[GC [DefNew 表示新生代使用的是Serial收集器。

注意:

-Xms1G 最小JAVA虚拟机内存1G,是JVM的内存大小而不是堆区的大小;
-Xmx2G 最大JAVA虚拟机内存2G,是JVM的内存大小而不是堆区的大小;

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。服务端一般都希望获取最短停顿时间,所以采用CMS较多。如下图,包括4个步骤:

  • 初始标记
    标记GC Roots能直接关联到的的对象,时间短;

  • 并发标记
    从“初始标记”阶段标记的对象开始找出所有存活的对象;

  • 重新标记、
    用来处理前一个阶段因为引用关系改变,导致没有标记到的存活对象,时间短;

  • 并发清理
    清除那些没有标记的对象并且回收空间。image.png缺点:

  • 对CPU资源敏感:CMS默认启用的回收线程数是 (CPU数量+3)/4 。一般来讲,大于4核时,并发阶段垃圾回收占用25%,可接受;不足4个时,回收程序占用过多,导致用户程序执行速度大大降低。

  • 无法处理浮动垃圾、出现Concurrent Mode Failure。在并发清理阶段,用户程序还在执行,会产生新的垃圾(即浮动垃圾),无法被回收。默认情况下,老年代使用92%时,出发垃圾回收。如果浮动垃圾大于8%,就会出现Concurrent Mode Failure失败。这时虚拟机将启动后备方案:临时启用Serial Old收集器对老年代进行垃圾回收,这样停顿时间就长了。因此,老年代比例参数 -XX:CMSInitiatingOccupancyuFraction 设置太高容易造成该错误。

  • 空间碎片。可以通过参数 -XXCMSFullGCsBeforeCompaction 多少次后进行一次压缩的Full GC

G1

G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。过程如下图:
在这里插入图片描述
优点:

  • 并行和并发,如上图所示。
  • 只有逻辑上的分代概念,与物理上分代有本质区别。
  • 不需要其他收集器配合,独立管理整个GC堆
  • 空间整合:无内存碎片
  • 可预测的停顿:建立了可预测的停顿时间模型,可以指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。模型原理:跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小 以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First名称的由来)。

G1适用于以下几种应用:

  • 可以像CMS收集器一样,允许垃圾收集线程和应用线程并行执行,即需要额外的CPU资源;
  • 压缩空闲空间不会延长GC的暂停时间;
  • 需要更易预测的GC暂停时间;
  • 不需要实现很高的吞吐量

G1内存布局

  • Region:G1整个堆空间分成若干个大小相等的内存区域称为Regions。默认将整堆划分为2048个分区,可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂)。如下图,Regions分为:Eden Regions、Survivor Regions、、Old Regions;Humongous Regions四类。当一个对象大于Region大小的50%,称为巨型对象;它就会独占一个或多个Region,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区即Humongous Region。

image.png

  • Card:card为很小的内存区域,G1将Java堆划分为相等大小的一个个区域,这个小的区域大小是512 Byte,称为Card。Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card。Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了。分配对象会占用物理上连续若干个卡片。
  • TLAB:在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:在开始标记的时候生成一个快照图,标记存活对象;在并发标记的时候,在写前栅栏,记录所有引用被改变了的对象,再把这些对象都变成非白的;这样可能会生成游离的垃圾对象,没关系,将在下次收集周期被收集。
  • 分代回收:G1保留了分代的概念,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Regions(不需要连续)的集合,每个Region都可能随G1的运行在不同代之间切换。年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间。

整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。

G1工作解析

1.分配内存
  • 当jvm开始运行时,堆内存开始分区,分成若干个大小相等的Regions
  • 新的对象被分配到Eden Region,一个Eden Region满了之后,分配下一个Eden Region
  • 当所有的Eden Regions都满了,就进行一次Young GC

Old Region中的对象也会指向了Eden Region中的对象:例如 一个"old" Map 存放进了 一个 “new” 的对象。图中B指向了E,但是没有其他对象指向B了,显然B和E都是垃圾对象。Young GC只会清理Eden Region,发现E有引用指向它,也不会去回收E。
image.png

  • 进过Young GCEden之后,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区
2.标记对象

image.png

  • **Card Table:**Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card。Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了。所以Card Table其实就是映射着内存中的对象,Young GC的时候只需要扫描状态是dirty的card。
  • Remembered Set( RSet,key为其他分区地址,value为本分区对象地址):每一个Region都有自己的RSet,RSet里面记录了引用——就是其他Region中指向本Region中所有对象的所有引用,也就是谁引用了我的对象。RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址。通过RSet可以找到,其他分区(地址)对本分区对象(地址)引用情况。

比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录一对键值对,key是regionB的起始地址,Value的值能映射到B所在的Card的地址,所以要查找B对象,就可以通过RSet中记录的卡片来查找该对象
本分区对象引用本分区自己的对象,这种引用不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此young->old和young->young也不需要在RSet中记录。而对于old->young和old->old的跨代对象引用,需要拥有RSet

  • G1进行GC时,只要扫描本Region中RSet所记录的引用指向的对象是否存活,进而确定本分区内的对象存活情况。而不需要扫描整个堆了。

3.三色标记算法

并发标记中的三色标记算法,将对象分成三种类型

  • 黑色: 根对象,或者该对象与它的子对象都被扫描
  • 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
  • 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
三色标记过程
  1. 当GC开始扫描对象时,按照如下图步骤进行对象的扫描:根对象被置为黑色,往下扫描,扫描子对象,子对象被置为灰色,如下图

image.png

  • 继续由灰色对象往下扫描,扫描子对象,子对象被置为灰色,将已扫描完子对象的对象置为黑色

image.png

  • 遍历了所有可达的对象后,所有可达的对象都变成了黑色。绝对不可能有黑对象指向白对象的情况发生。白色对象需要被清理

image.png

G1的过程

G1收集器的收集活动主要有四种操作:新生代垃圾收集;后台收集、并发周期;混合式垃圾收集;必要时候的Full GC。

年轻代收集

年轻代收集,不会进行并发标记,所以它全程都是STW。应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。工作过程:

  • 根扫描 Root Scanning:静态和本地对象等被扫描
  • 更新已记忆集合 Update RSet:对dirty卡片的分区进行扫描,来更新RSet
  • RSet扫描:在收集当前CSet之前,扫描CSet分区的RSet,检测old->young这种引用情况
  • 转移和回收-Object Copy:讲CSet分区存活对象的转移到新survivor或old Region,回收CSet内垃圾对象
  • 引用处理:主要针对软引用、弱引用、虚引用、final引用、JNI引用;当占用时间过多时,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理

在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。回收前后的过程如下图所示:
image.png
image.png
image.png

年老代收集

当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集。过程如下:

  • 在年轻代收集之后或巨型对象分配之后,会去检查这个空间占比。
  • 年老代收集同时会执行年轻代收集,进行年老代的roots探测,即初始标记,stw
  • 然后恢复应用线程,进行年老代并发标记
  • 重新标记:stw
    • STAB处理
    • 引用处理
  • 清除垃圾:继续stw
  • 恢复应用线程
混合收集

Mixed GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中。假设一个Region的存活对象达到95%,而进行复制,效率很低,所以G1允许浪费部分内存,那么这个Region不会被混合收集,-XX:G1HeapWastePercent:默认5%。
**它的GC步骤分2步:**1.全局并发标记(global concurrent marking);2.拷贝存活对象(evacuation)。在G1 GC中,并发标记主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  1. 初始标记(initial mark,STW):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
  2. 根区域扫描(root region scan):G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
  3. 并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
  4. 最终标记(Remark,STW):该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
  5. 清除垃圾(Cleanup,STW):在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mixed GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
image.png
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
image.png

转移失败的担保机制 Full GC

在某些情况下,G1触发了Full GC,这时G1会退化到使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:

  1. 并发模式失败

G1启动标记周期,但在Mixed GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

  1. 晋升失败或者疏散失败

G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
A,增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
B,通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
C,也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

  1. 巨型对象分配失败

当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

G1参数调优

  • -XX:MaxGCPauseMillis (主要参数)

前面介绍过使用G1的最基本的参数:-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200。前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?

这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。
1.Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制Young GC的开销。2.Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

  • -XX:G1HeapRegionSize=n

设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。

  • -XX:ParallelGCThreads=n

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

  • -XX:ConcGCThreads=n

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

  • -XX:InitiatingHeapOccupancyPercent=45

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

GC 时间参数分析

GC日志中会详细的记录每一次GC事件所花费的时间信息,每一个GC事件所花费的时间都会以’user’、‘sys’、'real’3个维度来记录,这3个时间含义如下:

  1. real time:GC事件整个过程自然流逝的绝对时间,这个跟钟表上的时间是一致的。(ps:如果GC从8点开始,8点30结束,real time就是30分钟)。
  2. user time:cpu花在用户态的时间
  3. sys time:cpu花在内核态的时间,也就是说内核发生系统调用所花费的时间,不包括调用lib库的时间,因为这是发生在用户态。

一般的GC事件中,real time是小于sys+user time的,因为一般是多个线程并发的去做GC,所以real time是要小于sys+user time的。比如说:user+sys是2秒,如果是有5个GC线程并发的做垃圾回收,那么real time差不多是2000/5=400ms左右。

GC日志中real时间比user+sys时间长该如何处理?

有时候会见到real time大于sys+user time的情况,比如:

[Times: user=0.20 sys=0.01, real=18.45 secs]

如果在GC日中出现大量的这种日志,说明你的应用可能存在下列问题:IO负载非常重或者是CPU不够用。具体分析如下:

原因一:IO负载繁重

当服务器的IO负载非常重的时候(网络、磁盘访问、用户交互),real time就会变大。应用做GC日志打印的时候,也需要访问磁盘。当磁盘的负载非常重的时候,GC事件就有可能被阻塞,这会导致real time变长。

注意:就算不是你的应用导致的磁盘负载重,如果服务器上其他的应用导致的磁盘负载重也会导致real time变长。

可以用命令 sar -d -p 1 来监控服务器的磁盘负载情况。这个命令会输出每秒钟磁盘的读写数量,关于sar的详细使用请参考: Generate CPU, Memory and I/O report using SAR command。如果你发现服务器的磁盘负载非常重,那么可以考虑下面的方法来解决:
(1)如果是你的应用引起的,优化你的应用的io
(2)杀掉服务器上磁盘负载高的进程
(3)把应用部署到另一台磁盘负载不高的机器上

原因二:CPU不够用

如果服务器上跑了很多进程,你的应用很不幸没有得到足够的CPU时间,它就需要很多的等待。当你的进程在等待的时候,real time显然就比sys+user时间长了。可以用top之类的监控工具来监控服务器的cpu使用情况,如果cpu的利用率非常高,你的应用没有得到足够的cpu,那么你可以这样做:
(1)减少服务器上运行的进程,让你的应用可以获得足够的cpu。如果是自身应用进程导致的,一般还需要进一步分析应用线程。
(2)增加cpu数量,比如你是使用的云服务,可以更换更多cpu的主机。
(3)把应用部署到有足够cpu的服务器上。

FullGC

  1. fullGC一般是YGC耗时的10倍左右。

参数调优

运行参数

-Xms --jvm堆的最小值
-Xmx --jvm堆的最大值
-XX:MaxNewSize  --新生代最大值
-XX:MaxPermSize=1028m  --永久代最大值
-Xmn -- 新生代内存区域的大小
-XX:NewRatio 新生代和老年代的比例。比如:1:4,就是新生代占五分之一。
-XX:SurvivorRatio=8 --新生代内存区域中Eden和Survivor的比例,当前值表示2(2格S):8 ,就是一个Survivor区占十分之一。
-XX:+HeapDumpOnOutMemoryError  发生OOM时,导出堆的信息到文件。
-XX:+HeapDumpPath 表示,导出堆信息的文件路径。
-XX:OnOutOfMemoryError 当系统产生OOM时,执行一个指定的脚本,这个脚本可以是任意功能的。比如生成当前线程的dump文件,或者是发送邮件和重启系统。
-XX:PermSize -XX:MaxPermSize 设置永久区的内存大小和最大值。永久区内存用光也会导致OOM的发生。
-Xss 设置栈的大小。栈都是每个线程独有一个,所有一般都是几百k的大小。
  • -Xms256M -Xmx1024M,其中-X表示它是JVM运行参数,ms是memorystart的简称,mx是memorymax的简称,分别代表最小堆窑量和最大堆窑量。但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成样大小,避免在GC后调整堆大小时带来的额外压力。
  • -XX:MaxTenuringThreshold参数能配置计数器的值到达某个阐值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor区交换14次之后,晋升至老年代。
  • -XX:+HeapDumpOnOutOfMemoryError,让JVM遇到OOM异常时能输出堆内信息,特别是对相隔数月才出现的OOM异常尤为重要。
  • -Xmn(jdk1.4orlator)建议设为整个堆大小的1/3或者1/4,两个值设为一样大。用于设置年轻代大小。例如:-Xmn10m,设置新生代大小为10m。此处的大小是(eden+2survivorspace).与jmap-heap中显示的Newgen是(eden+1survivorspace)不同的。
  • -XX:SurvivorRatio用于设置Eden和其中一个Survivor的比值,默认比例为8(Eden):1(一个survivor),这个值也比较重要。
  • XX:MaxPermSize=1280m:Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多的类,经常出现致命错误。“Exceptioninthread’dubboclientx.xconnector’java.lang.OutO阳emoryE口or:PennGenspace。为了解决该问题,需要设定运行参数-XX:MaxPermSize=1280m,如果部署到新机器上,往往会因为NM参数没有修改导致故障再现。
  • cms的空间碎片。为了解决这个问题,CMS可以通过配置-XX:+UseCMSCompactAtFullCollection参数,强制JVM在FGC完成后对老年代进行压缩,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置-XX:+CMSFullGCsBeforeCompaction=n参数,在执行了n次FGC后,JVM再在老年代执行空间碎片整理。

日志参数

JVM的GC日志的主要参数包括如下几个:

-XX:+PrintGC 输出GC日志
-verbose:gc ---XX:+PrintGC别名
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息,这个打印的基本格式跟PrintGCDetails类似
-Xloggc:../logs/gc.log 日志文件的输出路径
-XX:+TraceClassLoading 监控类的加载
-XX:+PrintClassHistogram 跟踪参数。
  • -XX:+PrintGC 输出GC日志,格式如下
[Full GC 178K->99K(1984K), 0.0253877 secs]
  • -XX:+PrintGCDetails 输出GC的详细日志
–Heap
– def new generation   total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000)
–  eden space 12288K,  91% used [0x27e80000, 0x28975f20, 0x28a80000)
–  from space 1536K,   0% used [0x28a80000, 0x28a80000, 0x28c00000)
–  to   space 1536K,   0% used [0x28c00000, 0x28c00000, 0x28d80000)
– tenured generation   total 5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)
–   the space 5120K,   0% used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000)
– compacting perm gen  total 12288K, used 142K [0x34680000, 0x35280000, 0x38680000)
–   the space 12288K,   1% used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000)
–    ro space 10240K,  44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
–    rw space 12288K,  52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)

new generation 就是堆内存里面的新生代。total的意思就是一共的,所以后面跟的就是新生代一共的内存大小。used也就是使用了多少内存大小。0x开头的那三个分别代表的是 底边界,当前边界,高边界。也就是新生代这片内存的起始点,当前使用到的地方和最大的内存地点。
eden space 这个通常被翻译成伊甸园区,是在新生代里面的,一些创建的对象都会先被放进这里。后面那个12288K就表示伊甸园区一共的内存大小,91% used,很明显,表示已经使用了百分之多少。后面的那个0x跟上一行的解释一样。
from space 和to space 是幸存者的两个区。也是属于新生代的。他两个区的大小必须是一样的。因为新生代的GC采用的是复制算法,每次只会用到一个幸存区,当一个幸存区满了的时候,把还是活的对象复制到另个幸存区,上个直接清空。这样做就不会产生内存碎片了。
tenured generation 就表示老年代。
compacting perm 表示永久代。由于这两个的格式跟前面我介绍的那个几乎一样,就不必介绍了。

  • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式),格式如下:
289.556: [GC [PSYoungGen: 314113K->15937K(300928K)] 405513K->107901K(407680K), 0.0178568 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
293.271: [GC [PSYoungGen: 300865K->6577K(310720K)] 392829K->108873K(417472K), 0.0176464 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

289.556表示从jvm启动到发生垃圾回收所经历的的时间。GC表示这是新生代GC(Minor GC)。

  • -XX:+TraceClassLoading 监控类的加载。格式如下:
•[Loaded java.lang.Object from shared objects file]
•[Loaded java.io.Serializable from shared objects file]
•[Loaded java.lang.Comparable from shared objects file]
•[Loaded java.lang.CharSequence from shared objects file]
•[Loaded java.lang.String from shared objects file]
•[Loaded java.lang.reflect.GenericDeclaration from shared objects file]
•[Loaded java.lang.reflect.Type from shared objects file]
  • -XX:+PrintClassHistogram 跟踪参数。这个按下Ctrl+Break后,就会打印以下信息:
num     #instances         #bytes  class name
----------------------------------------------
   1:        890617      470266000  [B
   2:        890643       21375432  java.util.HashMap$Node
   3:        890608       14249728  java.lang.Long
   4:            13        8389712  [Ljava.util.HashMap$Node;
   5:          2062         371680  [C
   6:           463          41904  java.lang.Class

–分别显示:序号、实例数量、总大小、类型。
这里面那个类型,B和C的其实就是byte和char类型。

日志解读

关键词

  • DefNew:表示新生代使用Serial串行GC垃圾收集器,defNew提供新生代空间信息;
  • Tenured:提供年老代空间信息;
  • Perm :提供持久代空间信息;
  • Metaspace:元空间
  • Full GC:full gc

实例解释

代码

JVM 参数: -Xmx512m -Xms512m -XX:+UseSerialGC -XX:+PrintGCDetails -Xmn1m
Java代码:

public class StopWorldDemo {
    public static class MyThread extends Thread {
        HashMap<Long, byte[]> map = new HashMap<>();
        @Override
        public void run() {
            try {
                while (true) {
                    if (map.size() * 512 / 1024 / 1024 >= 450) {
                        //大于450M时,清理内存。
                        System.out.println("============准备清理==========:" + map.size());
                        map.clear();
                        System.out.println("clean map");
                    }
                    for (int i = 0; i < 100; i++) {
                        //消耗内存。
                        map.put(System.nanoTime(), new byte[512]);
                    }
                    Thread.sleep(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static class PrintThread extends Thread {
        public static final long START_TIME = System.currentTimeMillis();
        @Override
        public void run() {
            try {
                while (true) {
                    long t = System.currentTimeMillis() - START_TIME;
                    System.out.println("time:" + t);
                    Thread.sleep(3000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyThread t = new MyThread();
        PrintThread p = new PrintThread();
        t.start();
        p.start();
    }
}

日志分析

[GC (Allocation Failure) [DefNew: 896K->64K(960K), 0.0018636 secs] 896K->454K(524224K), 0.0018915 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 959K->63K(960K), 0.0019817 secs] 1350K->1314K(524224K), 0.0020026 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 959K->64K(960K), 0.0009841 secs] 2210K->2192K(524224K), 0.0010037 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
...
[GC (Allocation Failure) [DefNew: 959K->63K(960K), 0.0044617 secs] 521135K->521132K(524224K), 0.0045187 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 959K->64K(960K), 0.0048572 secs] 522028K->522026K(524224K), 0.0048875 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 960K->63K(960K), 0.0044425 secs] 522922K->522916K(524224K), 0.0044705 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 959K->959K(960K), 0.0000198 secs][Tenured: 522852K->523263K(523264K), 0.5906292 secs] 523812K->523808K(524224K), [Metaspace: 3182K->3182K(1056768K)], 0.5906972 secs] [Times: user=0.47 sys=0.09, real=0.59 secs] 
[Full GC (Allocation Failure) [Tenured: 523263K->523263K(523264K), 0.3959853 secs] 524223K->524221K(524224K), [Metaspace: 3182K->3182K(1056768K)], 0.3960172 secs] [Times: user=0.39 sys=0.01, real=0.40 secs] 
[Full GC (Allocation Failure) [Tenured: 523263K->523263K(523264K), 0.4043830 secs] 524223K->524222K(524224K), [Metaspace: 3182K->3182K(1056768K)], 0.4044080 secs] [Times: user=0.40 sys=0.00, real=0.40 secs] 
[Full GC (Allocation Failure) [Tenured: 523263K->515972K(523264K), 0.4705590 secs] 524222K->515972K(524224K), [Metaspace: 3182K->3182K(1056768K)], 0.4705814 secs] [Times: user=0.45 sys=0.00, real=0.47 secs] 

  1. Allocation Failure 表示GC的原因:年轻代空间不够,分配失败导致。
  2. 对于3.836 这是具体发生GC的时间点。这是时间戳是从jvm启动开始计算的,我们也可以用PrintGCDateStamps 来打印时间日期格式的时间。
  3. DefNew 是指GC发生的区域。
  4. 第一行:896K->64K(960K),这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大小。
  5. 第一行:896K->454K(524224K),这三个数字分别对应GC之前占用堆内存的总大小,GC之后堆内存占用,以及整个堆内存的大小。
  6. Times 是该时间点GC占用耗费时间。
  7. 第八行: Tenured: 522852K->523263K(523264K) 表示老年代的变化。 [Metaspace: 3182K->3182K(1056768K)] 表示元空间。
  8. 对象引用还在,导致老年代无法回收,最终OOM

可视化分析

使用https://gceasy.io/进行分析:
结果详见:GC Intelligence Report

  • JVM memory size
    | Generation | **Allocated ** | **Peak ** |
    | :— | :— | :— |
    | Young Generation | 960 kb | 960 kb |
    | Old Generation | 511 mb | 511 mb |
    | Meta Space | 1.01 gb | 3.13 mb |
    | Young + Old + Meta space | 1.51 gb | 515.07 mb |

  • GC Causes
    | Cause | Count | Avg Time | Max Time | Total Time | Time % |
    | :— | :— | :— | :— | :— | :— |
    | Allocation Failure | 9 | 612 ms | 510 ms | 5 sec 510 ms | 52.58% |
    | Full GC - Allocation Failure | 13 | 382 ms | 470 ms | 4 sec 970 ms | 47.42% |
    | Total | 22 | n/a | n/a | 10 sec 480 ms | 100.0% |

常见问题分析

concurrent mode failure

日志描述如下:

[GC 197.976: [ParNew: 260872K->260872K(261952K), 0.0000688 secs]197.976: [CMS197.981: [CMS-concurrent-sweep: 0.516/0.531 secs] (concurrent mode failure): 402978K->248977K(786432K), 2.3728734 secs] 663850K->248977K(1048384K), 2.3733725 secs]
  • 现象:这显示,ParNew收集请求执行,但是没有成功。因为此时系统估计没有老生代中没有足够的空间去容纳这些对象(预测之后可能会出现老生代的空余空间将会被系统占光),我们称这种情况为 “full promotion guarantee failure”。在这种情况下下,并发式的CMS被阻塞了,full GC执行了,GC算法进入了concurrent mode failure状态,调用一个serail Old GC(阻塞了其他线程)来清理系统的Heap。日志显示,Full GC花费了2.3733725s,老生代空间由402978K降到了248977K。
  • 解决方法:concurrent mode failure可以通过增大老生代的空间或者通过设置CMSInitiatingOccupancyFraction一个小的值使得CMS Collection发生的更频繁(CMSInitiatingOccupancyFraction可以控制CMS执行的时间,假设设置为70,说明老生代在利用率为70%时发生CMS),但是把这个值设置小也会导致CMS发生更加频繁。
  • concurrent mode failure产生的原理:CMS并发处理阶段用户线程还在运行中,伴随着程序运行会有新的垃圾产生,CMS无法处理掉它们(没有标记),只能在下一次GC的时候处理。同样的,用户线程运行就需要分配新的内存空间,为此,CMS收集器并不会在老年代全部被填满以后在进行收集,会预留一部分空间提供并发收集时的程序运行使用。即使是这样,还是会存在CMS运行期间预留的内存无法满足程序需求,就会出现"Concurrent Mode Failure"失败;这时,虚拟机将会启动备案操作:临时启动Serial Old 收集器来重新进行老年代的垃圾收集,Serial Old收集器会Stop the world,这样会导致停顿时间过长。
  • 其他场景:某些情况下,promotion failures也会发生,即使是老生代有足够的空间。这个原因是老生代的可用空间不是连续的,而将新生代的对象移动到老生代需要连续的可用空间。而CMS是不会对内存进行压缩的算法,因此造成了这种问题。

问题排查方法

java问题排查
Dump线程
Dump堆

GC问题集锦

其他

jvm内存与操作系统内存之间的关系jvm内存与操作系统内存之间的关系 https://blog.csdn.net/Jbinbin/article/details/85004909

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值