第3章 垃圾收集器和内存分配策略

3.2 对象是否死亡

堆里存放几乎所有的对象实例,垃圾收集器对堆回收前,要判断对象中哪些是活着的,哪些不可能再被任何途径使用的对象。

3.2.1 引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不能再被使用的。

但如果对象A和对象B都有instance,让A.instance=B和B.instance=A,除此之外,两个对象没有任何其他引用,该两个对象也不可能再被访问,但是因为互相引用对方,所以计数器都不为0,所以也无法回收。

3.2.2 可达性分析算法

通过一系列为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径叫引用链,一个对象到GC Roots没有任何引用链相连,也就是到GC Roots不可达,说明对象是不可用的。

这里写图片描述

object5、6、7之间虽然关联,但是因为到GC Roots不可达,所以会判定为可回收对象。

Java中可被作为GC Roots对象的有几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI引用的对象

3.2.3 引用分类

JDK1.2后的引用扩充,引用分为4种:强引用、软引用、弱引用、虚引用,强度依次减弱

  1. 强引用: 代码中普遍存在的,例如Object obj = new Object(),只要强引用还在,GC永不会回收该对象。
  2. 软引用: 一些有用还非必需的对象,软引用关联的对象,在将要发生内存溢出之前,将把这些对象列进行回收范围中进行第二次回收。提供SoftReference类实现软引用。
  3. 弱引用: 描述非必需对象,强度比软引用更弱,弱引用关联的对象只能生存到下一次GC发生前。进行GC时,无论内存是否足够,弱引用关联的对象都会被回收。提供WeakReference类实现弱引用。
  4. 虚引用: 是最弱的引用关系,对象是否有虚引用,完全不会对其生存时间构成影响,无法通过虚引用来获得一个对象实例。虚引用唯一一个目的就是在这个对象被GC时收到一个系统通知。PhantomReference类实现虚引用。

3.2.4 是否可以逃脱回收

对于在可达性分析算法中不可达的对象,也不是必须被GC。要真正被GC要经历两次标记过程。
对象在进行可达性分析后发现没有与GC Roots连接的引用链,将被第一次标记且进行一次筛选,筛选对象是否有必要执行finalize()方法。如果对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,就没必要执行。

如果对象被判定执行finalize方法,该对象会被放在一个F-Queue队列中,等待执行方法。
finalize方法是对象被GC前最后一次机会,稍后GC对F-Queue中对象进行第二次小规模标记,对象要在finalize中救自己-只要重新与引用链上的任何一个对象关联即可。例如,把自己用this关键字,赋值给某个类变量或成员变量,就会在第二次被标记时将它移除“被GC的集合”。

一个对象的finalize()方法最多只会被系统自动调用一次。
对象自救演示

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes.im still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //finalize优先级很低,暂停0.5s等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,im dead");
        }

        //代码和上面一样,但这次失败了
        SAVE_HOOK = null;
        System.gc();
        //finalize优先级很低,暂停0.5s等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,im dead");
        }
    }
}

第一次成功了,第二次却失败了,因为finalize只会被系统调用一次,如果对象面临下一次回收,它的finalize不会再执行,自救失败了。
finalize能做的功能,try-catch可以做的更好,应该在工作中避免使用finalize方法。

3.2.5 回收方法区

方法区也就是虚拟机中的永久代,主要垃圾回收两部分内容:废弃常量和无用的类。

回收废弃常量和回收堆中对象类似。假如一个字符串“abc”已经进入常量池,但没有一个String对象是叫做“abc”的,也就是没任何对象引用常量池中“abc”常量,如果这时有内存回收,“abc”常量就会被清出常量池。
常量池中其他类(接口)、方法、字段的符号引用也类似。

判断类是否无用,要满足三个条件:
1. 该类所有实例被回收,堆中不存在任何该类的实例
2. 加载该类的ClassLoader以及被回收
3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足了3个方法就可以回收,但不一定必然回收。
Hotspot提供-Xnoclassgc参数进行控制,还可以用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP等要频繁自定义ClassLoader的场景都需要具备类卸载的功能,保证永久代不会溢出。

3.3 垃圾收集算法

3.3.1 标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有要回收的对象,标记完成后统一回收所有被标记的对象。标记过程在之前说的。
后续算法都是基于此来改进的。

不足有两个:
一个是效率,标记和清除两个过程效率都不高;
另一个就是空间,标记清除后产生大量不连续内存碎片,碎片化严重会导致,分配较大对象时,找不到连续内存而提前出发另一次GC。

3.3.2 复制算法

