Java 垃圾回收(GC)

简单介绍

当对象不再被使用的,就需要被回收了。当进行回收的时候之前主要考虑的是,哪些对象需要回收,什么时候进行回收,如何回收。

回收区域

JVM 运行时数据区有程序计数器、虚拟机栈、本地方法栈、堆、方法区 5 个区域。其中前三个区域随线程的创建而创建,随线程的消亡而消亡。因此这三个区域的不需要过多的考虑垃圾回收问题。而 Java 堆和方法区则不一样会。堆会存储大量的对象,方法区也会存储类元信息。垃圾收集器所关注的也就是这部分内存,主要是堆内存。

image.png

判断堆内对象是否回收

垃圾收集器在对堆进行回收前,首先需要判断哪些对象还“活着”,哪些已经“死去”。通常判断的堆内存是否回收的方法有引用计数算法、可达性分析算法。

image.png

引用计数算法

引用计数算法给对象中添加一个引用计数器,每当一个地方引用它时,计数器值加 1;当引用失效时,计数器值减 1,如果计数器的值为 0,则说明对象不再被使用(死去了)。然而 Java 虚拟机中并没有选用计数算法来管理内存,因为引用计数算法难以解决对象之间相互循环引用的问题。

image.png

可达性分析算法

可达性分析算法是将一系列称为“GC Roots”的对象作为起始节点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的(也死去了)。其中可作为 GC Roots 对象的有:虚拟机栈(栈帧中本地变量表)中引用的对象,方法去中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象。

image.png

finalization 机制

①. finalize( ) 方法允许在子类中被重写,用于对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等

②. 当垃圾回收器发现没有引用指向一个对象,即:垃圾收集此对象之前,总会先调用这个对象的 finalize( )方法

③. Java 语言提提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义逻辑

image.png

四种引用

强引用

类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

软引用

SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

弱引用

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

判断方法区内存是否回收

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。方法区(HotSpot 中的永久代)的垃圾收集主要回收两部分内容:废弃常量和无用类。

判断一个常量是否是废弃常量只需判断是否还存在对该常量有引用的对象。

而判断无用类需要同时满足 3 个条件:

  1. 该类所有的实例都已经被回收,即 Java 堆中不存在该类的任何实例;
  2. 加载该类的 ClassLoader 已经被回收;
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法(Mark-Sweep 算法)

标记可以回收,统一清除

算法分为两个部分(标记、清除),首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法主要有两个不足:一个是效率问题,标记和清除两个过程的效率都不高;一个是空间问题,标记清除后会产生大量的内存碎片。标记清除算法的执行过程如下图所示:

image.png

复制算法(新生代主要使用)

从 s0 复制存活的到 s1,统一清除 s0

为了解决标记-清除算法效率问题,复制算法将可用内存两等分,每次只使用其中一部分,当使用的部分用完,就将存活的对象复制到令一块中,然后将使用过的部分一次清除。这样避免了内存碎片的额问题,但是内存空间的利用率不高。复制算法执行如下:

image.png

image.png

现在的商业虚拟机都采用这种方法收集新生代,因为新生代中的对象 98% 是“朝生夕死”,所以并不需要按照 1:1 分配内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和用过的 Survivor 空间。如果回收时,Eden 和 Survivor 中还存活的对象空间大于另外一块 Survivor 空间时,这些存活的对象可直接通过分配担保机制进入老年代。

标记-整理算法(老年代主要使用)

标记可回收,整理到一侧,统一清除

复制算法在对象存活率较高时就需要进行较多的复制操作,效率会变低。而且复制算法会浪费 50% 的内存空间。老年代中对象的存活率较高,所以在老年代一般不能直接选用复制算法。根据老年代对象存活率较高的特点,“标记-整理”算法应运而生,该算法首先也是标记所有需要回收的对象,然后将存活的对象都向一端移动,然后直接清理掉端边界以外的内存。如下图所示:

image.png

分代收集算法

该算法并没有新的思想,只是根据对象存活周期的不同将内存划分为几块。一般把 Java 堆分为新生代和老年代,这样就可以根据各年代的特点采用适当的收集算法,对于新生代中大批对象死去的特点,选择复制算法;针对老年代中对象存活率高的特点,使用“标记-清除”或“标记-整理”算法进行回收。

垃圾收集器

image.png

串⾏收集器(Serial Collector)

单线程执⾏所有的垃圾回收⼯作, 进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。适⽤于单核 CPU 服务器

⼯作进程-----|(单线程)垃圾回收线程进⾏垃圾收集 |—⼯作进程继续

image.png

并⾏收集器(Parallel Collector)ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本

⼜称为吞吐量收集器(关注吞吐量), 以并⾏的⽅式执⾏年轻代的垃圾回收, 该⽅式可以显著降低垃圾回收的开销(指多条垃圾收集线程并⾏⼯作,但此时⽤户线程仍然处于等待状态)。适⽤于多处理器或多线程硬件上运⾏的数据量较⼤的应⽤。

⼯作进程-----|(多线程)垃圾回收线程进⾏垃圾收集 |—⼯作进程继续

image.png

并发收集器(Concurrent Collector)ParNew 收集器

以并发的⽅式执⾏⼤部分垃圾回收⼯作,以缩短垃圾回收的暂停时间。适⽤于那些响应时间优先于吞吐量的应⽤, 因为该收集器虽然最⼩化了暂停时间(指⽤户线程与垃圾收集线程同时执⾏,但不⼀定是并⾏的,可能会交替进⾏), 但是会降低应⽤程序的性能

