垃圾回收机制
文章目录
基础
C++ VS JAVA
自动垃圾回收
自动根据对象是否使用由虚拟机来回收对象
- 优点:降低程序员实现难度,降低对象回收 bug 的可能性
- 缺点:程序员无法控制内存回收的及时性
手动垃圾回收
由程序员编程实现对象的删除
- 优点:回收及时性高,由程序员把控回收的时机
- 缺点:便携不当容易出现空指针,重复释放,内训泄露等问题
运行时数据区
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。
而方法中的栈帧在执行完方法之后就会自动弹出栈并释放对应的内存。
方法区的回收
手动触发回收
引用计数法
对每一个对象维护一个引用计数器,当对象呗引用的时候+1,取消引用的时候-1
缺点:
- 每次引用和取消引用的时候都要维护计数器,有一定的性能开销
- 存在循环引用问题,无法回收
查看垃圾回收日志
-verbose:gc
可达性分析算法
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。
如何区分 GC Root 对象?
- 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
- 系统类加载器加载的java.lang.Class对象。
- 监视器对象,用来保存同步锁synchronized关键字持有的对象。
- 本地方法调用时使用的全局对象。
查看 GC Root
四种引用
软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。
在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。
执行过程
- 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
- 内存不足时,虚拟机尝试进行垃圾回收。
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
- 如果依然内存不足,抛出OutOfMemory异常。
弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。
在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。
弱引用对象本身也可以使用引用队列进行回收
虚引用和终结器引用
- 这两种引用在常规开发中是不会使用的。
- 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
- 终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。
垃圾回收算法
核心思想
- 找到内存中存活的对象
- 释放不再存活的对象,程序能再次利用这部分空间
评价标准
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
历史与分类
标记清除算法
标记清除算法的核心思想分为两个阶段:
- 标记阶段,将所有存活的对象进行标记,Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象
- 清除阶段,从内存中删除没有被标记也就是非存活对象
标记清除法的优缺点
优点:
实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可
缺点:
-
碎片化问题
由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元,如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小而无法分配
-
分配速度慢
由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次粗腰遍历到链表最后才能获得合适的内存空间
复制算法
复制算法的核心思想是:
- 准备凉快空间
From 空间
和To 空间
,每次在对象分配阶段,只能使用其中一块空间(From 空间) - 在垃圾回收 GC 阶段,将 From 中存活对象复制到To 空间
- 将两块空间的From 和 To 名字互换
- 将堆内存分割成凉快 From 空间 To 空间,对象分配阶段,创建对象
- GC 阶段开始,将 GC Root 搬运到 To 空间
- 将 GC Root 关联的对象,搬运到 To 空间
- 清理 From 空间,并把名称互换
标记整理算法
也叫标记压缩法,是对标记清理算法中容易产生内存碎片问题的一种解决方案
核心思想分为两个阶段:
- 标记阶段,将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象
- 整理阶段,将存活对象移动到堆的一端,清理掉存活对象的内存空间
优点:
- 内存使用效率高,整个堆内存都可以使用,不会像复制算法只能使用半个内存
- 不会发生碎片化,在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都可以分配对象的有效空间
缺点:
- 整理阶段的效率不高,整理算法有很多种,比如Lisp2整理算法需要对整个堆中的对象搜索3次,整体性能不佳。可以通过Two-Finger、表格算法、ImmixGC等高效的整理算法优化此阶段的性能
分代垃圾回收算法
arthas查看分代之后的内存情况
-
在JDK8中,添加-XX:+UseSerialGC参数使用分代回收的垃圾回收器,运行程序。
-
在arthas中使用memory命令查看内存,显示出三个区域的内存情况。
为什么分代GC算法要把堆分成年轻代和老年代?
-
系统中的大部分对象,都是创建出来之后很快就不再使用可以被回收,比如用户获取订单数据,订单数据返回给用户之后就可以释放了。
-
老年代中会存放长期存活的对象,比如Spring的大部分bean对象,在程序启动之后就不会被回收了。
-
在虚拟机的默认设置中,新生代大小要远小于老年代的大小
主要原因有:
-
可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
-
新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
-
分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。
垃圾回收器的组合关系
垃圾回收起是垃圾回收算法的具体实现
由于垃圾回收器分为年轻代和老年代,除了 G1 之外其他垃圾回收器必须成对组合进行使用
Serial垃圾回收器-年轻代
Serial是一种单线程串行
回收年轻代的垃圾回收器
复制算法
优点:单 CPU 处理器下吞吐量非常出色
缺点:多 CPU 下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程等待很长时间
适用场景:Java 编写的客户端程序或者硬件有限的场景
SerialOld垃圾回收器-老年代
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
-XX:+UseSerialGC 新生代、老年代都使用串行回收器。
优点:单 CPU 处理器下吞吐量非常出色
缺点:多 CPU 下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程等待很长时间
适用场景:与 Serial 垃圾回收器搭配使用,或者在 CMS 特殊情况下使用
ParNew垃圾回收器-年轻代
ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
复制算法
-XX:+UseParNewGC 新生代使用ParNew回收器, 老年代使用串行回收器
优点:多 CPU 处理器下停顿时间较短
缺点:吞吐量和停顿时间不如 G1,所以在 JDK9 之后不建议使用
适用场景:JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
CMS(Concurrent Mark Sweep)垃圾回收器-老年代
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
参数:XX:+UseConcMarkSweepGC
标记清除法
优点:系统由于垃圾回收出现的停段时间较短,用户体验好
缺点:
- 内存碎片问题
- 退化问题
- 浮动垃圾问题
- CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N 参数(默认0)调整N次Full GC之后再整理
- 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收
- 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代
适用场景:大型的互联网系统中用户请求数据量大,平频率高的场景,比如:订单接口,商品接口等
CMS执行步骤:
- 初始标记,用极短的时间标记出GC Roots能直接关联到的对象
- 并发标记, 标记所有的对象,用户线程不需要暂停。
- 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
- 并发清理,清理死亡的对象,用户线程不需要暂停。
线程资源争抢问题
Parallel Scavenge垃圾回收器-年轻代
Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。
复制算法
优点:吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数
缺点:不能保证单次的停顿时间
适用场景:后台任务,不需要与用户交互,并且容易产生大量的对象,比如:大数据的处理,大文件导出
Parallel Old垃圾回收器-老年代
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
参数:
-XX:+UseParallelGC 或
-XX:+UseParallelOldGC可以使用
Parallel Scavenge + Parallel Old这种组合
标记整理算法
优点:并发收集,在多核 CPU 下效率较高
缺点:暂停时间比较长
适用场景:与 Paraller Scavenge 配套使用
G1 垃圾回收器
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。
CMS关注暂停时间,但是吞吐量方面会下降。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
-
支持巨大的堆空间回收,并有较高的吞吐量。
-
支持多CPU并行垃圾回收。
-
允许用户设置最大暂停时间。
JDK9之后强烈建议使用G1垃圾回收器。
内存结构
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
G1垃圾回收有两种方式:
-
年轻代回收(Young GC)
回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
-
混合回收(Mixed GC)
FULL GC
注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
G1 – Garbage First 垃圾回收器
参数1: -XX:+UseG1GC 打开G1的开关,JDK9之后默认不需要打开
参数2:-XX:MaxGCPauseMillis=毫秒值,最大暂停的时间
组合记忆
JDK8及之前:
ParNew + CMS(关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、 G1(JDK8之前不建议,较大堆并且关注暂停时间)
JDK9之后: G1(默认)
从JDK9之后,由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以强烈建议在生产环境上使用G1。