将可用内存按容量分为大小相等两个区域,每次只用其中一块。当这块内存用完,就将还存活的对象复制到另一个区域,把刚才用过的空间一次清理掉。每次都对整个半区进行内存回收。

代价就是将内存缩小到原来的一般,通过这种算法来回收新生代

实现就是内存分为一块较大的Eden区域,和两块较小的Survivor空间,每次使用Eden和其中一块Survivor区域。当回收时,将Eden和survivor区域活的对象复制到另一个survivor区域。然后清理掉Eden和survivor区域。

默认Eden和survivor大小比例是8:1,如果没办法保证不多于10%的对象存活,也就是survivor区域空间不够,可以依赖其他内存(年老代)进行分配担保。

3.3.3 标记-整理算法

如果不想浪费50%空间,就要有额外空间进行分配担保,应该内存中所有对象100%都存活的极端情况,老年代一般不直接选这种复制算法。

根据老年代特点衍生出的算法。
与标记-清理算法相似,但后续不是对可回收对象进行清理,而是让所有存活对象都向一端移动,然后清理端边界以外的内存。

3.3.4 分代收集算法

当前虚拟机的垃圾收集都用“分代收集”,根据对象存活周期不同将内存分为几块。
一般把堆分为新生代和年老代,根据不同年代采用各自合适的算法。新生代就用复制算法,老年代就用标记-整理算法。

3.5 垃圾收集器

讨论基于JDK1.7 update14之后的虚拟机
这里写图片描述
共有7种不同分代的收集器,有处于新生代或老年代的收集器。

3.5.1 Serial收集器

Serial收集器是历史悠久的收集器,曾是新生代的唯一选择。是一个单线程收集器。
它进行垃圾收集时,必须暂停其他所有的工作线程,直到它结束。

新生代收集器的升级:Serial收集器到Parallel收集器,再到CMS乃至G1。

现在依然是虚拟机client模式下的默认新生代收集器,优点是简单高效,单CPU下没有线程交互开销,可以获得最高的单线程收集效率。

3.5.2 ParNew收集器

ParNew收集器是Serial收集器的多线程版本,在行为上除了是多线程外都和Serial很像。包括可用的所有控制参数(-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)收集算法、回收策略等都和Serial一样。

是许多Server模式下的虚拟机首选新生代收集器,只有它能喝CMS收集器配合工作。


名词解释
并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:指用户线程与垃圾收集线程同时执行,用户程序继续运行,垃圾收集程序运行在另一个CPU上。

3.5.3 Parallel Scavenge 收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法的收集器,也是多线程的。
该收集器特点与其他的不同,CMS收集器等是尽可能缩短GC时用户线程停顿时间。
Parallel Scavenge目的是达到一个可控制的吞吐量,吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾回收时间)。高吞吐量可以高效利用CPU时间,尽快完成运算任务。

Parallel Scavenge有两个参数可以精确控制吞吐量,分别是最大GC停顿时间:-XX:MaxGCPauseMillis和直接设置吞吐量大小的:-XX:GCTimeRatio

MaxGCPauseMillis允许值大于0的毫秒数,收集器尽可能GC时间不超过设定值。并不是把值设置越小GC变得更快,GC停顿时间靠牺牲吞吐量和新生代空间换来。
数值小了,新生代空间也小了,GC会变得更频繁,吞吐量也下来了。

GCTimeRatio参数值应大于0且小于100的整数,GC时间占总时间比率,相当于吞吐量的倒数。

3.5.4 Serial Old收集器

Serial Old是Serial的老年代版本,同样是单线程收集器,使用“标记-整理”算法。
主要给client模式下的虚拟机使用。
在Server模式下主要两个用途:在1.5之前配合Parallel Scavenge使用;作为CMS收集器后备方案。

3.5.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。从1.6开始提供的,注重吞吐量和CPU资源敏感的,优先考虑Parallel Scavenge和Parallel Old收集器。

3.5.6 CMS收集器

该收集器以获得最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现的。

整个GC过程有4个步骤:
1. 初始标记(CMS initial mark)
2. 并发标记(CMS concurrent mark)
3. 重新标记(CMS remark)
4. 并发清除(CMS concurrent sweep)

