写点什么

深入理解 JVM:内存管理与垃圾回收机制探索

作者:乘云 DataBuff
  • 2024-07-08
    浙江
  • 本文字数:3838 字

    阅读完需:约 13 分钟

深入理解JVM:内存管理与垃圾回收机制探索

1. JVM 简介

  • JVM 是什么?


Java 虚拟机(JVM)是 Java 程序的运行环境,它负责将编写的 Java 字节码转换为特定操作系统上的机器指令,并管理程序的运行时环境。简而言之,JVM 是 Java 跨平台特性的基石。


  • JVM 的重要性


JVM 允许 Java 程序在不同的硬件和操作系统上无缝运行,无需重新编译。它不仅管理内存分配、垃圾回收等底层任务,还通过即时编译(JIT)提升运行效率,确保了 Java 应用的高性能与可移植性。

2. JVM 结构图


  • 类加载子系统:负责查找并加载类文件的字节码,转换成 JVM 能理解的数据结构,并存储在方法区中。包括类的加载、验证、准备、解析和初始化等阶段。

  • 运行时数据区域:主要包括堆、栈、方法区、运行时常量池和本地方法栈。

  • 方法区:存储已被加载的类信息、常量、静态变量等。

  • 运行时常量池:存放编译期生成的各种字面量和符号引用。

  • 堆:存放几乎所有的对象实例。

  • 虚拟机栈:JVM 为每个线程创建的,用来存储 Java 方法调用的状态信息,包括局部变量、操作数栈、动态链接和方法返回地址等。它是线程私有的,遵循后进先出(LIFO)原则。

  • 本地方法栈:为 JVM 使用到的 Native 方法服务。

  • 程序计数器:每个线程私有的,它记录了当前线程执行的字节码指令的地址,用于控制程序流程,实现指令间的跳转和调用返回,是线程切换和异常处理的关键部件。

  • 执行引擎:负责解释或编译字节码,执行 Java 程序。包含解释器、即时编译器(JIT)等组件,实现高效代码执行。

  • 本地方法接口(JNI):一种标准的编程接口,它定义了 Java 代码如何调用用其他编程语言(如 C、C++)编写的方法的规则。

  • 本地库(Native Libraries):一组用非 Java 语言编写的代码库,它们通常以动态链接库(如 Windows 的.dll 文件,Linux 的.so 文件,macOS 的.dylib 文件)的形式存在。

3. 内存模型演变


  • JDK 1.6: JVM 的内存模型包含了一个被称为永久代(PermGen)的区域,它用于存储类的元数据、静态变量以及 JVM 内部的数据结构。这个区域是在 JVM 启动时创建的,并且它的大小是固定的,不会因为 JVM 的运行而改变。

  • JDK 1.7:虽然永久代仍然存在,但是 JVM 开始逐渐减少对永久代的使用。字符串常量池和静态变量被移动到了堆上。这是因为永久代的空间有限,如果永久代满了,就会触发 Full GC,这会严重影响系统的性能。因此,将这些数据移动到堆上,可以更好地管理这些数据的生命周期,并且避免频繁的 Full GC。

  • JDK 1.8 及之后:永久代被完全移除,取而代之的是元空间(Metaspace)。元空间并不在 Java 堆中,而是在本地内存中。这意味着元空间的大小只受本地内存的限制。运行时常量池和类常量池都被移动到了元空间。但是,字符串常量池仍然在堆上,这是因为字符串常量池中的数据生命周期和普通的 Java 对象一样,更适合放在堆上管理。


总的来说,从 JDK 1.6 到 1.8,最大的变化是永久代被元空间取代,这改善了内存管理和 GC 性能,减少了内存溢出的风险,并为开发者提供了更好的内存使用透明度。

