深入了解jvm回收机制

目的

主要介绍java 垃圾回收如何与hotspot JVM配合使用的基础知识。在了解了垃圾收集器的功能后,了解Visual VM监控垃圾收集的过程。

探索JVM架构

Hotspot 架构

HotSpot JVM 的架构支持强大的特性和能力基础,并支持实现高性能和大规模可扩展性的能力。例如,HotSpot JVM JIT 编译器生成动态优化。换句话说,它们在 Java 应用程序运行时做出优化决策,并生成针对底层系统架构的高性能本机机器指令。此外,通过其运行时环境和多线程垃圾收集器的成熟演变和持续工程,HotSpot JVM 即使在最大的可用计算机系统上也能产生高可扩展性。


Hotspot 架构

关键组件

在调整性能时,JVM 有三个组件是重点。堆是存储对象数据的位置。然后,此区域由启动时选择的垃圾回收器管理。大多数优化选项都与调整堆大小和根据您的情况选择最合适的垃圾回收器有关。JIT 编译器对性能也有很大的影响,但很少需要使用较新版本的 JVM 进行调优。


Key Hotspot Components

垃圾回收

什么是自动垃圾回收?

自动垃圾回收是查看堆内存、识别哪些对象正在使用以及哪些对象未使用以及删除未使用对象的过程。正在使用的对象或引用的对象意味着程序的某些部分仍保留指向该对象的指针。未使用的对象或未引用的对象不再被程序的任何部分引用。因此,可以回收未引用对象使用的内存。

在像C这样的编程语言中,分配和释放内存是一个手动过程。在 Java 中,释放内存的过程由垃圾回收器自动处理。基本过程分为以下几步:

1. 标记

标记的作用是垃圾回收器识别哪些内存正在使用和哪些未使用。
引用的对象以蓝色显示;未引用的对象以金黄色显示。
在标记阶段,所有对象将被扫描,并被标记。如果必须扫描系统中的所有对象,这可能是一个非常耗时的过程。


标记

2. 正常删除

正常删除会删除未引用的对象,留下引用的对象和指向可用空间的指针。
内存分配器保存对可用空间块的引用,可以在其中分配新对象。


正常删除

使用压缩进行删除

若要进一步提高性能,除了删除未引用的对象外,还可以压缩其余引用的对象。通过将引用的对象移动到一起,这使得新的内存分配更加容易和快捷。


压缩删除

为什么要进行按“代”进行垃圾回收?

通过前面的描述,我们可以了解到标记和压缩jvm中所有的对象是低效的、耗时的。随着分配的对象越来越多,对象的列表会越来越长,从而导致垃圾回收的时间越来越长。然而通过对应用程序的实际分析发现,大部分对象的使用时间都是短暂的。
下面是此类数据的示例,Y 轴显示分配的字节数,X 访问显示随时间分配的字节数。


image.png

如您缩减,随着时间的推移,保留分配的对象越来越少。所以实际上,大多数的对象寿命非常短,正如图左侧的较高值所示。

JVM年龄代

JVM代分布

堆空间包括:年轻代、年老代、永生代。
年轻代(eden) 是分配和老化所有新生对象的地方。当年轻代被填满时,会导致小的垃圾回收。
S0S1是两个大小相同的内存区域,主要存放每次垃圾回收后eden区存活的对象,作为对象从eden过渡到年老代的缓冲地带。
年老代主要存放生命周期长的存活对象。通常为年轻代设置阈值,当达到该年龄后,对象将被移动到年老代。
永生代 主要存放所有已加载的类信息、方法信息、常量池等。可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。

对象分配和老化的过程

  1. 任何新对象都会被分配到eden区,S0、S1开始都是空的


    image.png
  2. 当eden区被填满时,将会触发一次小的垃圾回收


    image.png
  3. 此时被引用的对象将会被移动到S0,未被引用的对象将被删除


    image.png
  4. 在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。然而在survivor区,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象


    image.png
  5. 再下一次Minor GC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。


    image.png
  6. 下面演示一下Promotion过程,再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(可通过参数配置,默认是8),就会被从年轻代Promotion到老年代。


    image.png
  7. 随着Minor GC一次又一次的进行,不断会有新的对象被promote到老年代。
    随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。

  8. 上面基本上覆盖了整个年轻代所有的回收过程。最终,Major GC将会在老年代发生,老年代的空间将会被清除和压缩。


    image.png

总结

从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。
关于方法区(共享内存区的持久代)即永久代的回收,永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  • 类的所有实例都已经被回收
  • 加载类的ClassLoader已经被回收
  • 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。

你可能感兴趣的:(深入了解jvm回收机制)