陈同学
微服务
Accelerator
About
# 高性能应用之理解JVM堆内存 > 原文:[Understanding the Java Virtual Machine Heap for High Performance Applications](https://www.pushtechnology.com/support/kb/understanding-the-java-virtual-machine-heap-for-high-performance-applications/) by Marcin Kruglik,11th October, 2017 > 翻译:[陈同学](https://chenyongjun.vip/) *译者前言*:由于译者已翻译 [JVM 栈和栈帧](https://chenyongjun.vip/articles/35)、[JVM内存管理](https://chenyongjun.vip/articles/40),本文将只翻译部分不重叠的内容,同时将翻译下面2篇文章的部分内容. * [Logging Stop-the-world Pauses in JVM](https://dzone.com/articles/logging-stop-world-pauses-jvm) * [Java (JVM) Memory Model – Memory Management in Java](https://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java) *翻译开始* --- 在高性能应用中,理解内存使用和垃圾收集对于JVM性能的影响十分重要。 在一个快速变化且可横向扩展的环境中,需要高效的内存回收,Java中使用的现代垃圾收集器也为此做了优化。然而,垃圾收集事件还是会对性能造成影响,在高可用和要对数据变化作出响应的系统中,应该降低这种影响。 理解JVM的行为是查找内存溢出、性能以及扩容等问题的第一步,同时内存配置文件也为之提供了宝贵的数据。 ## JVM堆区域 JVM内存结构由驻留在本机内存中的几个数据区域组成,各个数据区域担任不同的角色。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/06/09/ddd84986b67344a4b645cb62379974bb.png) 本文将专注于堆内存区域,堆内存是所有类实例和数据内存分配的地方。 堆在JVM启动时创建且线程间共享。在Java程序运行期间,线程为新创建的对象在堆上分配内存。随着时间推移,有限的内存将被不可达对象填满,在对象不再被任何地方引用时才可以被回收。如果不回收,由于内存中充满了不可达对象,将导致堆内存耗尽,以至于没有任何空间用于新对象的分配。 在C/C++语言中,开发人员需要自行管理内存,但Java中内存被自动管理,即垃圾收集(GC)。JVM垃圾收集器将堆内存分成几个称为 **代** 的小部分,分别为年轻代和永久代。不同代有不同的垃圾收集算法。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/06/09/925f8736a04f474bb557af96cdd5d243.png) *译者注:年轻代、永久代的概念以及相关GC算法不再重复翻译* ## JVM内存使用 ### 健康应用 在一个健康的JVM应用中,使用的内存不断增长是比较正常的,会一直增长到执行清理 *死亡* 对象的老年代GC为止。这个过程将创建一个锯齿状的堆内存使用图,如下所示: ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/06/09/69176067989546339580fdccc8ba969f.png) > 横轴代表时间,纵轴代表使用的内存 * **蓝色**:表示内存分配率,即运行中的应用程序为新对象分配内存的速率。它越陡峭,说明相同时间内分配的对象就越多。 * **黑色**:表示发生了GC事件。在年轻代或老年代垃圾收集完之后,那些不可达对象占用的内存将被释放,可以用于为新对象分配内存。 * **绿色**:GC之后的内存基线趋势,它代表了存活(可达)对象的堆内存利用情况。 上图是健康应用的内存使用情况,绿色带有箭头的基线代表堆内存的使用趋势一直保持在相同水平。 ### 内存泄漏应用 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/06/09/b3a9b645a6ef402f8d149d8a8350f9c9.png) 上图表明,在每次GC之后,堆内存没有被完全回收,因此内存使用的基线(绿色尖头线)随着时间推移不断增长。 图中峰值与谷值的差异越来越小,表示每次能够回收的内存也越来越少。最终,将达到红色水平线处的最大堆内存,程序将因 **OutOfMemoryError** 异常而终止。 如果堆内存很大,GC事件的执行会花费较长的时间。在这种场景下,可以观察到内存使用量稳步增长,但是如果发生内存泄漏将会打断这种趋势。事实上,这是JVM的自然行为,最终会发生一次老年代GC事件以清理堆中死亡的对象。 > 译者注:本文最后一部分关于Java Misson Control的使用已省略翻译。 ## Stop the World Event > 译者注:本部分翻译于 [Java (JVM) Memory Model – Memory Management in Java](https://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java) 所有的垃圾收集都是 "Stop the World" 事件,因为GC期间应用的所有线程将被暂停,直到GC操作完成。 由于年轻代仅存储生命很短的对象,因此年轻代GC(Minor GC)速度很快而且应用基本不会受到影响。 然而,老年代GC(Major GC)会花费较长时间,因为它需要检查所有存活的对象。应该尽可能少触发Major GC,因为它会导致在GC期间你的应用无法响应。因此,如果你拥有一个响应式应用,同时又有很多Major GC发生,你需要注意下响应超时问题。 垃圾收集器消耗的时间依赖于GC算法,这也是为什么在高速响应式应用中有必要监视和优化垃圾收集器以避免超时的原因。 ## 打印JVM中的 Stop-the-world 停顿的日志 > 译者注:本部分翻译于 [Logging Stop-the-world Pauses in JVM](https://dzone.com/articles/logging-stop-world-pauses-jvm) 不同事件均可导致JVM暂停应用的所有线程。这些"停顿"称为 **Stop-The-World(STW)** 停顿。导致STW停顿最常见的原因是GC事件的触发([见Github例子](https://github.com/gvsmirnov/java-perv/blob/master/labs-8/src/main/java/ru/gvsmirnov/perv/labs/safepoints/FullGc.java)),但是不同的JIT活动([例子](https://github.com/gvsmirnov/java-perv/blob/master/labs-8/src/main/java/ru/gvsmirnov/perv/labs/safepoints/Deoptimization.java))、偏向锁(Biased Lock)的撤销([例子](https://github.com/gvsmirnov/java-perv/blob/master/labs-8/src/main/java/ru/gvsmirnov/perv/labs/safepoints/BiasedLocks.java))、某些[JVMTI](https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#FollowReferences)操作以及很多[其它操作](http://hg.openjdk.java.net/jdk9/jdk9/hotspot/file/0e31ab6e8375/src/share/vm/runtime/vm_operations.hpp)都需要停止应用。 应用中线程可以安全stopped的点称为安全点,这个术语经常用于指代STW停顿。 一般很少启用GC日志,而且,即使启用也不会捕获所有安全点上的信息。为了得到这些信息,可以使用JVM参数: ``` -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime ``` 如果你想知道明确的GC名字,不要惊慌,打开这些选项后将会记录所有安全点信息,而不仅仅是垃圾收集导致的GC停顿。如果你执行这个例子: ```java public class FullGc { private static final Collection<Object> leak = new ArrayList<>(); private static volatile Object sink; public static void main(String[] args) { while (true) { try { leak.add(new byte[1024 * 1024]); sink = new byte[1024 * 1024]; } catch (OutOfMemoryError e) { leak.clear(); } } } } ``` 将会在标准输出中看到一些类似的信息: ``` Application time: 0.3440086 seconds Total time for which application threads were stopped: 0.0620105 seconds Application time: 0.2100691 seconds Total time for which application threads were stopped: 0.0890223 seconds ``` 从上述数据可以知道,应用在344毫秒前都是有效的工作,然后所有线程暂停了62毫秒,之后的210毫秒又在继续有效工作,最后又是8毫秒的停顿。 可以结合`-XX:+PrintGCDetails` 运行上面的例子,输出的可能是以下内容: ``` [Full GC (Ergonomics) [PSYoungGen: 1375253K->0K(1387008K)] [ParOldGen: 2796146K->2049K(1784832K)] 4171400K->2049K(3171840K), [Metaspace: 3134K->3134K(1056768K)], 0.0571841 secs] [Times: user=0.02 sys=0.04, real=0.06 secs] Total time for which application threads were stopped: 0.0572646 seconds, Stopping threads took: 0.0000088 seconds ``` 可以看出,由于GC应用停顿了57毫秒,其中8毫秒用于等待应用所有线程到达安全点。如果我们使用另外一个例子: ```java import java.util.concurrent.locks.LockSupport; import java.util.stream.Stream; public class BiasedLocks { private static synchronized void contend() { LockSupport.parkNanos(100_000); } // Run with: -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDetails // Notice that there are a lot of stop the world pauses, but no actual garbage collections // This is because PrintGCApplicationStoppedTime actually shows all the STW pauses // To see what's happening here, you may use the following arguments: // -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 // It will reveal that all the safepoints are due to biased lock revocations. // Biased locks are on by default, but you can disable them by -XX:-UseBiasedLocking // It is quite possible that in the modern massively parallel world, they should be // turned back off by default public static void main(String[] args) throws InterruptedException { Thread.sleep(5_000); // Because of BiasedLockingStartupDelay Stream.generate(() -> new Thread(BiasedLocks::contend)) .limit(10) .forEach(Thread::start); } } ``` 可能会看到如下输出: ``` Total time for which application threads were stopped: 0.0001273 seconds, Stopping threads took: 0.0000196 seconds Total time for which application threads were stopped: 0.0000648 seconds, Stopping threads took: 0.0000174 seconds ``` > 译者注:更多例子请查看原文,不再一一赘译。翻译这两个例子意在解释stop-the-world。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
高性能应用之理解JVM堆内存
文章链接:
https://chenyongjun.vip/articles/41
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力