对于 JAVA 开发人员来说,进行程序的性能优化是很有挑战的工作,也是很有意义的一件事。本篇主要根据 JVM 内存模型和垃圾回收的详细讲解,可以更好的理解JVM的调优的根本原理。
JVM 架构
应该已经使用了一些像这样的 JVM 配置
JAVA_OPTS=”-server -Xms2560m -Xmx2560m -XX:NewSize=1536m -XX:MaxNewSize=1536m -XX:MetaspaceSize=768m -XX:MaxMetaspaceSize=768m -XX:InitialCodeCacheSize=64m -XX:ReservedCodeCacheSize=96m -XX:MaxTenuringThreshold=5″
那么 JVM 是如何驻留在内存上的?JVM 消耗主机操作系统内存上的可用空间。
然而,在 JVM 内部,存在独立的内存空间(堆、非堆、缓存),以存储运行时数据和编译后的代码。
以下是有关服务器应用程序堆大小的一般准则:
Java 通过一个称为垃圾收集器的程序提供自动内存管理。
“移除不再使用的对象。”
上面的一切都是在堆中完成的,堆是运行时动态内存分配的空间,用于包含所有 java 对象。除了堆之外,还有堆栈,其中包含支持线程执行的局部变量和函数调用。
许多人认为垃圾收集会收集并丢弃死对象。事实上,Java 垃圾收集的作用恰恰相反!活动对象被跟踪,其他所有对象都被指定为垃圾。这种根本性的误解可能会导致许多性能问题。
让我们从堆开始,它是用于动态分配的内存区域。在大多数配置中,操作系统会提前分配堆,以便在程序运行时由 JVM 管理。这有几个重要的影响:
新对象简单地分配在已用堆的末尾
一旦某个对象不再被引用并且因此应用程序代码无法访问该对象,垃圾收集器就会将其删除并回收未使用的内存。
每个对象树必须有一个或多个根对象。只要应用程序可以到达这些根,那么整棵树都是可以到达的。但是这些根对象什么时候被认为是可达的呢?称为垃圾收集根,它是特殊对象始终是可访问的,任何在其根处具有垃圾收集根的对象也是如此。
Java中有四种GC root:
GC 根是 JVM 本身引用的对象,因此可以防止其他所有对象被垃圾收集。
因此,一个简单的 Java 应用程序具有以下 GC 根:
为了确定哪些对象不再使用,JVM 间歇性地运行所谓的“标记和清除”算法。正如所直觉的,这是一个简单的两步过程:
活动对象在上图中表示为蓝色。当标记阶段结束时,每个活动对象都被标记。因此,所有其他对象(上图中的灰色数据结构)都无法从 GC 根访问,这意味着应用程序无法再使用无法访问的对象。此类对象被视为垃圾,GC 应在以下阶段中删除它们。
标记阶段需要注意以下重要方面:
对于不同的 GC 算法,未使用对象的删除略有不同,但所有此类 GC 算法都可以分为三步:清除(sweeping)、压缩(compacting)和复制(copying)。
标记和清除算法在概念上使用最简单的垃圾处理方法,即忽略此类对象。这意味着在标记阶段完成后,未访问对象占用的所有空间都被视为空闲,因此可以重用以分配新对象。
该方法需要使用所谓的空闲列表记录每个空闲区域及其大小。空闲列表的管理增加了对象分配的开销。这种方法还有另一个弱点——可能存在大量空闲区域,但如果没有一个区域足够大来容纳分配,分配仍然会失败(在 Java 中会出现 OutOfMemoryError 错误)。
它通常被称为标记-清除算法。
Mark-Sweep-Compact算法通过将所有标记的对象 (即活动的对象)移动到内存区域的开头来解决Mark-and-Sweep算法的缺点。这种方法的缺点是增加了GC暂停时间,因为我们需要将所有对象复制到一个新位置,并更新对这些对象的所有引用。Markand Sweep的好处也是显而易见的--在这样一个压缩操作之后,通过指针碰撞,新对象的分配再次变得非常便宜。使用这种方法,空闲空间的位置总是已知的,也不会触发碎片问题。
它通常被称为标记-压缩算法。
标记和复制算法非常类似于标记和压缩,因为它们也重新定位所有活动对象。重要的区别在于,对象搬迁的目标是不同的记忆区域,作为幸存对象的新家。标记和复制方法具有一些优点,因为复制可以与标记在同一阶段同时发生。缺点是需要多一个内存区域,该内存区域应该足够大以容纳幸存的对象。
它通常被称为标记复制算法。
所有垃圾收集都是“Stop the World”事件。这意味着所有应用程序线程都将停止,直到操作完成。垃圾收集始终是“Stop the World”事件。
老年代用于存储长期存活的对象。通常,为年轻代对象设置一个阈值,当达到该年龄时,该对象将被移动到老年代。最终需要收集老年代。此事件称为主垃圾收集。
主垃圾收集也是 Stop the World 事件。通常,主垃圾收集要慢得多,因为它涉及所有活动对象。因此,对于响应式应用程序,应最大程度地减少主要垃圾收集。另请注意,主要垃圾收集的 Stop the World 事件的长度受到用于老年代空间的垃圾收集器类型的影响。
当应用程序启动并在 Eden 空间上分配内存时。蓝色是活动对象,灰色是死对象(无法到达)。当给定空间已满时,应用程序尝试创建另一个对象,并且 JVM 尝试在 Eden 上分配某些内容,但分配失败。这实际上会导致轻微GC。
第一次minor GC后,所有存活对象将被移动到Survivor 1,年龄为1,死亡对象将被删除。
应用程序正在运行,新对象再次在 Eden 空间中分配。有些对象在 Eden 空间和 Survivor 1 上都变得无法访问
在第二次 Minor GC 之后,所有存活对象将被移动到 Survivor 2(来自年龄为 1 的 Eden 和年龄为 2 的 Survivor 1),并且死亡对象将被删除。
应用程序仍在运行,新对象在 Eden 空间上分配,过了一会儿,一些对象从 Eden 和 Survivor 2 都无法访问
在第三次minor GC之后,随着年龄的增加,所有存活对象将从Eden和Survivor 2移动到Survivor 1,并且死亡对象将被删除。
在Survivor中存活时间较长的对象,如果年龄大于-XX:MaxTenuringThreshold,将会被提升到老年代(Tuner)
我们可以使用 VisualVM 的插件 VisualGC 附加到已检测的 HotSpot JVM,收集并以图形方式显示垃圾收集、类加载器和 HotSpot 编译器性能数据。
通常,在调整 Java 应用程序时,重点是两个主要目标之一:响应速度和吞吐量。
响应能力是指应用程序或系统响应所请求的数据的速度。示例包括:
对于注重响应能力的应用程序来说,较长的暂停时间是不可接受的。重点是在短时间内做出响应。
吞吐量侧重于在特定时间段内最大化应用程序的工作量。如何测量吞吐量的示例包括:
对于注重吞吐量的应用程序来说,较长的暂停时间是可以接受的。由于高吞吐量应用程序关注较长时间段的基准,因此不考虑快速响应时间。
CMS垃圾收集本质上是升级的标记和清除算法。它使用多个线程扫描堆内存。它经过修改以利用更快的系统并增强了性能。
它尝试通过与应用程序线程同时执行大部分垃圾收集工作来最大程度地减少由于垃圾收集而导致的暂停。它在年轻代中使用并行的 stop-the-world 标记复制算法,在老年代中使用大多数并发的标记清除算法。
要使用 CMS GC,请使用以下 JVM 参数:
-XX:+UseConcMarkSweepGC
该算法对年轻代使用标记-复制,对老生代使用标记-清除-压缩。它在单线程上工作。执行时,它会冻结所有其他线程,直到垃圾收集操作结束。
由于串行垃圾收集的线程冻结性质,它仅适用于非常小的程序垃圾收集。
要使用串行 GC,请使用以下 JVM 参数:
-XX:+UseSerialGC
与串行GC类似,它在年轻代中使用标记复制,在老年代中使用标记清除紧凑。多个并发线程用于标记和复制/压缩阶段。可以使用 -XX:ParallelGCThreads=N 选项配置线程数。
如果主要的目标是通过有效利用现有系统资源来提高吞吐量,则并行垃圾收集器适用于多核计算机。使用这种方法,可以大大缩短 GC 循环时间。
要使用并行 GC,请使用以下 JVM 参数:
-XX:+UseParallelGC
G1(垃圾优先)垃圾收集器在 Java 7 中可用,旨在作为 CMS 收集器的长期替代品。G1 收集器是一个并行、并发、增量压缩的低暂停垃圾收集器。
这种方法涉及将内存堆分割成多个小区域(通常为 2048 个)。每个区域都被标记为年轻代(进一步分为eden regions或survivor regions)或老年代。这使得 GC 可以避免一次收集整个堆,而是逐步解决问题。这意味着一次仅考虑区域的子集。
G1 持续跟踪每个区域包含的实时数据量。该信息用于确定包含最多垃圾的区域;所以首先收集它们。这就是为什么它被称为垃圾优先收集。
不幸的是,就像其他算法一样,压缩操作是使用 Stop the World 方法进行的。但根据其设计目标,可以为其设置特定的性能目标。还可以配置暂停持续时间,例如在任何给定的秒内不超过 10 毫秒。垃圾优先 GC 将尽最大努力以高概率实现这一目标(但不确定,由于操作系统级别的线程管理)。
如果你想在 Java 7 或 Java 8 机器上使用,请使用 JVM 参数,如下所示:
-XX:+UseSerialGC
-XX:+UseG1GC
-XX:+UseStringDeduplication
-XX:+ParallelRefProcEnabled
-XX:+AlwaysPreTouch
-XX:+DisableExplicitGC
-XX:ParallelGCThreads=8
-XX:GCTimeRatio=9
-XX:MaxGCPauseMillis=25
-XX:MaxGCMinorPauseMillis=5
-XX:ConcGCThreads=8
-XX:InitiatingHeapOccupancyPercent=70
-XX:MaxTenuringThreshold=10
-XX:SurvivorRatio=6
-XX:-UseAdaptiveSizePolicy
-XX:MaxMetaspaceSize=256M
-Xmx4G
-Xms2G
请注意,JVM性能调优是一个复杂的过程,需要结合具体的应用程序特性和需求来进行调优。不同的应用场景可能需要不同的调优策略。在进行JVM性能调优时,应该先进行性能测试和分析,找出性能瓶颈,然后有针对性地进行优化。同时,及时记录和备份调优前的配置和参数,以便在调优过程中出现问题时能够恢复到原始状态。