「学习笔记」Java垃圾回收

什么是垃圾回收

我们都很了解对象初始化的重要性,但常常会忘记同样重要的清理工作,因为平时开发中根本感知不到垃圾回收的存在,简单来说,垃圾回收就是帮我们回收那些已经分配出去的堆内存,以便分配给新的对象。它实现了一种高速的、有无限空间可供分配的堆模型。

工作流程

如何知道那些垃圾需要回收?

垃圾回收首先要知道哪些对象是需要被拿出来清理的,有两种垃圾标记的思想,分别是引用计数法可达性分析

引用计数法

引用计数法是一种简单但速度很慢的垃圾回收计数。
它的具体实现是是这样的,每个对象含有一个引用计数器,当有引用指向一个对象时,对象引用计数加1。当引用指向其他对象时,该对象的引用计数减1。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间。

引用计数法有哪些缺点:

  • 需要额外的空间来存储计数器,并且需要维护更新,在整个程序生命周期中将持续发生
  • 不能解决对象之间的循环引用问题(A对象只引用B对象,且B对象也只引用A对象,除此之外没有其他对象引用A、B对象),对于这种情况就会出现——对象应该被回收,但引用计数却不为零。

引用计数常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中。
——引用自《Java编程思想》

可达性分析

可达性分析的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。意思是从一组存活的引用出发,追踪它所引用的对象,然后再找到此对象包含的所有引用,一直进行下去,最后未被探索到的对象就是可回收的对象。

java虚拟机将GC Roots作为初始的存活对象合集,找到所有可达的对象。
那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:

  • Java 方法栈桢中的局部变量;
  • 已加载类的静态变量;
  • JNI handles;
  • 已启动且未停止的 Java 线程。

可达性分析可以解决引用计数法不能解决的对象循环引用问题

stop-the-world

可达性分析在具体实现中有一个需要注意的问题,在垃圾回收时,程序工作线程和垃圾回收线程是多线程执行的环境,假设有这个场景:垃圾回收追溯到A对象引用了B对象,紧接着工作线程将A对象中的引用更新为C对象或者什么对象都不引用了,这个时候就有问题了。如果是把引用赋值为null,最多是这次少回收了B对象。但将引用更新为C对象时,如果C对象已经被回收,就会造成程序异常甚至崩溃。所以为了解决这个问题,就有了stop-the-world。

什么是stop-the-world?
既然垃圾回收在标记回收对象时会出现多线程并发操作的问题,那么在垃圾回收线程执行时让其他线程停止就能解决问题了。这个使程序”停止工作“的方法就被称为stop-the-world

Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。
安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。
举个例子,当 Java 程序通过 JNI 执行本地代码时,如果这段代码不访问 Java 对象、调用 Java 方法或者返回至原 Java 方法,那么 Java 虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。只要不离开这个安全点,Java 虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。

stop-the-world在具体实现时进行了较多的优化,比如上面所说的安全点机制,使程序在垃圾回收时仍能继续执行工程代码。还有减少暂停时间的安全点检测,有兴趣的话可以深入了解。

垃圾回收方式

标记-清除

标记-清除的思想是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象并标记,这个过程不会回收任何对象,只有全部标记工作完成时,清理工作才会开始。
开始清除工作时,会把死亡对象的内存地址记录到空闲列表中,这样当有新建对象时就会从这个列表寻找空闲内存,分配给新建对象。
缺点:

  • 会造成堆内存“空洞”,导致可能堆空间足够,但创建某个大对象申请内存时失败(堆中对象是连续分布的)
  • 内存分配效率低,当堆内存是连续时,分配内存可以用指针加法,而遍历空闲内存列表的方式则有性能损耗
标记-压缩

同样用上面说的方法标记存活对象,当标记工作完成时,将存活对象聚集到内存区域的起始位置,留出一段连续的内存空间,这就是压缩方式的思想。
这种方式能解决内存碎片化的问题,代价是压缩算法的性能开销。

停止-复制

即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。
同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。

根据实际使用中提出的优化——分代回收思想

统计发现,程序实际运行过程中,大部分对象只会存活一小段时间,而存活下来的小部分对象则会存活很长时间。
所以垃圾回收在实际实现时,可以区分处理,Java虚拟机中就用了分代回收的方式。
分代是指将堆空间划分为两代,分别是新生代和老年代,新生代用来存储新建的对象,当对象存活时间够长时,则将其移动到老年代。对不同代可以采用不同的垃圾回收算法,按各代中对象的特点,我们可以让使用的算法达到最好的效果。

新生代的垃圾回收

我们猜测新生代中大部分的 Java 对象只存活一小段时间,那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。

新生代在堆中分为Eden 区,以及两个大小相同的 Survivor 区。

新创建的对象存放在Eden区中,当Eden区的空间耗尽时,会触发一次minor GC,minor GC用复制这个方式进行垃圾回收,因为新生代中的存活对象少,所以需要复制的对象少,这个算法的效果也很好。
存活下来的对象被复制到Survivor区。Survivor有两个区,跟上面说的复制算法中的分区类似,Survivor分为指向对象存储区域的from区和空的to区,第一次minor GC时,Eden区中的存活对象就被复制到from区,当from区空间耗尽时,GC过后的存活对象被复制到to区,同时JVM会标记每次对象存活的次数,当次数达到一定数量(对应虚拟机参数 -XX:+MaxTenuringThreshold)就会将该对象晋升至老年区。
另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。

老年代的垃圾回收

当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收
老年代的垃圾回收发生时,一般会进行全堆扫描,也就是我们说的Full GC,耗时将不计成本

垃圾回收器

待完善

Java1.8中垃圾回收及内存结构

待完善

Reference:《Java编程思想》、极客时间《深入拆解Java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值