JVM内存之GC

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM内存之GC相关的知识,希望对你有一定的参考价值。

技术分享图片
技术分享图片

1、JVM内存划分为堆内存和非堆内存

2、堆内存用途:存放对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

3、非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

元数据: calss的文本,路径等
类属性: static属性
类方法;

  • 在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

4、JDK1.8为什么要废弃永久代?

移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题;

5、移除永久代的影响?

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。

元空间有注意有两个参数:

MetaspaceSize :初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

分代GC

对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。

1、年轻代(Young Generation), 对应的GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

2、年老代(Old Generation),对象如果在年轻代存活了足够长的时间而没有被清理掉,则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC;

如果对象比较大(比如长字符串或大数组,集合),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

 

3、Major GC就是Full GC吗?

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
Partial GC:并不收集整个GC堆的模式

Young GC:只收集young gen的GC
Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

当有人说“major GC”的时候一定要问清楚他想要指的是上面的Full GC还是Old GC;

最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:

Young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
Full GC:1、当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(常见于CMS); 2、如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发Full GC。

GC算法

1、标记-清除算法
技术分享图片
如图所示,当进行过标记清除算法之后,出现了大量的非连续内存。当java堆需要分配一段连续的内存给一个新对象时,发现虽然内存清理出了很多的空闲,但是仍然需要继续清理以满足“连续空间”的要求。所以说,这种方法比较基础,效率也比较低下。

2、标记-复制算法
为了解决效率与内存碎片问题

技术分享图片
从图中可以看出,整理后的内存十分规整,但是白白浪费一般的内存成本太高。然而这其实是很重要的一个收集算法,因为现在的商业虚拟机都采用这种算法来回收新生代。根据这个算法衍生出现在主流的JVM 新生代内存模型:
技术分享图片

3、标记-整理算法
复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进行分配担保。所以在老年代中这种情况一般是不适合的。
技术分享图片
与标记-清除算法一样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。

STW

stop - the -world
是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

显然上面的三类gc算法,老年代的标记 - 整理算法会触发STW ;

垃圾收集器

串行收集器(Serial)

单线程。收集时,必须暂停应用的工作线程,直到收集结束。

并行收集器(Parallel)
适用于新生代 ;

多条垃圾收集线程并行工作,在多核CPU下效率更高,收集线程工作时应用线程仍然处于等待状态

CMS收集器(Concurrent Mark Sweep)
适用于老年代; “Mostly Concurrent Mark and Sweep Garbage Collector”;
CMS收集器是缩短暂停应用时间为目标而设计的,是基于标记-清除算法实现,整个过程分为4个步骤,包括:

初始标记(Initial Mark)
并发标记(Concurrent Mark)
重新标记(Remark)
并发清除(Concurrent Sweep)

其中,初始标记、重新标记这两个步骤仍然需要暂停应用线程。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间段小。
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。

GC收集分析

引用计数法和可达性分析算法

引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就+1,;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能在被使用。
(1)、优点

判定效率很高

(2)、缺点

不会完全准确,因为如果出现两个对象相互引用的问题就不行了
技术分享图片
很明显,到最后两个实例都不再用了(都等于null了),但是GC却无法回收,因为引用数不是0,而是1,这就造成了内存泄漏。也很明显,现在虚拟机都不采用此方式。
分析上述代码
技术分享图片

GC ROOT

一个对象可以属于多个root,GC root有几下种:

Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
Thread - 活着的线程
Stack Local - Java方法的local变量或参数
JNI Local - JNI方法的local变量或参数
JNI Global - 全局JNI引用
Monitor Used - 用于同步的监控对象
Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。

GC 策略

垃圾回收器的可用组合:
技术分享图片
标红组合为现在主流的gc策略;

GC日志分析:
1、Minor GC
技术分享图片

