JVM中垃圾回收分为scanvenge gc和full GC
1,新生代:
(1)所有对象创建在新生代的Eden区,当Eden区满后触发新生代的Minor GC,将Eden区和非空闲Survivor区存活的对象 复制到另外一个空闲的Survivor区中。
(2)保证一个Survivor区是空的,新生代Minor GC就是在两个Survivor区之间相互复制存活对象,直到Survivor区满为止。
2,老年代:
当Survivor区也满了之后就通过Minor GC将对象复制到老年代。老年代也满了的话,就将触发Full GC,针对整个堆(包括新生代、老年代、持久代)进行垃圾回收。
3,持久代:
持久代如果满了,将触发Full GC。
full GC触发的条件:除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
5. 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
垃圾回收包含的内容不少,但顺着下面的顺序捋清知识也并不难。首先要
搞清垃圾回收的范围(栈需要GC去回收吗?),然后就是回收的前提条件
如何判断一个对象已经可以被回收(这里只重点学习根搜索算法就行了),
之后便是建立在根搜索基础上的三种回收策略,最后便是JVM中对这三种
策略的具体实现。
1.范围:要回收哪些区域?
Java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,
所以这些区域不需要考虑回收问题。Java堆和方法区是GC回收的重点区域,
因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要
的内存可能也不一样,而这两个区域又对立于栈可能随时都会有对象不再
被引用,因此这部分内存的分配和回收都是动态的。
2.前提:如何判断对象已死?
(1)引用计数法
引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,
但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B
也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的
是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
下面是循环引用的例子,在Hotspot JVM下可以被正常回收,可以证实JVM
采用的不是简单的引用计数法。通过-XX:+PrintGCDetails输出GC日志。
[java] view plaincopy
package com.cdai.jvm.gc;
public class ReferenceCount {
final static int MB = 1024 * 1024;
byte[] size = new byte[2 * MB];
Object ref;
public static void main(String[] args) {
ReferenceCount objA = new ReferenceCount();
ReferenceCount objB = new ReferenceCount();
objA.ref = objB;
objB.ref = objA;
objA = null;
objB = null;
System.gc();
System.gc();
}
}
[Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K), [Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
(2)根搜索
通过选取一些根对象作为起始点,开始向下搜索,如果一个对象到根对象
不可达时,则说明此对象已经没有被引用,是可以被回收的。可以作为根的
对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。
因为每个线程都有一个栈,所以我们需要选取多个根对象。
附:对象复活
在根搜索中得到的不可达对象并不是立即就被标记成可回收的,而是先进行一次
标记放入F-Queue等待执行对象的finalize()方法,执行后GC将进行二次标记,复活
的对象之后将不会被回收。因此,使对象复活的唯一办法就是重写finalize()方法,
并使对象重新被引用。
[java] view plaincopy
package com.cdai.jvm.gc;
public class DeadToRebirth {
private static DeadToRebirth hook;
@Override
public void finalize() throws Throwable {
super.finalize();
DeadToRebirth.hook = this;
}
public static void main(String[] args) throws Exception {
DeadToRebirth.hook = new DeadToRebirth();
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
DeadToRebirth.hook = null;
System.gc();
Thread.sleep(500);
if (DeadToRebirth.hook != null)
System.out.println("Rebirth!");
else
System.out.println("Dead!");
}
}
要注意的两点是:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
3.策略:垃圾回收的算法
(1)标记-清除
没错,这里的标记指的就是之前我们介绍过的两次标记过程。标记完成后就可以
对标记为垃圾的对象进行回收了。怎么样,简单吧。但是这种策略的缺点很明显,
回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。
虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了
后两种策略的产生。
(2)标记-复制
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另
一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。
但这种策略也有缺点,可用内存变为一半了!
怎样解决呢?聪明的程序员们总是办法多过问题的。可以将堆不按1:1的比例分离,
而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象
复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。
从下图可以看到这三块区域如何配合完成GC的,具体的对象空间分配以及晋升请
参加后面第6条补充。
为什么不是全部呢?如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是
老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。那么
老年代也使用标记-复制策略吧?当然不行!老年代中的对象可不像新生代中的,
每次回收都会清除掉大部分。如果贸然采用复制的策略,老年代的回收效率可想而知。
(3)标记-整理
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将
所有存活下来的对象都向一端移动。
4.实现:虚拟机中的收集器
(1)新生代上的GC实现
Serial:单线程的收集器,只使用一个线程进行收集,并且收集时会暂停其他所有
工作线程(Stop the world)。它是Client模式下的默认新生代收集器。采用的是复制算法。
ParNew:Serial收集器的多线程版本。在单CPU甚至两个CPU的环境下,由于线程
交互的开销,无法保证性能超越Serial收集器。新生代采用复制算法,老年代采用标记整理
Parallel Scavenge:也是多线程收集器,与ParNew的区别是,它是吞吐量优先
收集器。吞吐量=运行用户代码时间/(运行用户代码+垃圾收集时间)。另一点区别
是配置-XX:+UseAdaptiveSizePolicy后,虚拟机会自动调整Eden/Survivor等参数来
提供用户所需的吞吐量。我们需要配置的就是内存大小-Xmx和吞吐量GCTimeRatio。
针对新生代,采用复制收集算法
(2)老年代上的GC实现
Serial Old:Serial收集器的老年代版本。新生代采用复制,老年代采用标记整理。
Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的话,
老年代只有Serial Old能与之配合。现在有了Parallel Old与之配合,可以在注重吞吐量
及CPU资源敏感的场合使用了。针对老年代,标记整理。
CMS:采用的是标记-清除而非标记-整理,是一款并发低停顿的收集器。但是由于
采用标记-清除,内存碎片问题不可避免。可以使用-XX:CMSFullGCsBeforeCompaction
设置执行几次CMS回收后,跟着来一次内存碎片整理。
5.触发:何时开始GC?
Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC
回收新生代。而Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:
(1)老年代空间不足
(2)PermSpace空间不足
(3)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
这里注意一点:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来
实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。
6.补充:对象的空间分配和晋升
(1)对象优先在Eden上分配
(2)大对象直接进入老年代
虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到
老年代中。因为新生代采用的是标记-复制策略,在Eden中分配大对象将会导致Eden区
和两个Survivor区之间大量的内存拷贝。
(3)长期存活的对象将进入老年代
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度
默认为15岁)时,就会晋升到老年代中。