深入理解 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 垃圾回收过程
创建新对象先放到 Eden 区空间
当 Eden 区装满时,会对 Eden 区进行垃圾回收(Minor GC),在 Eden 区实现清除策略,没有被引用的对象直接被回收,再加载新的对象放到 Eden 区。
Eden 区中依然存活的对象会被移送到 Survivor 区。
在每次 Minor GC 后,存活的对象会被从当前使用的 Survivor 区域(假设是 Survivor 0)转移到另一个 Survivor 区域(即 Survivor 1),并且对象的年龄会增加。这一过程称为对象的“存活跨越”。
随着对象在 Survivor 区域之间被复制的次数增多,它们的年龄(即 age计数器)也随之增加。当对象达到预定义的最大年龄阈值(默认是 15,可通过-XX:MaxTenuringThreshold参数调整)时,它们将被晋升到老年代(Old Generation)。
老年代用于存储长期存活或大尺寸的对象,当老年代内存不足时,再次触发 GC: Major GC, 进行老年代的内存清理。
当老年代空间不足时,JVM 将执行 Major GC(也称作 Full GC),这通常包括对整个年轻代和老年代的垃圾回收。Major GC 比 Minor GC 更消耗资源,因为它涉及更多的内存空间。
如果在执行 Major GC 后,仍然没有足够的空间来满足新的内存分配需求,JVM 将抛出 OutOfMemoryError(OOM)异常,表明无法继续为程序分配所需的内存。
版权声明: 本文为 InfoQ 作者【乘云 DataBuff】的原创文章。
原文链接:【http://xie.infoq.cn/article/7db38b8b2e41084649f516000】。文章转载请联系作者。
评论