JVM 新生代老年代与GC

Java堆可以分为新生代和老年代两个区,其中新生代又可以分为一个Eden区和两个Survivor区,两个Survivor区分别被命名为From和To以示区分,新生代和老年代的比例为1:2,它们共同组成堆的内存区,所以新生代占堆的1/3,老年代占2/3,但这个比例可以修改,下面分别来介绍一下新生代和老年代。

1、【新生代】

新生代分为三个区域,一个Eden区和两个Survivor区,它们之间的比例为(8:1:1),这个比例也是可以修改的。通常情况下,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中。Java虚拟机每次使用新生代中的Eden和其中一块Survivor(From),在经过一次Minor GC后,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上(这里使用的复制算法进行GC),最后清理掉Eden和刚才用过的Survivor(From)空间。将此时在Survivor空间存活下来的对象的年龄设置为1,以后这些对象每在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中。

在新生代中进行GC时,有可能遇到另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代;

总结:

1、Minor GC是发生在新生代中的垃圾收集,采用的复制算法;

2、新生代中每次使用的空间不超过90%,主要用来存放新生的对象;

3、Minor GC每次收集后Eden区和一块Survivor区都被清空;

2、【老年代】

老年代里面存放都是生命周期长的对象,对于一些较大的对象(即需要分配一块较大的连续内存空间),是直接存入老年代的,还有很多从新生代的Survivor区域中熬过来的对象。

老年代中使用的是Full GC,Full GC所采用的是标记-清除算法。老年代中的Full GC不像Minor GC操作那么频繁,并且进行一次Full GC所需要的时间要比Minor GC的时间长。

 

3、【GC】

   YGC :对新生代堆进行GC。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。

   FGC :全堆范围的GC。默认堆空间使用到达80%(可调整)的时候会触发FGC。以我们生产环境为例,一般比较少会触发FGC,有时10天或一周左右会有一次。

2.什么时候执行YGC和FGC

   a.Eden空间不足,执行 young gc

   b.old空间不足,perm空间不足,调用方法System.gc() ,YGC时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gc

 

案例一:

先把应用的heap dump下来分析下:

jmap -dump:format=b,file=path pid

用IBM的Heap Analyser分析,发现dubbo rpc调用的RpcInvocation对象和taglibs的SimpleForEachIterator对象占用了很大部分内存。

正常来说,这两种类型的对象都应该可以很快被回收掉,怎么会占用了那么大的内存空间?是不是有别的对象引用了它们,导致不能释放?

再仔细分析,发现RpcInvocation对象都是root refer的,也就是根对象,正常来说根对象应该可以很快就被回收掉的,为什么在内存中会有那么多对象?

再查看应用的JVM参数:

-Xms2g -Xmx2g -Xmn256m -XX:SurvivorRatio=8 -XX:ParallelGCThreads=8 -XX:PermSize=512m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled

首先发现应用的新生代,即-Xmn256m 设置得太小了。对照上面RpcInvocation对象占用了226M,SimpleForEachIterator占用了267M内存。

显然在新生代里,没办法放下那么多的对象,这些对象必然是被放到老生代(old space)里去了。

既然RpcInvocation对象和SimpleForEachIterator对象应该都是可以很快被回收了,那么思路变成,触发一下线上的FullGC,看下对象有没有被回收。

在触发之前,先用jmap -histo pid统计下对象的数量:jmap介绍https://blog.csdn.net/gyxinguan/article/details/106434970
  34:        136762        4376384  com.alibaba.dubbo.rpc.RpcInvocation
 129:         16345         392280  org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
用 jmap -histo:live <pid> 触发Full GC之后:
 294:           625          20000  com.alibaba.dubbo.rpc.RpcInvocation
 495:           292           7008  org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
果然数量大大的减少了。

所以结论比较明显了,新生代(Young generation)的空间太小,导致有一些本应该可以很快就被回收的对象被放到了老生代(Old generation)里,导致老生代上涨很快,频繁Full GC。

于是想办法增加新生代的大小,把JVM参数改为:

 

 -Xms2g -Xmx2g -XX:ParallelGCThreads=8 -XX:PermSize=256m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled 


因为观察到PermSize实际上只用了不到200M,没有必要设置为512M,浪费内存,所以改为 -XX:PermSize=256m -XX:MaxPermSize=512m 。

另外,把新生代最大限制-Xmn256m 去掉。因为默认的NewRatio = 2,即除了PermSize,新生代大约占内存的1/3,即约(2048 - 256) /3 = 597M。和原来相比增大了一倍不止。

修改上线之后,观察发现Old Space增长缓慢,FullGC次数大大减少,时间在50ms下,Yong GC都在10ms下,达到了想要的效果。

 

简单的GC过程分析

首先来看一张GC的模型图,很形象:

简单来说,对于GC,我们了解到这些信息就足够了。