4. JVM 垃圾回收

  • 什么是垃圾回收?*自动管理内存的过程,回收不再使用的对象所占的内存空间,防止内存泄露。

  • 垃圾回收如何工作?*通过可达性分析判断对象是否可达,不可达的对象被视为垃圾。常见的算法包括标记-清除、复制、标记-压缩等。

4.1 垃圾回收器的类型及组合方式


(红色虚线)在 jdk8 时将这两个组合声明为废弃,并在 jdk9 中完全取消;**(绿色虚线)在 jdk14 中废弃(绿色虚线)jdk14 中,删除 CMS 垃圾收集器**


  • 新生代收集器:


ParNew(Serial 收集器的多线程版本,采用的也是复制算法。可通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。命令参数:-XX:+UseParNewGC)。


Serial(最基本、最古老的垃圾回收器,它在进行垃圾回收时会暂停所有的应用线程,适用于单核处理器的环境以及对延迟不敏感的应用。参数命令:-XX:+UseSerialGC)。


Paralel Scavenge(并行收集的多线程收集器,采用的是复制算法。通过设置参数命令,达到可控制吞吐量(Thoughput ,CPU 用于运行用户代码的时间/CPU 总消耗时间)ParallelScavenge 是可以保证新生代的吞吐量优先,但是不能保证整体吞吐量。参数命令:-XX:+UseParallelGC。


  • 老年代收集器:


CMS(多线程并发的收集器,基于标记清除算法。参数命令:-XX:+UseConcMarkSweepGC)。


Serial Old GC(单线程串行的收集器,是基于标记-整理算法。-XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器)。


Parallel Old GC(多线程并发的收集器,基于标记整理算法。参数命令:-XX:+UseParallelOldGC)。


G1(一种全新的垃圾收集器,它既可以用于新生代,也可以用于老年代。G1 收集器将堆内存划分为多个小块,可以并行和并发地进行垃圾收集。G1 收集器主要用于大堆内存的环境,它的目标是实现高吞吐量和可预测的停顿时间。参数命令:-XX:+UseG1GC)。

4.2 JVM 垃圾回收算法

  • 标记清除算法


它的基本思想是分两个阶段进行:标记阶段和清除阶段。



1.标记阶段:在标记阶段,垃圾回收器会遍历所有的对象,对每个对象进行检查。如果一个对象被其他对象引用,或者是根对象(例如全局变量或者栈上的变量),那么这个对象就被标记为“存活”的。


2.清除阶段:在清除阶段,垃圾回收器会再次遍历所有的对象,如果一个对象没有被标记为“存活”的,那么这个对象就被视为垃圾,垃圾回收器会回收这个对象所占用的内存。


标记-清除算法的优点是实现简单,且可以处理循环引用的情况。但是它的缺点是在清除阶段会产生大量的内存碎片,这可能会导致后续的内存分配变得困难。此外,标记-清除算法需要暂停应用程序来进行垃圾回收,可能会导致应用程序的响应时间增加。


  • 复制算法


复制算法是为了解决标记清除的效率问题。


它的基本思想是将可用内存分为两个相等的部分,每次只使用其中的一半。当这一半的内存用完时,就将还在使用的对象复制到另一半,然后再把已使用的内存清空,用于下一次的内存分配。



复制算法的步骤如下:


1.将堆内存分为两个相等的部分,只在其中一个部分(称为 From 区域)上进行内存分配。


2.当 From 区域的内存用完时,启动垃圾回收过程。遍历 From 区域中的所有对象,检查它们的引用。如果一个对象被引用,则将它 3.复制到另一个区域(称为 To 区域)。


4.复制过程中,会更新所有指向被复制对象的引用,使它们指向新的位置。


5.复制完成后,交换 From 区域和 To 区域的角色。原来的 From 区域现在变成了 To 区域,将被清空并用于下一次的内存分配。


复制算法的优点是实现简单,且没有内存碎片。但是它的缺点是需要两倍的内存空间,且复制过程需要暂停应用程序,可能会导致应用程序的响应时间增加。这种算法通常用于新生代的垃圾回收,因为新生代的对象大多数都是“朝生暮死”的,只有少数对象会存活下来,所以复制算法的效率较高。


  • 标记整理算法


它的基本思想也是分两个阶段进行:标记阶段和整理阶段。



1.标记阶段:在标记阶段,垃圾回收器会遍历所有的对象,对每个对象进行检查。如果一个对象被其他对象引用,或者是根对象(例如全局变量或者栈上的变量),那么这个对象就被标记为“存活”的。


2.整理阶段:在整理阶段,垃圾回收器会移动所有存活的对象,使它们在内存中连续排列,然后直接回收边界以外的内存。


标记-整理算法的优点是可以有效地处理内存碎片问题,因为它会将存活的对象集中到内存的一端。但是,这种算法的缺点是需要移动对象,这会增加垃圾回收的开销,并且需要暂停应用程序来进行垃圾回收,可能会导致应用程序的响应时间增加。


  • 分代收集算法


它基于一个观察:大部分对象在内存中存在的时间很短。这种策略将 Java 堆分为两个或更多的部分,每个部分称为一个代(Generation)。常见的有新生代(Young Generation)和老年代(Old Generation)。


新生代通常包含新创建的对象。当新生代满了,垃圾回收器就会清理新生代中不再使用的对象,这个过程称为 Minor GC。如果一个对象在 Minor GC 后仍然存活,那么它就会被移动到老年代。 老年代包含长时间存活的对象。只有当老年代满了,垃圾回收器才会清理老年代中不再使用的对象,这个过程称为 Major GC 或 Full GC。


分代收集的优点是可以高效地回收短生命周期的对象,同时减少了对长生命周期对象的回收频率。这种策略在处理大量短生命周期的对象时特别有效,例如在处理 HTTP 请求或者 GUI 事件时创建的临时对象。 在 Java 中,新生代通常使用复制算法进行垃圾回收,而老年代通常使用标记-清除-整理算法进行垃圾回收。这样可以兼顾垃圾回收的效率和内存的利用率。

4.3 JVM 垃圾回收过程


  1. 创建新对象先放到 Eden 区空间

  2. 当 Eden 区装满时,会对 Eden 区进行垃圾回收(Minor GC),在 Eden 区实现清除策略,没有被引用的对象直接被回收,再加载新的对象放到 Eden 区。

  3. Eden 区中依然存活的对象会被移送到 Survivor 区。

  4. 在每次 Minor GC 后,存活的对象会被从当前使用的 Survivor 区域(假设是 Survivor 0)转移到另一个 Survivor 区域(即 Survivor 1),并且对象的年龄会增加。这一过程称为对象的“存活跨越”。

  5. 随着对象在 Survivor 区域之间被复制的次数增多,它们的年龄(即 age󠁪计数器)也随之增加。当对象达到预定义的最大年龄阈值(默认是 15,可通过-XX:MaxTenuringThreshold󠁪参数调整)时,它们将被晋升到老年代(Old Generation)。

  6. 老年代用于存储长期存活或大尺寸的对象,当老年代内存不足时,再次触发 GC: Major GC, 进行老年代的内存清理。

  7. 当老年代空间不足时,JVM 将执行 Major GC(也称作 Full GC),这通常包括对整个年轻代和老年代的垃圾回收。Major GC 比 Minor GC 更消耗资源,因为它涉及更多的内存空间。

  8. 如果在执行 Major GC 后,仍然没有足够的空间来满足新的内存分配需求,JVM 将抛出 OutOfMemoryError󠁪(OOM)异常,表明无法继续为程序分配所需的内存。


发布于: 刚刚阅读数: 5
用户头像

让云运维更简单 2023-06-25 加入

云观测领导者

评论

发布
暂无评论
深入理解JVM:内存管理与垃圾回收机制探索_JVM_乘云 DataBuff_InfoQ写作社区