JVM 内存回收策略

JVM 内存回收策略

就Java语言本身来说,通常显式的内存申请有两种:一种是静态内存分配,另一种是动态内存分配。

静态内存分配和回收

在 Java 中静态内存分配是指在Java 被编译时就已经能够确定需要的内存空间,当程

序被加载时系统把内存一次性分配给它。这些内存不会在程序执行时发生变化,直到程序

执行结束时内存才被回收。在Java 的类和方法中的局部变量包括原生数据类型(int、long、

char 等)和对象的引用都是静态分配内存的

静态内存空间是在Java 栈上分配的,当这个方法运行结束时,对应的栈帧也就撤销,所以分配

的静态内存空间也就回收了。

动态内存分配和回收

在 Java 中对象的内存空间是动态分配的,所谓的动态分配就是在程序执行时才知道

要分配的存储空间大小,而不是在编译时就能够确定的。

内存的分配是在对象创建时发生的,而内存的回收是以对象不再引

用为前提的。这种动态内存的分配和回收是和Java 中一些数据类型关联的,Java 程序员

根本不需要关注内存的分配和回收,只需关注这些数据类型的使用就行了。

那么如何确定这个对象什么时候不被使用,又如何来回收它们,这正是JVM 的一个

很重要的组件——垃圾收集器要解决的问题。

如何检测垃圾

垃圾收集器必须能够完成两件事情:一件是能够正确地检测出垃圾对象,另一件是能

够释放垃圾对象占用的内存空间。其中如何检测出垃圾是垃圾收集器的关键所在。

只要是某个对象不再被其他活动对象引用,那么这个对象就

可以被回收了。这里的活动对象指的是能够被一个根对象集合到达的对象

虽然根对象集合和JVM 的具体实现也有关系,但是大都会包含如下一些元素。

◎ 在方法中局部变量区的对象的引用:如在前面的staticData 方法中定义的lg 和o

等对象的引用就是根对象集合中的一个根对象,这些根对象直接存储在栈帧的局

部变量区中。

◎ 在Java 操作栈中的对象引用:有些对象是直接在操作栈中持有的,所以操作栈肯

定也包含根对象集合。

◎ 在常量池中的对象引用:每个类都会包含一个常量池,这些常用池中也会包含很

多对象引用,如表示类名的字符串就保存在堆中,那么常量池中只会持有这个字

符串对象的引用。

◎ 在本地方法中持有的对象引用:有些对象被传入本地方法中,但是这些对象还没

有被释放。

◎ 类的Class 对象:当每个类被JVM 加载时都会创建一个代表这个类的唯一数据类

型的Class 对象,而这个Class 对象也同样存放在堆中,当这个类不再被使用时,

在方法区中类数据和这个Class 对象同样需要被回收。

JVM 在做垃圾回收时会检查堆中的所有对象是否都会被这些根对象直接或者间接引

用,能够被引用的对象就是活动对象,否则就可以被垃圾收集器回收。

基于分代的垃圾收集算法

该算法的设计思路是:把对象按照寿命长短来分组,分为年轻代和年老代,新创建的

对象被分在年轻代,如果对象经过几次回收后仍然存活,那么再把这个对象划分到年老代。

年老代的收集频度不像年轻代那么频繁,这样就减少了每次垃圾收集时所要扫描的对象的

数量,从而提高了垃圾回收效率。

这种设计的思路是把堆划分成若干个子堆,每个子堆对应一个年龄代

JVM 将整个堆划分为Young 区、Old 区和Perm 区,分别存放不同年龄的对象,这三

个区存放的对象有如下区别。

◎ Young 区又分为Eden 区和两个Survivor 区,其中所有新创建的对象都在Eden 区,

当Eden 区满后会触发minor GC 将Eden 区仍然存活的对象复制到其中一个

Survivor 区中,另外一个Survivor 区中的存活对象也复制到这个Survivor 中,以

保证始终有一个Survivor 区是空的。

Old 区存放的是Young 区的Survivor 满后触发minor GC 后仍然存活的对象,当

