JVM详解——垃圾回收

垃圾回收

在这里插入图片描述

1、创建对象的过程

  1. JVM遇到字节码new指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,检查引用代表的类是否已经被加载、解析和初始化,如果没有就执行类加载
  2. 类加载之后,开始创建对象,未对象分配内存空间。判断该对象内存值是否大于-XX:PetenureSizeThreshold​,若大于则直接分配到老年代,若不大于则判断新生代中Eden区(伊甸园)空间是否充足,若充足则分配到Eden区进行下一步,若不足则进行 Minor GC​ 新生代垃圾回收,然后分配内存空间。
  3. 内存分配完成后虚拟机将成员变量设为零值,之后设置对象头(包括哈希码、GC信息、锁信息、对象所属类的类元信息)
  4. 执行 init ​​方法,初始化成员变量执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

2、对象内存分配的方式

在类加载之后,就开始为对象分配内存,根据对象所需内存大小从堆中划分,分配方式主要有两种:指针碰撞空闲列表

  • 指针碰撞: 如果堆内存规整(没有内存碎片)占用的内存在一边,没有占用的内存放在另一边,中间有一个分界指针,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。
  • 空闲列表: 如何堆内存不规整,JVM需要维护一个列表记录哪些内存可用,再分配时在列表找到一块足够大的内存进行分配,并更新列表记录。

由此可见,选择哪种分配方式取决于堆空间是否规整,堆空间是否规整由垃圾回收器是否带有压缩整理功能决定。比如 SerialParNew垃圾回收期适用指针碰撞,CMS垃圾回收器使用空闲列表。


3、对象内存分配的线程安全问题

创建对象是非常频繁的事情,如果不能保证线程安全,那么会很容易出现多线程操作同一块内存区域,因此保证线程安全对于虚拟机来说至关重要,虚拟机采用两种方式保证线程安全:

  1. CAS + 失败重试: 保证更新操作的原子性
  2. TLAB本地线程分配缓冲: JVM在Eden区为每个线程分配一个私有缓冲区域,在多线程分配内存时,首先在TLAB中进行分配

JVM优先使用TLAB进行对象内存分配,当对象所需内存大于TLAB剩余空间或TLAB内存耗尽时,才会采用CAS+重试进行内存分配。


4、对象在堆中的生命周期

  1. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区。 JVM 会为对象定义一个对象年龄计数器-XX:MaxTenuringThreshold​)

  2. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC

    • JVM 会把存活的对象转移到 Survivor​ 中,并且对象年龄 +1
    • 对象在​ Survivor ​​中同样也会经历 Minor GC​,每经历一次 Minor GC​,对象年龄都会+1,如果超过一定次数就会被放入老年代。
  3. 如果分配的对象超过了-XX:PetenureSizeThreshold​,对象会直接被分配到老年代

  4. 在老年代中存放的对象当进行Major GC时被回收,或者Full GC时被回收。


5、垃圾回收机制

JVM为Java程序提供自动内存管理,包括为对象分配内存回收分配给对象的内存。而垃圾回收机制就是JVM回收分配给对象的内存的过程。

在系统运行的过程中,随着时间的推移一些对象会变成无用的,这些对象占据一定的内存,如果不及时对这些对象占用的内存进行回收再利用,内存会逐渐被耗尽。

垃圾回收机制包括三个方面:判断对象是否可回收、对可回收的对象进行回收、如何更好的进行垃圾回收


6、判断一个对象是否可以回收

在进行垃圾回收时,首先需要判断一个对象是否可以被回收,有两种方法引用计数算法可达性分析算法

  1. 引用计数算法:为对象设置一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器-1,引用计数为0的对象可以被回收。如果两个对象相会引用就会出现循环引用问题,两个对象无法被回收。
  2. 可达性分析算法:基本思想通过一系列 GC Roots对象作为起点,从这些节点开始向下搜索,节点走过的路径成为引用链,当一个对象到GC Roots没有任何引用链时也就是不可达,那么就说明该对象可以被回收。

JVM使用可达性分析算法判断对象是否可以被回收

GC Roots 对象都有哪些?

栈中引用的对象(包括虚拟机栈和本地方法栈)、方法区中类静态属性引用的对象、方法区中常量引用的对象


7、垃圾回收算法

1、标记-清除

标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
在这里插入图片描述

不足:

  • 标记和清除过程效率都不高;
  • 会产生大量的内存碎片,导致无法给之后的大对象分配内存。

2、标记-复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
在这里插入图片描述

主要不足是只使用了内存的一半。

大多虚拟机采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 两个Survivor 的大小比例默认为 8:1:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

3、标记-整理

标记所有不需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述


8、内存分配策略

  1. 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC

  1. 大对象直接进入老年代

大对象是指需要连续内存空间的对象,典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold​,大于此值的对象直接在老年代分配,避免在 Eden 区Survivor 区之间的大量内存复制。

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

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold​ 用来定义年龄的阈值。

  1. 动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到​ MaxTenuringThreshold​ 才能晋升老年代,如果在Survivor​中相同年龄所有对象大小的总和大于 Survivor ​​空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold ​​中要求的年龄。

  1. 空间分配担保

在发生​ Minor GC ​​之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立的话,那么Minor GC​可以确认是安全的。

如果不成立虚拟机会查看 HandlePromotionFailure​ 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC​,如果小于或者​ HandlePromotionFailure ​​设置不允许担保失败,那么就要进行一次 Full GC


9、Full GC 的触发条件

  1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

  1. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  1. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次Full GC

  1. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报​ Concurrent Mode Failure ​​错误,并触发​ Full GC​。


10、故障排查工具

  1. jps : JDK提供的查看当前Java进程的工具
  2. jstack: JDK提供的线程堆栈分析工具,可以查看或导出Java应用程序中线程堆栈信息
  3. jinfo: JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息。
  4. jmap: 生成Java程序的dump文件,也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息
  5. jstat: 虚拟机统计信息监视工具,用于监视虚拟机运行状态信息,可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据。


参考文章:

  1. https://pdai.tech/md/java/jvm/java-jvm-gc.html
  2. https://javaguide.cn/java/jvm/jvm-garbage-collection.html
  3. https://blog.csdn.net/weixin_45629285/article/details/128050932

猜你喜欢

转载自blog.csdn.net/qq_52595134/article/details/129363593