CMS收集器GC过程是和用户线程并发执行的,但有三个缺点
1. 对CPU资源敏感。会导致程序变慢,吞吐量降低,CMS默认回收线程数=(CPU数+3) / 4
2. 无法处理浮动垃圾,出现Concurrent Mode Failure导致Full GC产生。
浮动垃圾就是CMS并发清理时用户线程还在产生新垃圾,这部分标记的新垃圾无法当次就处理掉,只好留到下次
3. CMS基于“标记-清除”算法的收集器,收集结束后会产生大量的碎片。碎片过多,会给分配大对象带来麻烦。可能会提前出发Full GC.
CMS提供了一个开关参数:-XX:UseCMSCompactAtFullCollection默认就是开启的,意思是要Full GC时进行碎片整理。
还有一个-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩GC后进行一次带压缩的,默认0,每次进去都进行带压缩GC。

3.5.7 G1收集器

G1是面向服务端应用的收集器

G1特点
1. 并行和并发:G1充分利用多核多线程的硬件优势,原本其他收集器需要停顿线程的GC动作,G1仍可以并发的让程序继续执行。
2. 分代收集:G1不需要其他收集器就可以独立管理整个GC堆,采用不同方式处理新的对象和存活已久的对象。
3. 空间整合:G1整体上是基于“标记-整理”算法的,但局部两个region之间又是基于“复制”算法的,这些算法都不会产生空间碎片,有利于程序长时间运行。
4. 可预测的停顿:除了低停顿外,使用可以明确指定长度为M毫秒的时间内,GC时间不得超过N毫秒。

G1将整个堆划分为多个大小相等的独立区域Region,虽然保留新生代和老年代,但新生代和老年代不是物理隔离的,都是一部分不需要连续的集合。

G1避免在全区域进行垃圾收集,而是跟踪各个Region里面的垃圾堆积的价值代销,维护一个列表,在允许时间内,优先回收价值最大的region。

G1收集器运作步骤:
1. 初始标记
2. 并发标记
3. 最终标记
4. 筛选回收

3.5.8 GC日志

每种收集器的日志都由自身决定的,每种不一样。
这里写图片描述
“33.125”代表GC发送的时间,数字是虚拟机启动以来经过的秒数。

[GC和[Full GC意思是GC的停顿类型,有Full说明发生了Stop the world。

[DefNew [Tenured [Perm 表示GC发生的区域
如果是Serial收集器,新生代名称叫“Default New Generation”,所以显示[DefNew
如果是ParNew收集器,新生代名称叫[ParNew
如果是Parallel Scavenge收集器,新生代名称叫[PSYoungGen

3324->152K(3712K):GC前该区域已使用容量->GC后该区域使用容量(该区域总容量)

0.0025925 secs 表示该区域GC所占用时间,单位是秒。

3.5.9 垃圾收集器参数总结

参数描述
UseSerialGC虚拟机client模式下默认值,打开后,使用Serial+Serial Old收集器组合回收
UseParNewGC打开此开关后,使用ParNew+Serial Old收集器组合回收
UseConcMarkSweepGC使用ParNew+CMS+Serial Old组合回收内存,Serial Old作为CMS失败后的后备
UseParallelGC虚拟机在Server模式下的默认值,使用Parallel Scavenge+Serial Old组合回收
UseParallelOldGC使用Parallel Scavenge+Parallel Old组合回收
SurvivorRatio新生代中Eden区域和Survivor区域比值,默认为8,Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小,大于这个参数的对象直接到老年代分配
MaxTenuringThreshold晋升到老年代的对象年龄,每个对象在经过一次GC后,年龄就增加1,超过这个值就进入老年代
UseAdaptiveSizePolicy动态调整堆中各区域大小及进入老年代的年龄
HandlePromotionFailure是否允许分配担保失败,老年代空间不足,而新生代所有对象都存活的情况
ParallelGCThreads设置并行GC时进行内存回收的线程数
GCTimeRatioGC时间占总时间比率,仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillisGC最大停顿时间,仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction设置CMS在老年代被使用多少后触发GC,默认68%
UseCMSCompactAtFullCollection设置CMS完成GC后是否进行碎片整理
CMSFullGCsBeforeCompaction设置CMS进行若干次GC后再进行一次碎片整理
PrintGCDetails打印收集器日志参数

3.6.2 大对象进入老年代

典型大对象就是很长的字符串及数组。大对象对虚拟机的内存分配是极不友好的,更糟糕的是一群生命极短的大对象,在写程序时应避免。
经常出现大对象容易导致内存还有很多时就提前触发GC来获得足够连续的空间来放置它们。

PretenureSizeThreshold=3m,设置直接晋升到老年代的对象大小,大于这个参数的对象直接到老年代分配。避免在新生代发生大量的内存复制。超过3m就到老年代

/**
 * VM:-verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
 * -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
 */
public class Test{
    private static final int _1MB = 1024 * 1024;

    public static void test(){
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值