JVM原理解读——垃圾回收

JVM原理解读——垃圾回收


1、垃圾判断算法

1.1、引用计数法(已废弃)

记录对象被引用的次数,当引用指向该对象时计数+1,取消指向该对象时计数-1。当对象被引用次数为0时,判定该对象可回收

引用计数法无法回收循环引用的对象,造成内存泄漏泄漏问题,所以被废弃

1.2、可达性分析

将一些堆外指向堆内的引用(GC Roots)作为根,从根开始探索所有可访问到的对象,并将他们标记为存活,后续再根据不同的垃圾回收算法进行垃圾对象的回收

可作为GC Root的对象包括但不限于:

  • 栈中的引用的对象
  • 方法区中的静态变量和常量
  • 本地方法引用的对象

2、引用类型

2.1、强引用(Strong Reference)

强引用是最普通的引用,只有程序跳出了引用的作用域,或强制将强引用弱化(赋值为null),JVM才会回收它,否则当内存不足时,JVM会抛出OOM异常

Object object = new Object();

2.2、软引用(Soft Reference)

只有在内存不足时,才会回收的引用对象

SoftReference<Object> reference = new SoftReference<>(new Object());

2.3、弱引用(Weak Reference)

只要发生gc,就会被回收

当你希望在不影响对象生命周期的情况下使用对象,你可以使用弱引用

public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        WeakReference<Object> reference = new WeakReference(o);
        o = null;
        System.gc();
        Thread.sleep(100);
        if(reference.get() != null){
            System.out.println("对象还没被回收,做些操作吧");
        }else {
            System.out.println("对象被回收了,就啥也别干了");
        }
    }

2.4、虚引用(Phantom Reference)

随时会被回收,必须与引用队列一起使用,用于在对象回收时做些操作

其实虚引用基本用不到

public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        Object o = new Object();
        WeakReference<Object> reference = new WeakReference(o,queue);
        o = null;
        System.gc();
        Thread.sleep(100);
        if(queue.poll().equals(reference)){
            System.out.println("对象被回收了,做些其他操作");
        }
    }

2、垃圾回收算法

2.1、标记—清除算法

将死亡对象所占内存标记为空闲内存并将内存地址记录空闲链表(free list)中,并在新建对象时分配给它

由于堆内对象的内存必须连续,这种方式会造成内存碎片(浪费),导致堆内总空间足够但是无法分配内存的情况

2.2、标记—整理算法

回收时把所有对象都往空间头部移动,以整理出一段连续的内存空间

解决了内存碎片化的问题,但是对象移动的成本太高

2.3、标记—复制算法

将内存区域分为from和to两部分,其中from指向的区域用来分配内存,垃圾回收时将from区存活对象复制到to区,并交换两个区的指针,保证from永远指向被分配的内存

同样解决了内存碎片化问题,但是内存利用率较低

2.4、分代收集算法

将堆空间分为新生代和老年代,分别执行不同的垃圾回收算法

2.4.1、新生代

主要存放新创建的对象,又被分为Eden区和大小相同的两个Survivor区,两区比例是根据生成对象的速率动态调整的

动态配置对应-XX:+UsePSAdaptiveSurvivorSizePolicy参数,默认启用,也可以通过-XX:SurvivorRatio=设置Eden和Survivor的比例

新生代垃圾回收执行标记—复制算法,将Eden区和Survivor-from的存活对象复制到Survivor-to中,再交换from和to的指针。选择此算法时因为大部分对象存活期较短

新生代的晋升条件:

  • 对象晋升到老年代的复制次数通过-XX:+MaxTenuringThreshold参数设置,默认为15
  • Survivor区的占用达到一定比例时,较高复制次数的对象也会晋升,通过-XX:TargetSurvivorRatio调整,默认50
  • Survivor区不够时,会跳过Survivor区,直接将新生代对象直接晋升到老年代
  • 超出某个大小的对象会直接分配在老年代,通过-XX:PretenureSizeThreshold设置,默认是0,表示首选Eden区分配
  • 动态对象年龄判定,由一些不受JVM参数控制的一些内部组合条件控制,了解即可
2.4.2、老年代

主要存放经历了多次gc依然存活下来的对象,老年代回收选择标记—整理算法

2.4.3、卡表

​ 新生代和老年代的回收时机一般是独立的,而引用实际上是可以是跨代的。新生代可达性分析时,为了找到被老年代引用的新生代且不触发整个老年代的扫描,JVM以512字节为一卡将堆划分,在每个实例变量的写操作时将是否引用新生代的结果维护在卡表中,当新生代可达性分析时,会同时将卡表中的脏卡(引用新生代的对象)放入GC Root中,减少了非脏卡老年代扫描的无谓消耗

​ 卡表虽然解决了跨代引用的问题,但是又会因为并发修改缓存行导致同步阻塞和缓存无效的问题(伪共享),为了根据环境调整卡表的写入方式,可以通过-XX:+UseCondCardMark=true配置JVM在每次写卡表时不写已经标识的卡表,虽然增加了判断开销,但是减少了并发阻塞的开销