2016-08-23T02:23:07.219-0200 – GC发生的时间;
64.322 – GC开始,相对JVM启动的相对时间,单位是秒;
GC – 区别MinorGC和FullGC的标识,这次代表的是MinorGC;
Allocation Failure – MinorGC的原因,在这个case里边,由于年轻代不满足申请的空间,因此触发了MinorGC;
ParNew – 收集器的名称,它预示了年轻代使用一个并行的 mark-copy stop-the-world 垃圾收集器;
613404K->68068K – 收集前后年轻代的使用情况;
(613440K) – 整个年轻代的容量;
0.1020465 secs – 这个解释用原滋原味的解释:Duration for the collection w/o final cleanup.
10885349K->10880154K – 收集前后整个堆的使用情况;
(12514816K) – 整个堆的容量;
0.1021309 secs – ParNew收集器标记和复制年轻代活着的对象所花费的时间(包括和老年代通信的开销、对象晋升到老年代时间、垃圾收集周期结束一些最后的清理对象等的花销);
[Times: user=0.78 sys=0.01, real=0.11 secs] – GC事件在不同维度的耗时,具体的用英文解释起来更加合理:
user – Total CPU time that was consumed by Garbage Collector threads during this collection
sys – Time spent in OS calls or waiting for system event
real – Clock time for which your application was stopped. With Parallel GC this number should be close to (user time + system time) divided by the number of threads used by the Garbage Collector. In this particular case 8 threads were used. Note that due to some activities not being parallelizable, it always exceeds the ratio by a certain amount.

2、晋升分析
开始的时候:整个堆的大小是 10885349K,年轻代大小是613404K,这说明老年代大小是 10885349-613404=10271945K,

收集完成之后:整个堆的大小是 10880154K,年轻代大小是68068K,这说明老年代大小是 10880154-68068=10812086K,

老年代的大小增加了:10812086-10271945=608209K,也就是说 年轻代到年老代promot了608209K的数据;

图形分析:
技术分享图片

3、Full/Major GC
技术分享图片
Phase 1: Initial Mark
这是CMS中两次stop-the-world事件中的第一次。它有两个目标:一是标记老年代中所有的GC Roots;二是标记被年轻代中活着的对象引用的对象。
标记结果如下:
技术分享图片

技术分享图片

016-08-23T11:23:07.321-0200: 64.42 – GC事件开始,包括时钟时间和相对JVM启动时候的相对时间,下边所有的阶段改时间的含义相同;
CMS Initial Mark – 收集阶段,开始收集所有的GC Roots和直接引用到的对象;
10812086K – 当前老年代使用情况;
(11901376K) – 老年代可用容量;
10887844K – 当前整个堆的使用情况;
(12514816K) – 整个堆的容量;
0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] – 时间计量;

Phase 2: Concurrent Mark
这个阶段会遍历整个老年代并且标记所有存活的对象,从“初始化标记”阶段找到的GC Roots开始。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
2016-08-23T11:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2016-08-23T11:23:07.357-0200: 64.460: [CMS-concurrent-mark: 035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
CMS-concurrent-mark – 并发收集阶段,这个阶段会遍历整个年老代并且标记活着的对象;
035/0.035 secs – 展示该阶段持续的时间和时钟时间;
[Times: user=0.07 sys=0.00, real=0.03 secs] – 同上

Phase 3: Concurrent Preclean(预洗)
这个阶段又是一个并发阶段,和应用线程并行运行,不会中断他们。前一个阶段在并行运行的时候,一些对象的引用已经发生了变化,当这些引用发生变化的时候,JVM会标记堆的这个区域为Dirty Card(包含被标记但是改变了的对象,被认为"dirty"),这就是 Card Marking。
技术分享图片
在pre-clean阶段,那些能够从dirty card对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了,如下:
技术分享图片
Phase 4: Concurrent Abortable(可中止) Preclean
又一个并发阶段不会停止应用程序线程。这个阶段尝试着去承担STW的Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。

Phase 5: Final Remark
这个阶段是CMS中第二个并且是最后一个STW的阶段。该阶段的任务是完成标记整个年老代的所有的存活对象。由于之前的预处理是并发的,它可能跟不上应用程序改变的速度,这个时候,STW是非常需要的来完成这个严酷考验的阶段。

通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候,目的是消除紧接着的连续的几个STW阶段。
技术分享图片

2016-08-23T11:23:08.447-0200: 65.550 – 同上;
CMS Final Remark – 收集阶段,这个阶段会标记老年代全部的存活对象,包括那些在并发标记阶段更改的或者新创建的引用对象;
YG occupancy: 387920 K (613440 K) – 年轻代当前占用情况和容量;
[Rescan (parallel) , 0.0085125 secs] – 这个阶段在应用停止的阶段完成存活对象的标记工作;
weak refs processing, 0.0000243 secs]65.559 – 第一个子阶段,随着这个阶段的进行处理弱引用;
class unloading, 0.0013120 secs]65.560 – 第二个子阶段(that is unloading the unused classes, with the duration and timestamp of the phase);
scrub string table, 0.0001759 secs – 最后一个子阶段(that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively)
10812086K(11901376K) – 在这个阶段之后老年代占有的内存大小和老年代的容量;
11200006K(12514816K) – 在这个阶段之后整个堆的内存大小和整个堆的容量;
0.0110730 secs – 这个阶段的持续时间;
[Times: user=0.06 sys=0.00, real=0.01 secs] – 同上;

