目的
本文讨论了 JDK8 及更高版本的堆内存和栈内存管理的基本概念。以及 GC 及其算法的基础知识。
内存管理的重要性
Java 垃圾收集器不能确保堆内存完全空闲,而且对于开发人员来说,不可能强制垃圾收集器在特定时间运行。因此,了解 Java 中的内存管理是如何工作的对开发程序会很有帮助。
了解内存管理有助于编写优化的内存效率代码,并有助于避免程序中任何与内存相关的问题,这些问题可能导致应用程序运行缓慢,并有助于避免 StackOverFlowError 和 OutOfMemoryError 等错误。
栈内存
栈是一种线性数据结构,是 Java 分配的静态内存,用于存储堆对象引用,也存储 Java 原始类型值。栈以后进先出 (LIFO) 顺序访问内存,并且栈比堆内存快。
每个线程在内存中创建自己的栈,这反过来又使栈内存线程安全。
Java 中的方法仅访问方法体(方法范围内)内的栈内存中的对象。当方法执行完成时,该方法对应的块会从栈中清除。
在上面的程序中,我们可以看到,当控件到达main方法时,栈中会有一个args的入口。然后当控件在下一行时,一个新条目被添加到栈中。
当控件超出方法的范围时,引用将从栈中删除。
如果栈内存已满,JVM 会抛出 StackOverFlowError。
堆内存
堆用于JVM在运行时为Java对象动态分配内存。任何新对象都存储在堆中,并且对象的引用(示例变量)存储在栈中。您可以在下面的示例中看到示例代码的变量如何存储在堆和堆栈中。
下面是上述代码片段在堆中的内存分配。
堆内存可以分解成更小的部分,称为代,它们是年轻代、年老/终身代和永久代。
年轻一代
所有新对象都在此内存段中分配。 年轻代由 Eden 和两个 Survivor 空间组成。当 Eden 填满时,垃圾收集发生在年轻代上,这称为 Minor GC。在 Minor GC 期间,来自年轻代的引用对象被移动到 Survivor 空间 #1,并且对象的年龄增加。
例如,在下图中,“对象 1”和“对象 2”将在第一次 Minor GC 之后移动到 Survivor 空间 #1,并且它们将具有指定的年龄。如果“对象 1”在第一次 Minor GC 中幸存下来,则年龄为零。现在如果“对象 1”在下一次 Minor GC 中也幸存下来,那么它将被移动到幸存者空间 2,并且年龄将再次增加。
在第二次 Minor GC 期间,驻留在 Survivor 空间 #1 中的对象(具有引用)将被移动到 Survivor #2,并且年龄将增加(即根据示例年龄将从零变为一)。并且从完整的年轻代空间中所有未引用的对象都将被删除。
老一代
老年代是存放长寿命对象(最老的对象)的地方。 年轻代对象有年龄的上限或阈值。 一旦对象达到该上限,则该对象将移至老一代或终身代。
终身代
这部分堆内存用于存储运行时类和方法的元数据。 从 JDK 8 开始,这部分内存已被 Java 完全删除,并被 Metaspace 概念所取代。您仍然可以设置 --XX:PermSize 和 -XX:MaxPermSize 配置。但是,如果您在 JDK 8 或更高版本上运行应用程序,则会在运行时收到警告。
元空间
这是从 JDK 8 版本开始引入的,它是一个可调整大小的内存区域并从本机内存中分配。元空间保存类元数据,它不是一个连续的内存位置。
每当 Metaspace 达到为 Metaspace 分配的最大大小时,Java 就会触发自动 GC 以释放 Metaspace 内存。
元空间选项是 -XX:MetaspaceSize=size 和 -XX:MaxMetaspaceSize=size
垃圾收集
Java程序编译并更改为字节码并在JVM(Java虚拟机)上运行。Java 程序的对象是在该程序的专用堆内存上创建的。随着时间的推移,会创建更多对象,并且程序不再需要一些对象(未引用和取消范围)。垃圾回收是 Java 执行自动内存管理并通过删除未引用对象来释放内存空间的过程。
JVM 结合了不同的垃圾收集算法。垃圾收集算法检查内存中的每个引用对象,其余对象被视为垃圾收集。
GC算法的类型
以下是 JVM 可用的 4 种类型的 GC 算法。
- 并行GC
- 串行GC
- 并发标记和扫描
- G1 垃圾优先
并行GC
专为具有中等或大量数据的多线程应用程序而设计,在多处理器环境中运行良好。但它会在垃圾收集期间冻结所有应用程序线程。 JVM 选项是 -XX:+UseParallelGC ,您可以选择使用 -XX:ParallelGCThreads= 设置并行线程数。
串行GC
主要设计用于单线程环境。 Liek Parallel GC,它还会在垃圾收集期间冻结所有应用程序线程。JVM 选项是 -XX:+UseSerialGC。
并发标记和渗漏(CMS)
这是一个并发 GC,旨在缩短 GC 暂停时间,并且不需要停止正在运行的应用程序来执行 GC。这就是为什么这个过程比串行或并行 GC 慢的原因。
它使用多线程进行垃圾收集,并且可以与垃圾收集器共享处理器资源。JVM 选项是 -XX:+UseConcMarkSweepGC。
G1 垃圾收集器(G1GC)
这是另一种最高效的并发 GC,专为具有大量内存的多处理器环境而设计。JVM 选项是 -XX:+UseG1GC。
选择 GC 算法的参数
除非您对 GC 时间有特定要求并且需要放置其他规范,否则最好让 JVM 自己选择 GC 算法。
如果要选择和配置 GC 算法,那么需要考虑的参数很少,如堆大小、CPU 核心数、应用程序数据集体积、吞吐量、暂停时间、延迟。
a、堆大小 - 分配给 JVM 的内存总量。更大的堆大小意味着 GC 将花费更多时间。更大的堆内存意味着与更少的堆内存相比,JVM 触发 GC 的频率不会那么频繁。JVM 选项是 -Xms=和 -Xmx=,其中 -Xms 表示最小值,-Xmx 是最大值。
b、CPU 核心 - GC 算法因 CPU 核心数量而异。其中一些是为单核 CPU 设计的,一些是为多核 CPU 设计的。
c、应用程序数据集 - 这是指应用程序使用的对象数量。创建更多数量的新对象,导致填充年轻代空间,需要更多的 GC 时间来释放内存。
d、吞吐量 - 它是完成应用程序任务所需的总时间(GC 外)的百分比。它与分配给 JVM 的内存成反比。
e、暂停时间 - GC 算法在内存回收期间停止应用程序所花费的时间。它根据不同的GC算法而有所不同。JVM 选项是 -XX:MaxGCPauseMillis=
f、延迟 - 它是应用程序的响应时间,直接取决于 GC 暂停时间。
根据上述参数,您必须选择最适合您的应用的 GC 算法。例如:
- ·如果应用程序很小并且使用较小的数据集并且在没有暂停时间要求的单处理器上运行,则串行 GC。
- 如果应用程序性能是最高优先级,则并行 GC。
- ·当应用程序的响应时间很重要时,G1GC 或 CMS 因为它在运行 GC 时不会保留应用程序。
原文链接:https://dzone.com/articles/jvm-memory-architecture-and-gc