Eden 区满后会将对象存放到Survivor 区中,如果Survivor 区仍然存不下这些对

象,GC 收集器会将这些对象直接存放到Old 区。如果在Survivor 区中的对象足

够老,也直接存放到Old 区。如果Old 区也满了,将会触发Full GC,回收整个堆

内存。

◎ Perm 区存放的主要是类的Class 对象,如果一个类被频繁地加载,也可能会导致

Perm 区满,Perm 区的垃圾回收也是由Full GC 触发的。

Sun 对堆中的不同代的大小也给出了建议,一般建议Young 区的大小为整个堆的1/4,

而Young 区中Survivor 区一般设置为整个Young 区的1/8。

GC 收集器对这些区采用的垃圾收集算法也不一样,Hotspot 提供了三类垃圾收集算

法,下面详细介绍这三类垃圾收集算法的区别和使用方法。这三类垃圾收集算法分别是:

◎ Serial Collector;

◎ Parallel Collector;

◎ CMS Collector。

1.Serial Collector

Serial Collector 是JVM 在client 模式下默认的GC 方式。可以通过JVM 配置参数

-XX:+UseSerialGC 来指定GC 使用该收集算法。我们指定所有的对象都在Young 区的Eden

中创建,但是如果创建的对象超过Eden 区的总大小,或者超过了PretenureSizeThreshold

配置参数配置的大小,就只能在Old 区分配了,如-XX:PretenureSizeThreshold= 30720 在

实际使用中很少发生。

当 Eden 空间不足时就触发了Minor GC,触发Minor GC 时首先会检查之前每次Minor

GC 时晋升到Old 区的平均对象大小是否大于Old 区的剩余空间,如果大于,则将直接触

发Full GC,如果小于,则要看HandlePromotionFailure 参数(-XX:-HandlePromotionFailure)

的值。如果为true,仅触发Minor GC,否则再触发一次Full GC。其实这个规则很好理解,

如果每次晋升的对象大小都超过了Old 区的剩余空间,那么说明当前的Old 区的空间已经

不能满足新对象所占空间的大小,只有触发Full GC 才能获得更多的内存空间。

2.Parallel Collector

Parallel GC 根据Minor GC 和Full GC 的不同分为三种,分别是ParNewGC、ParallelGC

和ParallelOldGC。

1)ParNewGC

可以通过-XX:+UseParNewGC 参数来指定, 它的对象分配和回收策略与Serial

Collector 类似,只是回收的线程不是单线程的,而是多线程并行回收。在Parallel Collector

中还有一个UseAdaptiveSizePolicy 配置参数,这个参数是用来动态控制Eden、From Space

和To Space 的TenuringThreshold 大小的,以便于控制哪些对象经过多少次回收后可以直

接放入Old 区。

2)ParallelGC

在Server 下默认的GC 方式,可以通过-XX:+UseParallelGC 参数来强制指定,并行回

收的线程数可以通过-XX:ParallelGCThreads 来指定,这个值有个计算公式,如果CPU 和

核数小于8,线程数可以和核数一样,如果大于8,值为3+(cpu core*5)/8。

可以通过-Xmn 来控制Young 区的大小,如-Xman10m,即设置Young 区的大小为10MB。

在Young 区内的Eden、From Space 和To Space 的大小控制可以通过SurvivorRatio 参数来完

成,如设置成-XX:SurvivorRatio=8,表示Eden 区与From Space 的大小为8:1,如果Young

区的总大小为10 MB,那么Eden、s0 和s1 的大小分别为8 MB、1 MB 和1 MB。但在默认

情况下以-XX:InitialSurivivorRatio 设置的为准,这个值默认也为8,表示的是Young:s0 为8:1。

当在 Eden 区中申请内存空间时,如果Eden 区不够,那么看当前申请的空间是否大于

等于Eden 的一半,如果大于则这次申请的空间直接在Old 中分配,如果小于则触发Minor

GC。在触发GC 之前首先会检查每次晋升到Old 区的平均大小是否大于Old 区的剩余空间,

