VM 内存模型和垃圾回收网络摘要总结

有些知识平常基本不会用到的,当每次去找工作的时候,都会被面试被问到,我想大家都猜到了就是中高级Java程序员一定会问的JVM内存模型和gc算法。想下我每次回答都是模模糊糊,知识点片片段段的,做技术最怕就是这样的,你说我不会吧,我的心又不甘,我确实懂,懂一点点点点🤦‍。看了很多别人写的JVM博客,想写一篇自己学习笔记心得,当我能写出来了,这些知识不单止刻在我的脑子了,还会留在我的心里。本文参考多篇博客内容编写的,如有雷同肯定是抄袭了。

JVM 运行时内存

根据OracleJVM运行的结构的说明,Java虚拟机在定义了在程序执行期间使用的各种运行时数据区域。Run-Time Data Area.png

  • 程序计数器: 记录当前线程正在执行的Java虚拟机指令的地址,每个线程都有自己的程序计数器。
  • Java虚拟机堆栈: 每个Java虚拟机线程都有一个私有Java虚拟机堆栈,与该线程同时创建。它保存局部变量和部分结果,并在方法调用和返回中起作用。
  • 堆: Java虚拟机运行时产生所有的对象和数组的内存区域,所有线程共享的。
  • 方法区: 主要存储每个类的信息包括类的版本、字段、方法、接口和常量池。尽管方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不进行垃圾回收或压缩
  • 运行时常量池: 就是方法区里面常量池,主要是类或者接口的各种常量类型。
  • 本地方法区: 调用系统原生native方法,产生栈内存结构。

以上区域虽然都是单独划分,但是方法区运行时常量池都是在Heap内存中,当存储不足的时候,则Java虚拟机机器抛出一个OutOfMemoryError。可以通过Java启动参数修改默认Heap内存 -xms -xmx 设置最大最小内存。本地方法区属于Java虚拟机堆栈,如果线程中的计算所需的Java虚拟机堆栈超出允许的范围,则Java虚拟机将抛出StackOverflowError。可以通过Java启动参数-xss调整堆栈内存大小。

JVM内存模型

Ukod54.png

  • Young Generation: 翻译叫年轻代,主要有Eden SpaceSurvivor Space组成。
  • Eden Space: 所有新创建对象都是从这里分配内存的。
  • Survivor Space: 幸存区空间,这里包含的对象都是从年轻代垃圾回收或者 Minor gc中幸存下来的,垃圾回收下面会具体说明。
  • Tenured Space: 也叫Old Generation Space 老年代,年轻代的对象经过young gc 或者minor gc 达到最大幸存阈值将被移动到老年代。
  • Meta Space: 元空间,这个是堆外内存,使用本机原生内存。元空间可以独立申请内存空间,不受JVM内存限制。元空间主要用于存储由类加载器加载的类定义,在1.7以前称作Perm Gen Space
  • Code Cache: JVM具有解释器来解释字节码并将其转换为依赖于硬件的机器码。作为JVM优化的一部分,Just In Time(JIT)引入了编译器,经常访问的代码块将由JIT编译为本地代码,并将其存储在代码缓存中。

    回收算法

    下面图片转载一文看懂 JVM 内存布局及 GC 原理
    mark-sweep 标记清除法
    标记算法.png
    将需要回收对象全部标记出来直接清空,优点回收速度快,但是会产生很多内存碎片。
    mark-copy 标记复制法
    标记复制法.png
    将内存分成大小相等的区域,将存活对象复制到新的区域,再将原来整个区域删除,这种算法回收速度快,不会产生内存碎片,内存利用率只能用50%。这种回收算法用于Survivor Space,一个S0区域保存对象,另一个S1保持空闲,当执行标记复制时,幸存对象移动到空闲的S1,S0清除对象变成空闲区域。
    mark-compact 标记 - 整理(也称标记 - 压缩)法
    UALE6J.png
    避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。这种算法主要用于老年代对象回收。

    对象回收判断

    主要通过两个方法去判断对象满足垃圾回收:对象引用计数,对象可达性分析。
  • 对象引用计算 : 给对象添加一个引用计数器,当对象被引用时,计数器+1,引用失效时计数器-1,计数器为0就满足垃圾回收。但是这种方法不能处理循环引用的对象,比如一个对象A引变了对象B,对象B引用了对象A。这样会导致计数器永远不会为0,这时就要使用对象可达性分析来判断了。
  • 对象可达性分析: 通过GC Root作为起点向下遍历,走过的路径作为引用链,以引用链上的对象作为可达性对象。
    UECqQP.png
    当A、B对象没有被GC ROOT所引用,对象是不可达的,会被垃圾回收器清除掉的。

    垃圾回收过程

  • Minor gc: 从年轻代(包括Eden和幸存区)进行垃圾回收的统称。
  • Major gc: 回收”Turn Space”
  • Full gc: 清理整个堆空间包括”Young space”和”Turn Space”一起回收。
    UZ4qiD.png
    如图所有新创建的对象都保存在Eden Space,当空间快满时,JVM就是启动minor gc回收不可达的对象。JVM选择幸存者空间之一作为“ To Space”,比如将S0当作”To Space”。JVM将可访问对象复制到” To Space”(S0),并将可访问对象的年龄增加1。当有大对象生成时,不适合进入幸存区而直接移动到年老代。
    Ue69AJ.png
    在上图中,红色的对象是将被垃圾回收的对象,幸存对象对象复制到 “To Space”,清空”Eden Space”空间。
    UeXj0g.png
    第二次minor gc,”Eden Space”、”To survivor space (S0)”的不可达对象都会被回收,复制幸存对象到S1幸存区中,幸存对象年龄将自增+1,在清空”Eden Space”和”To survivor space (S0)”。
    Uezhvt.png
    没经过一个minor gc,存活下来的对象年龄就会增加1,当对象那年龄达到最大阈值15,对象将会移动到年老代,可通过-XX:MaxTenuringThreshold调整这个阈值。根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了。

总结那些情况会触发Major gc:

  1. 开发者调用System.gc()或者Runtime.getRunTime().gc()JVM启动GC。
  2. 年老代空间不足
  3. 在Minor gc期间,如果JVM无法从伊甸园或幸存者空间中回收足够的内存,则可能会触发Major GC。
  4. 如果我们为JVM设置了“ MaxMetaspaceSize”选项,但没有足够的空间来加载新类,则JVM会触发一个Major GC。

参考资料
https://www.infoq.cn/article/3WyReTKqrHIvtw4frmr3
https://dzone.com/articles/understanding-the-java-memory-model-and-the-garbag
https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc