高性能应用之理解JVM堆内存

原文:Understanding the Java Virtual Machine Heap for High Performance Applications by Marcin Kruglik,11th October, 2017 翻译:陈同学 欢迎访问译者博客

译者前言:由于译者已翻译 JVM 栈和栈帧JVM内存管理,本文将只翻译部分不重叠的内容,同时将翻译下面2篇文章的部分内容.

翻译开始


在高性能应用中,理解内存使用和垃圾收集对于JVM性能的影响十分重要。

在一个快速变化且可横向扩展的环境中,需要高效的内存回收,Java中使用的现代垃圾收集器也为此做了优化。然而,垃圾收集事件还是会对性能造成影响,在高可用和要对数据变化作出响应的系统中,应该降低这种影响。

理解JVM的行为是查找内存溢出、性能以及扩容等问题的第一步,同时内存配置文件也为之提供了宝贵的数据。

JVM堆区域

JVM内存结构由驻留在本机内存中的几个数据区域组成,各个数据区域担任不同的角色。

本文将专注于堆内存区域,堆内存是所有类实例和数据内存分配的地方。

堆在JVM启动时创建且线程间共享。在Java程序运行期间,线程为新创建的对象在堆上分配内存。随着时间推移,有限的内存将被不可达对象填满,在对象不再被任何地方引用时才可以被回收。如果不回收,由于内存中充满了不可达对象,将导致堆内存耗尽,以至于没有任何空间用于新对象的分配。

在C/C++语言中,开发人员需要自行管理内存,但Java中内存被自动管理,即垃圾收集(GC)。JVM垃圾收集器将堆内存分成几个称为 的小部分,分别为年轻代和永久代。不同代有不同的垃圾收集算法。

译者注:年轻代、永久代的概念以及相关GC算法不再重复翻译

JVM内存使用

健康应用

在一个健康的JVM应用中,使用的内存不断增长是比较正常的,会一直增长到执行清理 死亡 对象的老年代GC为止。这个过程将创建一个锯齿状的堆内存使用图,如下所示:

横轴代表时间,纵轴代表使用的内存

  • 蓝色:表示内存分配率,即运行中的应用程序为新对象分配内存的速率。它越陡峭,说明相同时间内分配的对象就越多。
  • 黑色:表示发生了GC事件。在年轻代或老年代垃圾收集完之后,那些不可达对象占用的内存将被释放,可以用于为新对象分配内存。
  • 绿色:GC之后的内存基线趋势,它代表了存活(可达)对象的堆内存利用情况。

上图是健康应用的内存使用情况,绿色带有箭头的基线代表堆内存的使用趋势一直保持在相同水平。

内存泄漏应用

上图表明,在每次GC之后,堆内存没有被完全回收,因此内存使用的基线(绿色尖头线)随着时间推移不断增长。

图中峰值与谷值的差异越来越小,表示每次能够回收的内存也越来越少。最终,将达到红色水平线处的最大堆内存,程序将因 OutOfMemoryError 异常而终止。

如果堆内存很大,GC事件的执行会花费较长的时间。在这种场景下,可以观察到内存使用量稳步增长,但是如果发生内存泄漏将会打断这种趋势。事实上,这是JVM的自然行为,最终会发生一次老年代GC事件以清理堆中死亡的对象。

译者注:本文最后一部分关于Java Misson Control的使用已省略翻译。

Stop the World Event

译者注:本部分翻译于 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

不同事件均可导致JVM暂停应用的所有线程。这些"停顿"称为 Stop-The-World(STW) 停顿。导致STW停顿最常见的原因是GC事件的触发(见Github例子),但是不同的JIT活动(例子)、偏向锁(Biased Lock)的撤销(例子)、某些JVMTI操作以及很多其它操作都需要停止应用。

应用中线程可以安全stopped的点称为安全点,这个术语经常用于指代STW停顿。

一般很少启用GC日志,而且,即使启用也不会捕获所有安全点上的信息。为了得到这些信息,可以使用JVM参数:

-XX:+PrintGCApplicationStoppedTime 
-XX:+PrintGCApplicationConcurrentTime

如果你想知道明确的GC名字,不要惊慌,打开这些选项后将会记录所有安全点信息,而不仅仅是垃圾收集导致的GC停顿。如果你执行这个例子:

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毫秒用于等待应用所有线程到达安全点。如果我们使用另外一个例子:

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。

原文链接:https://www.pushtechnology.com/support/kb/understanding-the-java-virtual-machine-heap-for-high-performance-applications/

原文作者:Marcin Kruglik

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java面试通关手册

面试必备之深入理解自旋锁

分享一个我自己总结的Java学习的系统知识点以及面试问题,目前已经开源,会一直完善下去,欢迎建议和指导欢迎Star: https://github.com/Sn...

2223
来自专栏linux驱动个人学习

input子系统事件处理层(evdev)的环形缓冲区【转】

在事件处理层(evdev.c)中结构体evdev_client定义了一个环形缓冲区(circular buffer),其原理是用数组的方式实现了一个先进先出的循...

2686
来自专栏玩转JavaEE

MongoDB管道操作符(二)

上篇文章中我们已经学习了MongoDB中几个基本的管道操作符,本文我们再来看看其他的管道操作符。 ---- $group 基本操作 $group可以用来对文档进...

2776
来自专栏PingCAP的专栏

TiDB 源码阅读系列文章(七)基于规则的优化

本篇将主要关注逻辑优化。先介绍 TiDB 中的逻辑算子,然后介绍 TiDB 的逻辑优化规则,包括列裁剪、最大最小消除、投影消除、谓词下推、TopN 下推等等。

5.5K15
来自专栏开发 & 算法杂谈

MultiRace-Efficient on-the-fly data race detection

     最近在研究数据竞争检测方法,之前的工作是参考了Eraser这个工具1997年提出的基于Lockset方法的动态数据检测,

782
来自专栏云霄雨霁

关系模型基本概念

1350
来自专栏Ryan Miao

java并发编程实践学习(2)--对象的组合

先验条件(Precondition):某些方法包含基于状态的先验条件。例如,不能从空队列中移除一个元素,在删除元素前队列必须处于非空状态。基于状态的先验条件的操...

34014
来自专栏风口上的猪的文章

.NET面试题系列[15] - LINQ:性能

当你使用LINQ to SQL时,请使用工具(比如LINQPad)查看系统生成的SQL语句,这会帮你发现问题可能发生在何处。

844
来自专栏炉边夜话

调查问卷:测试你对多核多线程的认知程度

        目前,多核多线程编程已经成为一种趋势,但大部分程序员还没有从串行程序的思维中走出来。即使有些人对多核多线程的概念有所了解,但也是一知半解,...

1002
来自专栏Java3y

操作系统第五篇【死锁】

2024

扫码关注云+社区