Parallel Scavenge 收集器

这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。

Serial Old 收集器

Serial 收集器的老年代版本,单线程,使用 标记 —— 整理

image.png

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

image.png

CMS 收集器(Concurrent Mark Sweep Collector)

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。并发标记清除收集器, 适⽤于那些更愿意缩短垃圾回收暂停时间并且负担的起与垃圾回收共享处理器资源的应⽤

运作步骤:

初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
image.png

G1 收集器(Garbage-First Garbage Collector)

适⽤于⼤容量内存的多核服务器, 可以在满⾜垃圾回收暂停时间⽬标的同时, 以最⼤可能性实现⾼吞吐量(JDK1.7 之后)

优点:并行与并发、分代收集、空间整合、可预测停顿。

运作步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

image.png

内存分配与回收策略

对象主要分配在新生代(Eden 和 Survivor)的 Eden 区上。少数情况下也可能会直接分配在老年代(Tenured)中,其分配的细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。

对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区中分配,当 Eden 区没有足够的空间进行内存分配时,虚拟机将发起一次 Minor GC(新生代内存回收)。

大对象直接进入老年代

大对象是指需要大量连续内存空间的 Java 对象,如字符串以及数组。虚拟机提供参数-XX:PretenureSizeThreshold 参数来指定大对象,大于该值的对象都是大对象。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活且能被 Survivor 容纳的话,将被移动到 Survivor 空间,并且对象年龄设为 1,对象在 Survivor 区中每经过一次 Minor GC,年龄就增加 1 岁,当年龄增加到一定程度(默认是 15 岁),就会晋升到老年代。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 设置。

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须到达 MaxTenuringThreshold 才能晋升到老年代,如果 Survivor 空间中相同年龄对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有内存空间,如果条件成立,那么 Minor GC 可以确保是安全的。只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。

Minor GC | Major GC | Full GC

Minor GC 发生在新生代
  1. 只针对新生代区域的 GC,指发生在新生代的垃圾收集动作,因为大多数 Java 对象存活率都不高,所以 Minor GC 非常频繁,一般回收速度也比较快
  2. 当 Eden 代满,会触发 minor GC ,Survivor 满不会引发 GC
  3. minor gc 会引发 STW,暂停其他用户线程,等垃圾回收结束,用户线程才能恢复
Major GC 发生在老年代

major GC 是回收老年代的垃圾;major gc 的速度一般比 Minor gc 慢 10 倍以上,STW 时间更长

Full GC
  1. Full GC 就会出现所谓的 STW(stop the world)现象,即所有的进程都挂起等待清理垃圾
  2. Full GC 是回收老年代和年轻代的垃圾
  3. Full gc 是开发调优中尽量避免的,这样暂时时间会短一些
全局 GC(major GC or Full GC):

发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次的 Minor GC(但并不是绝对的)。

Major GC 的速度一般要比 Minor GC 慢上 10 倍以上

垃圾回收器参数

参数描述
-XX:+UseSerialGC启⽤串⾏收集器
-XX:+UseParallelGC启⽤并⾏垃圾收集器,配置了该选项,那么 -XX:+UseParallelOldGC 默认启⽤
-XX:+UseParNewGC年轻代采⽤并⾏收集器,如果设置了 -XX:+UseConcMarkSweepGC 选项,⾃动启⽤
-XX:ParallelGCThreads年轻代及⽼年代垃圾回收使⽤的线程数。默认值依赖于 JVM 使⽤的 CPU 个数
- XX:+UseConcMarkSweepGC(CMS)对于⽼年代,启⽤ CMS 垃圾收集器。 当并⾏收集器⽆法满⾜应⽤的延迟需求是,推荐使⽤ CMS 或 G1 收集器。启⽤该选项后, -XX:+UseParNewGC ⾃动启⽤。
-XX:+UseG1GC启⽤ G1 收集器。 G1 是服务器类型的收集器, ⽤于多核、⼤内存的机器。它在保持⾼吞吐量的情况下,⾼概率满⾜ GC 暂停时间的⽬标。

修改垃圾收集器,在 bin/catalina.sh 的脚本中 , 追加如下配置 :

JAVA_OPTS="-XX:+UseConcMarkSweepGC"

通过 jdk 自带的 j console 查看具体信息

虚拟机运⾏优化(参数调整)

Java 虚拟机的运⾏优化主要是内存分配和垃圾回收策略的优化:

内存直接影响服务的运⾏效率和吞吐量

垃圾回收机制会不同程度地导致程序运⾏中断(垃圾回收策略不同,垃圾回收次数和回收效率都是不同的)

1) Java 虚拟机内存相关参数

参数参数作⽤优化建议
-server启动 Server,以服务端模式运⾏服务端模式建议开启
-Xms最⼩堆内存建议与-Xmx 设置相同
-Xmx最⼤堆内存建议设置为可⽤内存的 80%
-XX:MetaspaceSize元空间初始值
- XX:MaxMetaspaceSize元空间最⼤内存默认⽆限
-XX:NewRatio年轻代和⽼年代⼤⼩⽐值,取值为整数,默认为 2不需要修改
-XX:SurvivorRatioEden 区与 Survivor 区⼤⼩的⽐值,取值为整数,默认为 8不需要修改

参数调整示例

JAVA_OPTS="-server -Xms2048m -Xmx2048m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"

参考文章

Java 垃圾收集机制

内存分配与回收策略

Java 虚拟机(JVM)你只要看这一篇就够了

JVM10_引用计数法

JVM05_堆的概述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值