jvm堆内存年轻代触发MInorGC和和老年代触发FullGC的场景分析

 

了解什么是内存碎片?

大量的实例对象在堆内存新生代中因为没有了栈内存的局部变量的引用,而成为新生代中需要被垃圾回收的垃圾对象.此时垃圾对象被回收之后,垃圾对象所占用的内存区域就成为了内存碎片.

了解什么是新生代的复制算法?

因为内存碎片的原因,可能导致大量的垃圾对象虽然被回收了.但是内存区域都是一小块一小块的,并不能被新产生的实例对象所使用.从而产生了内存浪费因此需要一种方法,能够将内存中存活的对象给移动到另一个块儿内存区域中,并且紧密排列,然后对剩下的批量对垃圾对象进行统一清除.然后新产生的对象就继续放到新的内存区域内,等到新内存区域同样满了的时候,就将该区域内的存活对象移动到上一块儿被清除过的内存区域中,然后全量回收垃圾对象. 两块儿内存区域各一半新生代内存.这就是复制算法.

复制算法缺点:

上面的描述,解决了内存碎皮的问题,但是产生了一个新的问题,就是两块儿区域来回倒腾的过程中,始终有一块儿内存区域被闲置.这样就同样会产生内存浪费.

复制算法优化:

采用三块儿内存区域,并且设置内存区域的比例为8:1:1,然后三者进行来回切换.   比例为8的叫Eden区(伊甸园区),比例为1的统称为Survivor区(存活区), 刚开始分配对象的时候以Eden区为主,等到快满的时候,将存活对象移动到Survivor1区,然后对Eden区进行MinorGC,然后下次实例化的对象将会继续放在Eden区,等到下次Eden区满的时候,就会将Eden区的对象和上次放在Survivor的存活对象批量移动到另一个Survivor2区,然后再对Eden区和Survivor1区进行垃圾对象的回收清空.以此循环往复,就能保证始终只有十分之一的内存区域被闲置,而十分之九的内存可以被使用.

 

为什么要设置新生代对象的年龄?

所以从一开始就可以设置新生代对象的年龄.通过-XX:MaxTenuringTreshold参数设置的低一点,默认值为15,比如这里我们设置为5.

目的就是让其防止一次性堆积太多对象.


什么时候新生代的存活对象会到老年代中?

上面的复制算法虽然好,但是总会产生存活对象满了的情况,这个时候大对象想要放入新生代放不下区,该怎么办?

第一种的情况就是将躲过15次MinorGC的对象移动到老年代.

第二种就是动态年龄对象判断,既Survivor区的经历过两次GC的对象大小大于Survivor区容量的一半的时候,如Survivor区是100m,里面的对象之和大于50m.就将这些2次GC还存活的对象移入到老年代中去.

第三种就是大对象,还没有进入到新生代的时候就被移动到老年代,这里有个参数,为-XX:PretenureSizeThreshold,可以设置值,比如1m,那么再进入堆内存的时候,就会检查这个实例对象的大小,如果大于这个阈值,就直接进入到老年代.

 

如果Eden区中对象过多,加上Survivor1区的存活对象,总容量大于了容量为堆内存的容量10分之一的Survivor2区.这个时候该怎么办呢?

此时会通过一个参数-XX:-HandlePromotionFalure是否允许担保失败来进行判断(jdk1.6以前可以用,之后的版本被废弃)为true,则会判断老年带可用内存空间是否大于历次进行移动老年代的对象容量的平均值,

如果大于,则尝试进行一次Minor GC(Yong GC),但这次Minor GC(Yong GC)依然是有风险的,失败后会重新发起一次Major GC(Full GC);
如果小于或者HandlePromotionFailure=false,则改为直接进行一次Major GC(Full GC)老年代回收.尽量腾出一点空间

(但是在jdk1.6 update 24之后-XX:-HandlePromotionFailure 不起作用了,只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则FullGC)

fullGC完毕之后,再尝试进行以此MinorGC.   这里会有三个问题,第一,GC完了之后的存活对象刚好能够放入到Survivor2区中.

