声明:此篇文章是读《深入理解JAVA虚拟机》的笔记
1. 对象已死?
堆中几乎存放着Java中所有的对象实例,垃圾收集器在回收前,如何判断哪些对象是活着,哪些对象已经死去?
-
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
但是,Java语言中没有选用引用计数算法来管理内在,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。
模拟代码如下:
/**
*JVM的GC日志的主要参数包括如下几个:
*-XX:+PrintGC 输出GC日志
*-XX:+PrintGCDetails 输出GC的详细日志
*-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
*-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
*-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
*-Xloggc:../logs/gc.log 日志文件的输出路径
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
//占用空间
private byte[] bigSize = new byte[2*_1MB];
public static void main(String[] args) throws InterruptedException {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
GC日志显示:
[GC (System.gc()) [PSYoungGen: 5336K->504K(6144K)] 5336K->608K(19968K), 0.0012319 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-
根搜索算法
这个算法的基本思路就是通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如图所示,object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以将会被判定为是可回收的对象:
在Java语言里,可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2. 方法区中的类静态属性引用的对象。
3. 方法区中的常量引用的对象。
4. 本地方法栈中JNI引用的对象。 再谈引用
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
1. 强引用就是在指程序代码之中普遍存在的,类似Object obj= new Object()这类的引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
2. 软引用用来区描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。在JDK1.2之后,提供了SoftReference类来实现软引用。
3. 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
4. 虚引用也称为幽灵引用或者幻影引用,它是最弱的一个引用关系。虚引用对一个对象的生存时间完全不会影响。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到 一个系统通知。生存还是死亡?
在根搜索算法中不可达对象,也并非是非死不可的,虚拟机在回收对象之前会调用对象的finalize()方法,来判断此对象是否重新被引用,前提是这个方法没有被执行过,因为这个方法只能被虚拟机调用一次。但是此方法的运行代价大,不确认性高,所以不推荐使用。回收方法区
永久代(元空间)的垃圾收集主要回收两部分内容:废弃常量和无用的类。
当在发生内存回收的时候,常量池中的某些常量没有被任何地方引用,那这个常量就会被请出常量池。
如果要判定类是否为无用的类,条件要苛刻的多:
1. 该类所有的实例都已经被回收。
2. 加载该类的ClassLoader已经被回收。
3. 该类对应的java.lang.Class对象没有在任何地主被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足以上3个条件的无用类进行回收。但是对于类的回收不是必然的,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类的加载和卸载信息。
注意:在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成jsp和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保永久代(元空间)不会溢出。
2. 垃圾收集算法
-
标记-清除算法
最基础的收集算法,包含标记和清除两个阶段。缺点有两个:一个是标记和清除的效率都不高;另一个是标记清除之后会产生大量不连续的内存碎片(当程序以后在分配较大的对象时,若无法找到足够的连续的内存,就不得不提前触发另一次垃圾回收动作)。
-
复制算法
将可用的内存容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法的代价是将内存缩小为原来的一半。
Tips:HotSpot虚拟机默认分为一个Eden空间和两个Survivor。Eden空间的大小与Survivor的大小比例为8:1; -
标记-整理算法
根据老年代的特点,提出的一种算法。先进行标记,然后把所有存活的对象都向一端移动,最后直接清理掉边界以外的内存。
-
分代收集算法
大部分虚拟机采用的算法,这种算法没有什么新的思想,只是根据对象的存活周期和不同将内存划分为几块。一般是把Java堆分为新生代和老生代,这样就可以根据各个年代的特点采用最适当的收集算法。
3. 垃圾收集器
简单来说垃圾收集器就是内存回收的具体实现。虽然有不同的收集器,但是目前为止还没有最好的收集器出现,也没有万能收集器,我们选择的只是对具体应用最合适的收集器。
-
Serial收集器
最基本,历史最悠久的收集器。单线程收集器,这里的单线程不是只会使用一个CPU或者一条收集线程去完成垃圾收集工作,而在它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
-XX:+UseSerialGC开启Serial收集器。 -
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,默认开启的收集线程数与CPU数量相同,使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
-XX:+UseParNewGC开启ParNew收集器。 -
Parallel Scavenge收集器
同样使用复制算法的收集器,又是并行的多线程收集器。其目标是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
Tips:
-XX:+UseParallelGC开启Parallel收集器。
-XX:MaxGCPauseMillis用来设置最大垃圾收集停顿时间。
-XX:GCTimeRatio用来设置吞吐量的大小。
-XX:+UseAdaptiveSizePolicy打开此参数时,虚拟机可以自己调节GC策略。自适应调节策略是Parallel Scavenge收集器与ParNew收集器的一个重要区别。 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-清理算法。Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。在注重吞吐量及CPU敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
-XX:+UseParallelOldGC开启Parallel Old收集器。-
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法,不过它的运作过程要复杂一些,分为4个步骤:
1. 初始标记(CMS initial mark)
2. 并发标记(CMS concurrent mark)
3. 重新标记(CMS remark)
4. 并发清除(CMS concurrent sweep)
并发收集、低停顿是CMS收集器的优点,但是它也是三个显著的缺点:
a. CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收垃圾时垃圾收集线程最多占用不超过25%的CPU资。但是当CPU不足4个时,那么CMS对用户程序的影响就可能变得很大。
b. CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。所谓的浮动垃圾是在清理期间用户线程产生的新垃圾。也正是用户线程还需要运行,所以还需要预留足够的空间给用户线程使用。可以用-XX:CMSInitiatingOccupancyFraction设置。
注意:设置的太高将会容易导致大量Concurrent Mode Failure,导致性能降低。
c. 最后一个缺点是基于标记-清除算法的收集器,会产生大量的空间碎片。
Tips:
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC开启CMS收集器(搭配新生代ParNew收集器)。
-XX:UseCMSCompactAtFullCollection参数用于在享受完一个FullGC后提供一次碎片整理过程。
-XX:CMSFullGCCsBeforeCompaction参数用于设置在执行多少次不压缩的Full GC后,来一次带压缩的。 G1收集器
基于标记-整理算法实现的收集器。优点:
1. 不会产生空间碎片
2. 可以非常精确地控制停顿
G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
Tips:
G1可用的命令行选项有:
-XX:+UseG1GC 让JVM使用G1垃圾回收器。
-XX:MaxGCPauseMillis=200 设置GC暂停时间目标值,缺省200毫秒。但这不是硬指标,JVM会尽力满足。
-XX:InitiatingHeapOccupancyPercent=45 整个堆被占用多少之后开始进行GC,缺省为45,0表示持续不停进行GC。
-XX:NewRatio=n 年轻代和老年代的比例,缺省为2。
-XX:SurvivorRatio=n Eden和Survivro的比例,缺省为8。
-XX:G1ReservePercent=n 保留的堆大小,减少晋升过程中出错的可能性,也就是增加可用的to-space内存,缺省是10。
-XX:G1HeapRegionSize=n G1中,堆分为大小相等的区域。这个参数设置区域的大小,缺省值取决于堆的总大小,有效取值是1M-32M。