如大于则再触发Full GC。在这次触发GC 后仍然会按照这个规则重新检查一次。也就是

如果满足上面这个规则,Full GC 会执行两次。

在 Young 区的对象经过多次GC 后有可能仍然存活,那么它们晋升到Old 区的规则可

以通过如下参数来控制:AlwaysTenure,默认为false,表示只要Minor GC 时存活就晋升

到old;NeverTenure,默认为false,表示永远晋升到old 区。如果在上面两个都没设置的情

况下设置UseAdaptiveSizePolicy,启动时以InitialTenuringThreshold 值作为存活次数的阈值,

在每次GC 后会动态调整,如果不想使用UseAdaptiveSizePolicy,则以MaxTenuringThreshold

为准,不使用UseAdaptiveSizePolicy 可以设置为-XX:- UseAdaptiveSizePolicy。如果Minor

GC 时To Space 不够,对象也将会直接放到Old 区。

当 Old 或者Perm 区空间不足时会触发Full GC,如果配置了参数ScavengeBeforeFullGC,

在Full GC 之前会先触发Minor GC。

3)ParallelOldGC

可以通过-XX:+UseParallelOldGC 参数来强制指定, 并行回收的线程数可以通过

-XX:ParallelGCThreads 来指定,这个数字的值有个计算公式,如果CPU 和核数小于8,线

程数可以和核数一样,如果大于8,值为3+(cpu core*5)/8。

它与ParallelGC 有何不同呢?其实不同之处在Full GC 上,前者Full GC 进行的动作

为清空整个Heap 堆中的垃圾对象,清除Perm 区中已经被卸载的类信息,并进行压缩。而

后者是清除Heap 堆中的部分垃圾对象,并进行部分的空间压缩。

GC 垃圾回收都是以多线程方式进行的,同样也将暂停所有的应用程序。

3.CMS Collector

可通过-XX:+UseConcMarkSweepGC 来指定,并发的线程数默认为4(并行GC 线程

数+3),也可通过ParallelCMSThreads 来指定。

CMS GC 与上面讨论的GC 不太一样,它既不是上面所说的Minor GC,也不是Full GC,

它是基于这两种GC 之间的一种GC。它的触发规则是检查Old 区或者Perm 区的使用率,

当达到一定比例时就会触发CMS GC,触发时会回收Old 区中的内存空间。这个比例可以通

过CMSInitiatingOccupancyFraction 参数来指定, 默认是92%, 这个默认值是通过

((100MinHeapFreeRatio)+(double)(CMSTriggerRatio*MinHeapFreeRatio)/100.0)/100.0 计算出

来的,其中的MinHeapFreeRatio 为40、CMSTriggerRatio 为80。如果让Perm 区也使用CMS

GC 可以通过-XX:+CMSClassUnloadingEnabled 来设定,Perm 区的比例默认值也是92%,这

个值可以通过CMSInitiatingPermOccupancyFraction 设定。这个默认值也是通过一个公式计

算出来的: ((100MinHeapFreeRatio)+(double)(CMSTriggerPermRatio*MinHeapFreeRatio)/

100.0)/100.0,其中MinHeapFreeRatio 为40,CMSTriggerPermRatio 为80。

触发 CMS GC 时回收的只是Old 区或者Perm 区的垃圾对象,在回收时和前面所说的

Minor GC 和Full GC 基本没有关系。

在这个模式下的Minor GC 触发规则和回收规则与Serial Collector 基本一致,不同之

处只是GC 回收的线程是多线程而已。

触发 Full GC 是在这两种情况下发生的:一种是Eden 分配失败,Minor GC 后分配到 To

Space,To Space 不够再分配到Old 区,Old 区不够则触发Full GC;另外一种情况是,当

CMS GC 正在进行时向Old 申请内存失败则会直接触发Full GC。

这里还需要特别提醒一下,在Hotspot 1.6 中使用这种GC 方式时在程序中显式地调用

了System.gc,且设置了ExplicitGCInvokesConcurrent 参数,那么使用NIO 时可能会引发

内存泄漏

jvm

相关推荐