Phase 6: Concurrent Sweep
和应用线程同时进行,不需要STW。这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。
技术分享图片
Phase 7: Concurrent Reset
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

JVM内存参数配置

参数名称 含义 默认值 解释
-Xms 初始堆大小 物理内存的1/64 空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制(MinHeapFreeRatio参数可以调整)
-Xmx 最大堆大小 物理内存的1/4 空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制(MaxHeapFreeRatio参数可以调整)
-Xmn 年轻代大小 eden+ 2*(survivor space),整个堆大小=年轻代大小 + 年老代大小 + 持久代大小
-XX:PermSize 设置持久代 物理内存的1/64 jdk8 无效
-XX:MaxPermSize 设置持久代 物理内存的1/4 jdk8 无效
-Xss 线程的栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K,在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
-XX:NewRatio 年轻代与年老代的比值 4 4表示年轻代与年老代所占比值为1:4,Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio Eden区与Survivor区的大小比值 8 8,则一个Eden区与两一个Survivor区与的比值为8:1,一个Survivor区占整个年轻代的1/10
-XX:MaxTenuringThreshold 对象最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.

CMS 配置

参数名称 含义 默认值 解释
-XX:+UseConcMarkSweepGC 使用CMS内存收集 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.

并行收集器配置

参数名称 含义 默认值 解释
-XX:+UseParallelGC 选择并行收集器.此配置仅对年轻代有效.
-XX:+UseParNewGC 设置年轻代为并行收集 可与CMS收集同时使用
JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值

GC 日志配置

参数名称 含义 默认值 解释
-XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs][Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

CATALINA_OPTS="-server"
CATALINA_OPTS="${CATALINA_OPTS} -Xms5184m -Xmx5184m"
CATALINA_OPTS="${CATALINA_OPTS} -XX:PermSize=512m -XX:MaxPermSize=512m"
CATALINA_OPTS="${CATALINA_OPTS} -Xmn2g"
CATALINA_OPTS="${CATALINA_OPTS} -XX:MaxDirectMemorySize=1g"
CATALINA_OPTS="${CATALINA_OPTS} -XX:SurvivorRatio=10"
CATALINA_OPTS="${CATALINA_OPTS} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5000"
CATALINA_OPTS="${CATALINA_OPTS} -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly"
CATALINA_OPTS="${CATALINA_OPTS} -XX:+ExplicitGCInvokesConcurrent -Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000"
CATALINA_OPTS="${CATALINA_OPTS} -XX:ParallelGCThreads=${CPU_COUNT}"
CATALINA_OPTS="${CATALINA_OPTS} -Xloggc:${MI_LOGS}/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
CATALINA_OPTS="${CATALINA_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${MI_LOGS}/java.hprof"
CATALINA_OPTS="${CATALINA_OPTS} -Djava.awt.headless=true"
CATALINA_OPTS="${CATALINA_OPTS} -Dsun.net.client.defaultConnectTimeout=10000"
CATALINA_OPTS="${CATALINA_OPTS} -Dsun.net.client.defaultReadTimeout=30000"
CATALINA_OPTS="${CATALINA_OPTS} -DJM.LOG.PATH=${MI_LOGS}"
CATALINA_OPTS="${CATALINA_OPTS} -DJM.SNAPSHOT.PATH=${MIDDLEWARE_SNAPSHOTS}"
CATALINA_OPTS="${CATALINA_OPTS} -Dfile.encoding=${JAVA_FILE_ENCODING}"
CATALINA_OPTS="${CATALINA_OPTS} -Dfastjson.parser.autoTypeSupport=true"

以上是关于JVM内存之GC的主要内容,如果未能解决你的问题,请参考以下文章

JVM之内存泄漏和内存溢出

JVM之内存泄漏和内存溢出

深入理解JVM之JVM内存区域与内存分配

深入JVM系列之内存模型与内存分配

你真的懂JVM内存结构吗?—深入理解JVM之内存结构

深入JVM之理解JVM内存区域与对象创建内存布局