说起jvm中的gc回收机制,我们首先要了解下jvm的内存结构。
一、jvm的内存结构如下图
根据上图我们可以清晰的看出jvm的内存结构,那么GC,是针对我们内存的垃圾回收机制,而对于内存中,需要经常进行GC的,就莫过于我们的heap(java堆),想要知道为什么堆是我们GC经常活动的场所呢?那么首先我们要知道堆是干什么的,有什么样的特性。
二、堆
java的堆是我们java对象的活动空间,程序中我们new出的对象所分配的内存空间就存放在我们的堆中,那么当我们的对象不在被引用的时候,那么我们可以说这个对象在我们的内存中就变成了垃圾,需要垃圾回收器进行回收!
堆根据我们的GC需求分为三区域:
1.新域(eden):新域中的对象,经过一定次数的GC循环后(一般经过15次的GC循环后),被移入旧域;当对象在堆创建时,将进入年轻代的EdenSpace。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B SuvivorSpace,如果B Suvivor Space已经满,则复制 Old Gen扫描A SuvivorSpace时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。扫描完毕后,JVM将EdenSpace和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivorSpace。我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的SuvivorSpace的方式来确保不存在内存碎片,采用空间换时间的方式来加速内存垃圾回收。
2.旧域(old):年老代主要存放JVM认为比较old的对象(经过几次的YoungGen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边),当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
3.持久代(Survivor):持久代主要存放类定义、字节码和常量等很少会变更的信息,从配置的角度空,这个域是独立的,不包括在jvm堆内,默认是4M.
说完这些我们正式谈谈GC.......
三、GC
1.GC算法:引用计数法,跟踪收集法,跟踪收集法又包含了:copying算法,mark-sweep算法,mark-compact算法。
1.1复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。效果图如下:
1.2.标记-整理(Mark-Compact)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。效果图如下:
2.GC处理与JVM内存分配:
2.1. 对象优先在Eden分配:年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)
2.旧域(old):新域中的对象,经过一定次数的GC循环后(一般经过15次的GC循环后),被移入旧域;当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制 Old Gen扫描A Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivor Space。我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保不存在内存碎片,采用空间换时间的方式来加速内存垃圾回收。
2.2.大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组,虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,
2.3.长期存活的对象将进入老年代
在经历了多次的Minor GC后仍然存活:在触发了Minor GC后,存活对象被存入Survivor区在经历了多次Minor GC之后,如果仍然存活的话,则该对象被晋升到Old区。
虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
2.4.动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
2.5.Minor GC后Survivor空间不足就直接放入Old区
2.6.空间分配担保
在发生MinorGC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次FullGC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行MinorGC;如果不允许,则也要改为进行一次Full GC。大部分情况下都还是会将HandlePromotionFailure开关打开,避免FullGC过于频繁。
2.7JVM GC组合方式
2.8如何监视GC
1.概览监视gc。
jmap -heap [pid] 查看内存分布
jstat -gcutil [pid] 1000 每隔1s输出java进程的gc情况
2.详细监视gc。
在jvm启动参数,加入-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log。
输入示例:
[GC [ParNew: 11450951K->1014116K(11673600K), 0.8698830 secs] 27569972K->17943420K(37614976K), 0.8699520 secs] [Times: user=11.28 sys=0.82, real=0.86 secs]
表示发生一次minor
GC,ParNew是新生代的gc算法,11450951K表示eden区的存活对象的内存总和,1014116K表示回收后的存活对象的内存总和,11673600K是整个eden区的内存总和。0.8699520
secs表示minor gc花费的时间。
27569972K表示整个heap区的存活对象总和,17943420K表示回收后整个heap区的存活对象总和,37614976K表示整个heap区的内存总和。
[Full GC [Tenured: 27569972K->16569972K(27569972K), 180.2368177secs]36614976K->27569972K(37614976K), [Perm :28671K->28635K(28672K)],0.2371537 secs]
表示发生了一次Full GC,整个JVM都停顿了180多秒,输出说明同上。只是Tenured: 27569972K->16569972K(27569972K)表示的是old区,而上面是eden区。