那么就可以不用进入老年代了.

第二就是GC完了存活对象大于Survivor2区的容量,并且小于老年代的可用容量.

第三就是MinorGC完了后存活对象大于Survivor2区的容量,并且也大于老年代的可用容量,此时就会再次对老年代进行fullGC.如果GC完了还是不够存放那些存活对象.就会出现我们常见的OOM异常.内存溢出了.

 

老年代的标记整理算法是什么?

新生代类似的问题,回收过程中为了避免内存碎片的产生.这里老年代并没有给自己分配空白内存区域.而是采用了标记整理算法.

本来是杂乱无章的存放在老年带内存中,现在将这些存活的对象进行标记,并且不断的移动位置.统一移动到相对紧凑的内存区域.留出大量的空间和垃圾对象,然后一次性对剩下的垃圾对象进行回收处理.

 

为什么要减少老年代的FullGC操作?

老年代的FullGC是十分缓慢的.其标记整理算法至少比新生代的复制算法要慢十倍.所以应该尽量减少老年代的垃圾回收.有种场景就是新生代的Eden和两个survivor区的大小分别是800兆,100m,100m.这样当每次eden区存活对象为200m的时候是进不去survivor区的.只能进入老年代中去,这样就会使老年代没过多久就会被填满.就会出问题.老年代的FullGC过于缓慢.为了避免存活对象快速的进入老年代去.这里应该让存活对象能够survivor区和Eden区快速流转..所以新生代要设置大一点.比如设置surivor区的大小为200m.也就是对应的堆内存新生代为2000m.eden区为1600m.这就是为什么一半新生代的大小设置的要比老年代多的原因.

上述方案虽然能解决一部分问题,但是因为动态年龄对象判断,既Survivor区的经历过两次GC的对象大小大于Survivor区容量的一半的时候,如Survivor区是100m,里面的对象之和大于50m.就将这些2次GC还存活的对象移入到老年代中去.   这个时候仍然可能存在对象频繁进入老年代去的.这个时候我们可以通过一个参数XX:SurvivorRatio=8设置eden区占新生代的比例.让survivor区的大小尽量大一点.就可以避免动态年龄对象判断所产生的对象进入老年代的问题.

 

 

什么是Stop the World?

系统在进行垃圾回收的时候会暂停java程序的执行.直到垃圾回收完成.所谓jvm的迭代演进其实就是基本上就是在解决减少垃圾回收,和垃圾回收的时间尽可能的短.一起都取决于垃圾回收算法.

新生代的垃圾回收器ParNew是什么?

参数为-XX:+UserParNewGC    默认值为CPU核数,该新生代垃圾回收器针对多核CPU做到了资源利用,可以支持多个线程进行执行垃圾回收.提升垃圾回收的性能,减少垃圾回收的时间

另外如果想要针对多核cpu做配置,-XX:ParallelGCThreads可以设置垃圾回收器的线程数,但是该值一般不用设置,系统默认就是几核cpu就几个线程.

平时进行启动java程序的时候有跟着加-server的,还有跟着加-client的,这是啥意思呢?

其实只要理解为java写的一些服务器程序,比如只是提供接口的程序,并且运行再linux的程序,可以设置-server,因为可以充分利用linux多核的cpu.此时可以提高运行效率.   相反,如果程序运行在windows的程序比如百度网盘,此时可以加上-client.证明该程序是客户端程序

服务器程序通常是网站系统,电商系统等大型系统,拥有很好的CPU支持,所以垃圾回收时可以采用ParNew充分利用其多线程特性.加快垃圾回收速度.   如果用了单线程垃圾回收,则会存在资源浪费.

客户端程序通常一个单核CPU,如果还是用ParNew开辟多个线程,反而加重了程序运行的负担.所以windows系统建议采用Serial垃圾回收器,单线程回收即可.但是很少现在客户端有用java写的.所以-client不常用.

 

 

 

 

 

 
posted @ 2020-07-11 13:37  后知、后觉  阅读(2029)  评论(0编辑  收藏  举报