JVM笔记(三)垃圾收集器与内存分配策略

程序计数器、虚拟机栈、本地方法栈这三个区域在线程运行时每个栈帧所分配的内存,在类结构确定下来时就已经确定。而java堆和方法区只有在程序运行时才知道创建哪些对象,内存分配和回收是动态的

一、怎样判断对象已死

1.引用计数算法

给每个对象分配一个引用计数器,对象每多一个引用计数器加1,损失一个引用计数器减1。缺点就是它无法解决对象之间相互引用的问题,例如:objA.instance=objB及objB.instance=objA,这种情况对象虽然无法再访问,但是计数器却不为0,对象无法回收。
优点是实现简单,判定效率高,使用案例为微软的COM、FlashPlayer、python语言等

2.根搜索算法(可达性算法)

通过一系列名为“GC Roots”的对象作为起点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,为可回收对象。
可作为GC Roots的对象包括下面几种:
(1) 虚拟机栈(栈帧中的本地变量表)中的引用对象
(2) 方法区中statisc修饰的类静态属性引用的对象
(3) 方法区中的常量引用的对象
(4) 本地方法栈中JNI(即一般说的Native方法)的引用对象

注:当一个对象如果没有到GC Roots的引用链时,并不一定死亡,如果该对象此时执行了finalize()方法并重新与引用链上的任何一个对象建立关联,例如把自己(this关键字)赋值给某个类变量或对象的成员变量,它将会被移出“即将回收”的集合。如果对象没有覆盖finalize()方法或已经执行过,该对象都无法逃脱。

代码演示如下:

/*** 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 */

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

输出结果如下:
在这里插入图片描述

二、垃圾收集算法

1.标记-清除算法

分为标记和清除两个过程。
缺点:
(1)标记和清除的效率都不高
(2)标记清除之后产生大量不连续的内存碎片,当以后分配较大对象时内存不 足不得不提前触发另一次垃圾回收
在这里插入图片描述

2.复制算法

该算法将内存分为大小相等的两块,每次只使用一块,当一块用完时将活着的对象复制到另一块,然后一次性清除掉使用的内存块。这种方式不用考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效,-XX:PretenureSizeThreshold
(例如:3M),设置此参数可以使大于该设置的对象直接进入老年代,避免Eden区以及两个Survivor之间大量的内存拷贝
在这里插入图片描述
缺点:内存缩为原来的一半,代价较高
复制算法在HotSpot的实现:根据新生代对象的特性(朝生夕死,存活率比较低),将内存分为Eden区、toServior区、fromServior区,大小比例为8:1:1,即每次新生代可使用内存为90%(80%+10%),10%为预留空间。当存活对象多于10%时,即如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代
图示流程如下:
在这里插入图片描述
分配担保机制:
每次Minor GC时,虚拟机都会检测晋升老年代对象的平均大小是否大于老年代的剩余空间大小,如果大于则改为直接进行一次Full GC,如果小于,会看HandlePromotionFailure设置是否允许担保失败,如果允许,则进行Minor GC,否则直接进行Full GC。HandlePromotionFailure最好还是要打开,避免Full GC过于频繁。

3.标记-整理算法

基于老年代对象存活率较高的特点,以及解决标记清除空间碎片的缺点提出标记-整理算法,处理过程是先对可回收对象进行标记,然后存活对象向一边移动,最后清除可回收对象。图示如下:
在这里插入图片描述

三、垃圾收集器

目前垃圾收集器分为七种,以及他们之间的组合如下图所示
在这里插入图片描述

1 Serial收集器

这是一个单线程收集器,它的最大缺点是在进行垃圾收集时要暂停所有的用户线程,其工作原理如下:
在这里插入图片描述
Serial简单而高效,对于单线程环境来说没有线程交互的开销,单线程收集效率较高,对于运行在Client模式下的虚拟机是一个很好的选择。

2. ParNew收集器

是基于Serial的多线程版本,在控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial一样,其工作原理如下:
在这里插入图片描述
它是在Server模式下首选的收集器,除了Serial外只有它能与CMS配合工作,XX:+UserConcMarkSweepGC选项选定后默认的新生代收集器,或者-XX:+UserParNewGC强制指定
缺点:在单CPU环境下垃圾收集效率很低,不如Serial,在多CPU环境下默认开启收集线程数与CPU个数一致。-XX:ParallelGCThreads 限制垃圾收集线程数

3. Parallel Scavenge收集器

该收集器的关注点是提高吞吐量(吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),以最高效率的利用CPU时间,尽快完成程序运算任务,适合交互较少的任务。
+XX:MaxGCPauseMillis :垃圾收集最大停顿时间(大于0的毫秒数),以牺牲吞吐量和新生代空间换取
+XX:GCTimeRatio : 设置吞吐量大小(0到100的整数),垃圾收集时间占总时间的比率,默认99。
GC自适应的调节策略:-XX:+UserAdaptiveSizePolicy,打开这个参数之后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数以提供合适的停顿时间和最大吞吐量,需要设置最大内存(-Xmx),设置优化目标最大停顿时间(+XX:MaxGCPauseMillis)或吞吐量(+XX:GCTimeRatio)

4.Serial Old收集器

老年代单线程收集器,采用标记-整理
目前在Client模式下虚拟机使用,在Server模式下作为CMS收集器的后备预案

5.Parallel Old收集器

使用多线程标记-整理算法的老年代版本,目前主要与Parallel Scavenge组合,收集以吞吐量优先,对CPU要求比较敏感。原理如下:
在这里插入图片描述

6.CMS收集器

(1) 关注最短停顿时间、响应速度、用户体验,目前主要应用在互联网或B/S系统的服务端。采用标记-清除算法,实现了用户线程与垃圾收集线程同时工作
过程如下:
(1) 初始标记:Stop The World,标记GC Roots能直接关联到的对象,速度很快
(2) 并发标记:GC Roots Tracing
(3) 重新标记: 修正并发标记中因程序运行而导致标记变动的标记记录,比初始标记耗时长,比并发标记耗时短
(4) 并发清除:耗时最长的并发标记和并发清除中,收集线程与用户线程一起并发运行
其运行示意图如下:
在这里插入图片描述
缺点:
(1) CMS对CPU资源非常敏感,默认启动的回收线程数为(CPU数量+3)/4,即CPU不足4个时CMS对用户线程影响很大
(2) CMS无法收集浮动垃圾(并发清除阶段程序运行产生的垃圾),需要在老年代预留部分空间,默认剩余68%时CMS激活,可通过-XX:CMSInitiatingOccupancyFraction设置,如果预留内存无法满足CMS就会退回至Serial Old。
(3) 产生大量的空间碎片,-XX:+UseCMSCompactAtFullCollection Full GC之后进行一次碎片整理(非并发),-XX:CMSFullGCsBeforeCompaction设置多少次Full GC之后进行一次碎片整理,碎片整理会导致停顿时间加长。

7.G1收集器

是一个基于标记-整理算法的全代垃圾收集器
优势:
(1) 不会产生空间碎片
(2) 可以精确的控制停顿时间,可以指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
(3) 在不牺牲吞吐量的前提下低停顿回收垃圾
(4) 全代收集,将java堆划分为多个大小固定的独立区域,并划分优先级,在允许收集的时间内优先收集垃圾最多的区域。

学习资料:《深入理解Java虚拟机:JVM高级特性与最佳实践》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值