2.4.4、线程本地分配缓存区(TLAB)

全称Thread Local Allocation Buffer

为减少多线程下申请Eden区公共空间的锁竞争,线程会在创建时向Eden区申请1%的空间作为自己线程专用的空间,维护一些小对象的生命周期,超过TLAB大小的对象还是会被直接分配在Eden区的公共空间

3、垃圾收集器

垃圾收集器遵循上述的可达性分析算法和分代收集算法

3.1、年轻代垃圾收集器

3.1.1、Serial收集器(了解即可)

单线程gc,导致用户程序频繁暂停,单线程环境下性能良好,一般用在swing这种客户端

3.1.2、ParNew收集器(了解即可)

由多个线程并行收集,提高收集的效率

3.1.3、Parallel Scavenge收集器

jdk1.8默认的新生代收集器,同样多线程并行收集

相比其他垃圾收集器,他更关注控制吞吐量( 运 行 代 码 时 间 / ( 运 行 代 码 时 间 + 垃 圾 收 集 时 间 ) 运行代码时间/(运行代码时间+垃圾收集时间) /(+))而不是降低正常stw的时间

可以通过-XX:MaxGCPauseMillis设置最大停顿时间,默认200ms,通过-XX:GCTimeRatio设置gc时间占总时间的比率(吞吐量的倒数)

通过开启-XX:+UseAdaptiveSizePolicy可以让他动态调节策略

3.2、老年代垃圾收集器

3.2.1、Serial Old收集器(了解即可)

Serial的老年代版本

3.2.2、CMS收集器(了解即可)

在jdk9中已经被deprecated,在jdk14中已经被removed

CMS也操作新生代,只不过对老年代作用更大,所以放在了老年代来说

以降低stw时间为目标,使得用户线程和gc线程可以并行执行,降低stw的延迟感受

很多人将CMS作为一个知识点,但其实他不常用,且有很多劣势,最终导致他被废弃:

  • 由于老年代使用的标记-清除算法会产生内存碎片,在新生代对象晋升或分配担保,且老年代空间不足时,会产生Concurrent Mode Failure,CMS会退化为Serial Old
  • 在gc时需要预留空间来分配浮动垃圾(即gc过程中产生的可回收对象)
  • 占用过多cpu资源
  • 高度可配置,导致学习和使用的成本过大

3.2.3、Parallel Old收集器

jdk1.8默认的收集器,Parallel Scavenge的老年代版本

3.3、G1收集器

全称GarbageFirst,同时作用在年轻代和老年代上,是jdk9以后默认的主流收集器

G1将堆切分成若干区域(Region),并优先回收垃圾最多的Region

年轻代在物理上不再区分Eden和Survivor区

3.3.1、RSet、CSet和dirty card queue

每个Region由若干RSet组成,用来存储“引用了当前Region中的对象的老年代对象”,可以为空

dirty card queue用来缓存脏卡的信息,由专门的GC线程负责收集,异步更新RSet

CSet基于RSet实现,用于存放待回收的对象

3.3.2、回收过程
3.3.2.1、年轻代回收(Young GC)
  1. 扫描根:收集GC Roots和RSet
  2. 更新RSet:由于Region中的RSet是异步更新,导致可达性分析时的RSet不是实时的,需要以dirty card queue为数据源再更新一次
  3. 处理RSet:GC Roots和RSet为根进行可达性分析
  4. 复制回收:使用标记复制算法,将Region中(逻辑上的Eden区)的对象复制到逻辑上的Survivor区,并清理Eden区,同时向老年代晋升一些多次存活的对象
3.3.2.2、混合回收(Mixed GC)

当堆内存占用达到一定比例,混合回收会被启动,比例通过-XX:InitiatingHeapOccupancyPercent调整,默认45%

  1. 初始标记(Initial Mark):启动条件达成时,初始标记阶段等待与年轻代的根扫描一同执行,复用年轻代扫描的GC Roots,只标记GC Roots直接可达的对象,该阶段会产生stw(与年轻代的根扫描共用一段停顿时间)
  2. 根区域扫描(Root Region Scan):扫描Survivor区并标记其中在初始标记阶段标记出来的、引用了老年代对象的对象,该阶段可以和用户线程并行
  3. 并发标记(Concurrent Mark):在初始标记的基础上,进行可达性分析,耗时较长,但是可以和用户线程并行。同时在该阶段,当引用关系发生变化时,G1会使用pre-write barrier将变化记录到队列中,为Remark做准备
  4. 再次标记(Remark):对当前版本的GC Roots再次进行可达性分析,只不过区间是并发标记阶段记录的发生了引用变化的对象。该阶段会stw,以保证再次标记的准确性
  5. 清理(Cleanup):回收全是垃圾对象的Region,有存活对象的region会在下个阶段有选择性地处理
  6. 拷贝存活对象(Evacuation):根据停顿预测模型,限制CSet大小,并选择年轻代的所有region,和老年代中垃圾较多的region执行对应的收集算法,在达到用户规定的停顿时间的同时(-XX:MaxGCPauseMillis),使回收的作用最大化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wheat_Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值