【java】---JVM内存模型以及垃圾回收算法

时间:2022-02-06 00:00:30

【前言】

     上篇文章的最后了解了一下java内存模型以及垃圾回收机制,并没有深入的去了解,这篇文章中我们总结一下.

【正文】

      在本文中,我使用了JDK8 Oracle Hot Spot 64位JVM。首先让我描述可用于Java进程的不同内存区域。

【java】---JVM内存模型以及垃圾回收算法

一旦我们启动JVM,操作系统就为进程分配内存。这里,JVM本身是一个进程,分配给该进程的内存包括堆,元空间,JIT代码缓存,线程堆栈和共享库。我们称之为本机内存。“ 本地内存 ”是由操作系统提供给进程的内存。操作系统分配给Java进程的内存大小取决于操作系统,处理器和JRE。让我来解释一下JVM可用的不同的内存块。

  Heap Memory:

         JVM使用这个内存来存储对象。这种记忆又分为两个不同的领域,称为"Young Generation"和"Tenured Space".

Young Generation:

        新空间分为两个部分,分别是"Eden Space"和"Survivor Space".

Eden Space:

       当我们创建一个对象时,内存将从Eden分配。

Survivor Space:

        包含Young GC或 Minor GC中幸存的对象。我们有两个等分的Survivor 空间S0和S1。

Tenured Space:

        在MinorGC 或者young GC收集期间,如果对象的大小已经是Tenured 空间的大小,将会被移入"Tenured "或者"老年代".


当我们在讨论垃圾回收过程时,我们将会知道如何使用上述内存空间.


Meta Space:

       这个内存不在堆内存中,是本机内存的一部分。根据默认的文档,元空间没有上限。在早期的Java版本中,我们称之为“ Perm Gen Space ”,  这个空间用于存放由类加载器加载的类定义,这种设计是为了避免内存错误而增长的,然而,如果它的增长超过了可用的物理内存,那么操作系统将使用虚拟内存,这将对应用程序的性能产生不利的影响,因为将数据从虚拟内存交换到物理内存,反之亦然,这是一个代价高昂的操作,我们有JVM选项来限制元空间由JVM使用,在这种情况下,我们可能会出现内存不足错误。

Code Cache:

      JVM有一个解释器来解释字节代码并将其转换成硬件相关的机器代码。作为JVM优化的一部分,已经引入了Just In Time(JIT)编译器。经常访问的代码块将被JIT编译为本地代码,并将其存储在代码缓存中。JIT编译的代码将不会被解释。

垃圾回收算法

       现在让我们来讨论垃圾收集过程。JVM使用单独的后台线程来进行垃圾收集。如上所述,当应用程序创建对象时,JVM尝试从eden空间获取所需的内存。JVM执行GC作为次要GC和主要GC。让我们了解一下Minor GC。

【java】---JVM内存模型以及垃圾回收算法


     最初的Survivor 和Tenured 是空的。当JVM无法从eden空间获取内存时,它启动次要的GC。Minor GC期间,不可达的对象被标记为收集。JVM选择其中一个存活空间作为“To Space”。可能是S0 / S1。让我们说JVM选择S0作为“To Space”。JVM将可达对象复制到“To Space”,并将可达对象的年龄递增1.不适合Survivor 的对象将被移动到Tenured 。 这个过程被称为“ premature promotion”。由于这个原因,我已经使“To Space”大于分配的空间。记住Survivor 的空间不会增长。

1. 标记 -清除算法

    “标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

 【java】---JVM内存模型以及垃圾回收算法

2. 复制算法

“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

【java】---JVM内存模型以及垃圾回收算法

3.标记-压缩算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

【java】---JVM内存模型以及垃圾回收算法

4.分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。