垃圾回收

文章目录

垃圾回收

Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。

如果需要手动触发垃圾回收,可以调用System.gc()方法。

方法区回收

方法区中能回收的内容主要就是不再使用的类。

判定一个类可以被卸载。需要同时满足下面三个条件:

  • 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
  • 加载该类的类加载器已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用。

堆回收

引用计数法

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。

只有无法通过引用获取到对象时,该对象才能被回收。

引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法

但是它也存在缺点:

  • 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响 。
  • 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。

可达性分析算法

Java使用的是可达性分析算法来判断对象是否可以被回收。

可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。

GC Root对象:

  • 线程Thread对象。
  • 系统类加载器加载的java.lang.Class对象。
  • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  • 本地方法调用时使用的全局对象。

五种对象引用

可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。

除了强引用之外,Java中还设计了几种其他引用方式。

软引用

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软 引用中的数据进行回收。

软引用的执行过程如下:

  1. 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
  2. 内存不足时,虚拟机尝试进行垃圾回收。
  3. 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
  4. 如果依然内存不足,抛出OutOfMemory异常。
java 复制代码
byte[] bytes = new byte[1024 * 1024];
// 将1m的数据放入软引用中
SoftReference<byte[]> softReference = new SoftReference<>(bytes);

SoftReference提供了一套队列机制来回收SoftReference对象本身:

  1. 软引用创建时,通过构造器传入引用队列
  2. 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
  3. 通过代码遍历引用队列,将SoftReference的强引用删除
弱引用

弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。

在JDK 1.2版之后提供了WeakReference类来实现弱引用

弱引用主要在ThreadLocal中使用。

弱引用对象本身也可以使用引用队列进行回收。

虚引用

虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。

Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

终结器引用

终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。

在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。

垃圾回收算法

Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。

评价标准

1)吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。

比如:虚拟机总共运行了 100 分钟,其中GC花掉 1 分钟,那么吞吐量就是 99%

2)最大暂停时间:最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。

3)堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。

标记-清除算法

标记清除算法的核心思想分为两个阶段:

1)标记阶段:将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。

2)清除阶段:从内存中删除没有被标记也就是非存活对象。

优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

缺点:

  • 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
  • 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法

复制算法的核心思想是:

1)准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。

2)在垃圾回收GC阶段,将From中存活对象复制到To空间。

3)将两块空间的From和To名字互换。

优点:

  • 吞吐量高:复制算法只需要遍历一次存活对象复制到To空间即可,比标记-整理 算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法, 因为标记清除算法不需要进行对象的移动
  • 复制算法在复制之后就会将对象按顺序放入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。

缺点:每次只能让一半的内存空间来为创建对象使用。

标记整理算法

标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。 核心思想分为两个阶段:

1)标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象。

2)整理阶段,将存活对象移动到堆的一端。清理掉存活对象的内存空间。

优点:

  • 内存使用效率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存。
  • 不会发生碎片化:在整理阶段可以将对象往内存的一侧进行 移动,剩下的空间都是可以分配对象的有效空间。

缺点:

  • 整理阶段的效率不高:整理算法有很多种,比如Lisp2整 理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能。
分代垃圾回收算法

现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收 算法(Generational GC)。

分代垃圾回收将整个内存区域划分为年轻代和老年代

分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。

随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为 Minor GC或者Young GC。 Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。

接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。 此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。

注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。

如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。 当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。 如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。

垃圾回收器

由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。

G1垃圾回收器

JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。

Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。

CMS关注暂停时间,但是吞吐量方面会下降。

而G1设计目标就是将上述两种垃圾回收器的优点融合:

  • 支持巨大的堆空间回收,并有较高的吞吐量。
  • 支持多CPU并行垃圾回收。
  • 允许用户设置最大暂停时间。

G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、 Old区。

Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其 中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。

1)年轻代回收:回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地 保证暂停时间。

  • 新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行 Young GC。
  • 标记出Eden和Survivor区域中的存活对象。
  • 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
  • 后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
  • 当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
  • 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是 4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。

G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。

比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。

2)混合回收:多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时 (-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成

混合回收分为:初始标记(initial mark)、并发标记(concurrent mark)、最终标记(remark或者Finalize Marking)、并发清理(cleanup)

对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。

最后清理阶段使用复制算法,不会产生内存碎片。

注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法, 此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。

参数1: -XX:+UseG1GC 打开G1的开关, JDK9之后默认不需要打开

参数2:-XX:MaxGCPauseMillis=毫秒值 最大暂停的时间参数

相关推荐
likuolei24 分钟前
XQuery 完整语法速查表(2025 最新版,XQuery 3.1)
xml·java·数据库
雨中飘荡的记忆29 分钟前
LangChain4j 实战指南
java·langchain
okseekw31 分钟前
Java 中的方法:从定义到重载的完整指南
java
雨中飘荡的记忆31 分钟前
深入理解设计模式之适配器模式
java·设计模式
用户849137175471633 分钟前
生产级故障排查实战:从制造 OOM 到 IDEA Profiler 深度破案
java·jvm
雨中飘荡的记忆36 分钟前
深入理解设计模式之装饰者模式
java·设计模式
雨中飘荡的记忆40 分钟前
秒杀系统设计与实现
java·redis·lua
CryptoPP1 小时前
使用 KLineChart 这个轻量级的前端图表库
服务器·开发语言·前端·windows·后端·golang
18你磊哥1 小时前
chromedriver.exe的使用和python基本处理
开发语言·python
小坏讲微服务1 小时前
Spring Cloud Alibaba 整合 Scala 教程完整使用
java·开发语言·分布式·spring cloud·sentinel·scala·后端开发