内存管理和垃圾回收是JVM非常关键的点,对Java性能的剖析而言,了解内存管理和垃圾回收的基本策略非常重要。最近进行了系列性能测试培训,对JVM内存管理涉及到的概念重新学习梳理了遍,总结下方便大家共同学习进步。

一、为什么要分代

在程序运行过程当中,会创建大量的对象,这些对象大部分是短周期的对象,小部分是长周期的对象。对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频繁垃圾回收以确保无谓地垃圾扫描检测。为了解决这种矛盾,Sun JVM的内存管理采用分代的策略。

 

二、如何分代

这里先上传一张模型图:


如图所示,虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

1.年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Survivor Space(命名为A和B)

  • 当对象在堆创建时,将进入年轻代的Eden Space。
  • 垃圾回收器进行垃圾回收时,扫描Eden Space和A Survivor Space,如果对象仍然存活,则复制到B Survivor Space,如果B Survivor Space已经满,则复制到 Old Gen
  • 扫描A Survivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。
  • 扫描完毕后,JVM将Eden Space和A Survivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Survivor Space)。

我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Survivor Space的方式来确保不存在内存碎片,采用空间换时间的方式来加速内存垃圾回收。
2.年老代(Tenured Gen):年老代主要存放JVM认为比较old的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

3.持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

 

 

三、什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GCFull GC

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

· 年老代(Tenured)被写满

· 持久代(Perm)被写满 

· System.gc()被显示调用 

·上一次GC之后Heap的各域分配策略动态变化