大部分新对象在Eden Space上分配,当Eden Space满了,则要用到Survivor Space来回收。YGC的算法是很快的。

多次YGC之后,还存活的对象就会被移到Old Generation(old space)上,当Old Generation满了的时候,就会FGC,FGC有通常比较慢。

Permanent Space只要你在开始时分配了足够大的空间,那它可以不用管。

我们可以得出一些结论:

 

合理减少对象进入老生代;
Old Space可能会一直增长,有时没有办法避免不让对象进入Old Space,当然也有一些程序是从来都不执行FGC的;
是不是尽全力防止对象进入老生代?显然不是,有些对象如果长久存在在新生代里,显然加重了YGC的负担,多次YGC之后仍然存活的对象显然应该放到Old Space里。
 

理想的GC/内存使用情况

总结下来,可以发现,理想的GC情况应该是这样的:

Old Space增长缓慢,FullGC次数少,FullGC的时间短(大部情况应该要在1秒内)。

 

案例二:

1、ps -ef|grep 进程名         查找运行中的进程

2、jstat -gcutil pid 500 3  (24253 pid;500 0.5秒;3次)

通过 jstat 来监视23998的Java进程统计信息,各项监视参数:

S0、S1 代表两个Survivor区;

E 代表 Eden 区;

O(Old)代表老年代;

P(Permanent)代表永久代;

YGC(Young GC)代表Minor GC;

YGCT代表Minor GC耗时;

FGC(Full GC)代表Full GC耗时;

GCT代表Minor & Full GC共计耗时。

1、使用top查看消耗用户态cpu高的进程
可以在交互区输入shift+p 按照cpu排序
可以在交互区输入shift+m 按照内存排序
可以在交互区输入shift+H 查看进程下的线程
top -p 进程号,查看某个进程
top,查看所有进程

2、查看进程下的所有线程信息
top -H -p 1963(进程号)
可以在交互区输入shift+t 按照占用cpu时间排序
将占用时间高的线程号1966 转化为十六进制
printf %x 1966 ===》7ae

3、查看线程下的哪个方法造成cpu高
jstack 1963|grep 0x7ae
 

4、jmap -heap pid 展示pid的整体堆信息

      jmap -histo pid 展示class的内存情况

      jmap -dump:live,format=b,file=a.log pid 将内存使用的详细情况输出到文件

1.如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况。
2.要制作堆Dump可以直接使用jvm自带的jmap命令
3.可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况。
4.使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。
5.也可以使用 jmap -dump:format=b,file=<fileName>命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容
6.在内存出现泄露、溢出或者其它前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析。

7.在用cms gc的情况下,执行jmap -heap有些时候会导致进程变T,因此强烈建议别执行这个命令,如果想获取内存目前每个区域的使用状况,可通过jstat -gc或jstat -gccapacity来拿到。

Java 堆分为新生代和老年代,新生代一般划分为三块区域,Eden + From Survivor + To Survivor,Eden 和 Survivor 的内存比为8:2,每次只使用一个Eden 和一个 Survivor 区域,另一个 Survivor 用于复制收集算法回收内存。

对象一般尽量分配到新生代中,而对于大对象(长字符串和大数组)直接分配在老年代中,同时“年龄”长的的对象会从新生代自动晋升到老年代中。

Java 方法区称为永久代,只有 HotSpot 虚拟机才存在永久代。

当 Eden 区域分配不足时,自动发生一次 Minor GC。

当发生 Minor GC 时,虚拟机会自动检测(比较)新生代晋升到老年代的对象内存大小和老年代剩余内存大小,如果晋升>剩余,则发生一次Full GC;如果晋升<剩余,则去检测老年代的内存担保 HandlePromotionFailure 是否允许担保失败,如果不允许担保失败,则发生一次Full GC,如果允许失败,则进行一次Minor GC。

 

官方推荐新生代占堆的3/8

幸存代占新生代的1/10

-Xms4:初始时堆内存

-Xmx4:最大堆内存

-Xmn2g:年轻代内存

-Xss1024K:一个线程的堆栈大小(1M)

-XX:PermSize=256:初始永久代内存

-XX:MaxPermSize=512m:最大永久代内存

-XX:ParallelGCThreads=8:并行收集器的线程数

-XX:+UseConcMarkSweepGC:并发标记清除(CMS)收集器

-XX:+UseParNewGC:并行收集

-XX:+UseCMSCompactAtFullCollection://在FULL GC的时候对年老代的压缩

-XX:NewRatio:新生代与老年代比值,例如:4,表示新生代:老年代=1:4,即新生代占整个堆的1/5

-XX:SurvivorRatio=4:设置两个Survivor区和eden的比值,例如:4,表示两个Survivor:eden=2:4,即一个Survivor占年轻代的1/6

-XX:MaxTenuringThreshold=10:垃圾最大年龄

-XX:CMSInitiatingOccupancyFraction=80://使用80%后開始CMS收集

 

案例三:

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值