1.概述

有没有想过为什么Java应用程序通过众所周知的-Xms和-Xmx调优标志消耗的内存比指定数量多得多?出于各种原因和可能的优化,JVM可以分配额外的本机内存。这些额外的分配最终会使消耗的内存超出-Xmx限制。

在本教程中,我们将列举JVM中的一些常见内存分配源,以及它们的大小调整标志,然后学习如何使用本机内存跟踪监视它们。

2.原生分配

堆通常是Java应用程序中最大的内存使用者,但还有其他人。除了堆之外,JVM还从本机内存中分配出一个相当大的块来维护类的元数据,应用程序代码,JIT生成的代码,内部数据结构等。在下面的部分中,我们将探讨其中的一些分配。

2.1. Metaspace(元空间)

为了维护有关已加载类的一些元数据,JVM使用名为Metaspace的专用非堆区域。在Java 8之前,被称为PermGen或Permanent Generation。 Metaspace或PermGen包含有关已加载类的元数据,而不是它们的实例,它们保存在堆中。

这里重要的是堆大小配置不会影响元空间大小,因为Metaspace是一个堆外数据区。为了限制Metaspace大小,我们使用其他调优标志:

  • -XX:MetaspaceSize和-XX:MaxMetaspaceSize设置最小和最大元空间大小
  • 在Java 8之前,-XX:PermSize和-XX:MaxPermSize设置最小和最大PermGen大小

2.2. Threads(线程)

JVM中最耗费内存的数据区之一是堆栈,与每个线程同时创建。堆栈存储局部变量和部分结果,在方法调用中起着重要作用。

默认的线程堆栈大小取决于平台,但在大多数现代64位操作系统中,它大约为1 MB。此大小可通过-Xss调整标志进行配置。

与其他数据区域相比,当对线程数没有限制时,分配给堆栈的总内存实际上是无限制的。值得一提的是,JVM本身需要一些线程来执行其内部操作,如GC或即时编译。

2.3. Code Cache(代码缓存)

为了在不同平台上运行JVM字节码,需要将其转换为机器指令。执行程序时,JIT编译器负责此编译。

当JVM将字节码编译为汇编指令时,它会将这些指令存储在称为代码缓存的特殊非堆数据区中。可以像管理JVM中的其他数据区一样管理代码缓存。 -XX:InitialCodeCacheSize-XX:ReservedCodeCacheSize调整标志确定代码缓存的初始值和可能最大值。

2.4. Garbage Collection(垃圾回收)

JVM附带了一些GC算法,每个算法适用于不同的用例。所有这些GC算法都有一个共同的特点:他们需要使用一些堆外数据结构来执行他们的任务。这些内部数据结构消耗更多本机内存。

2.5. Symbols(符号)

让我们从 Strings 开始,这是应用程序和库代码中最常用的数据类型之一。由于它们无处不在,它们通常占据堆的很大一部分。如果大量的这些字符串包含相同的内容,那么堆的很大一部分将被浪费。

为了节省一些堆空间,我们可以存储每个 String 的一个版本,并让其他版本引用存储的版本。此过程称为 String Interning 。由于JVM只能内部编译时间字符串常量,我们可以手动调用字符串的intern方法来获取内部编译字符串。

JVM将实际存储的字符串存储在本机特殊固定大小并称为字符串表的哈希表中,也称为字符串池。我们可以通过-XX:StringTableSize调整标志配置表大小(即桶的数量)。

除了字符串表之外,还有另一个称为运行时常量池的本机数据区域。 JVM使用此池来存储常量,如编译时数字文字或必须在运行时解析的方法和字段引用。

2.6. Native Byte Buffers(本地字节缓冲区)

JVM通常有大量分配本机内存的嫌疑,但有时开发人员也可以直接分配本机内存。最常见的方法是被JNI调用的malloc和NIO中可直接调用的ByteBuffers。

2.7. Additional Tuning Flags(额外的调整标志)

在本节中,我们针对不同的优化方案使用了少量JVM调优标志。使用以下提示,我们几乎可以找到与特定概念相关的所有调优标志:

$ java -XX:+PrintFlagsFinal -version | grep <concept>

PrintFlagsFinal打印JVM中的所有-XX选项。例如,要查找所有与Metaspace相关的标志:

$ java -XX:+PrintFlagsFinal -version | grep Metaspace
// truncated
uintx MaxMetaspaceSize = 18446744073709547520 {product}
uintx MetaspaceSize = 21807104 {pd product}
// truncated

3. 本机内存跟踪 (NMT)

现在我们已经了解了JVM中本机内存分配的常见来源,现在是时候找出如何监视它们了。首先,我们应该使用另一个JVM调优标志启用本机内存跟踪:-XX:NativeMemoryTracking = off | sumary | detail。默认情况下,NMT处于关闭状态,但我们可以使其查看其观察的摘要或详细视图。

假设我们想要跟踪典型Spring Boot应用程序的本机分配:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar

在这里,我们在分配300 MB堆空间的同时启用NMT,G1作为我们的GC算法。

3.1. 实例快照

启用NMT后,我们可以使用jcmd命令随时获取本机内存信息:

$ jcmd <pid> VM.native_memory

为了找到JVM应用程序的PID,我们可以使用jps命令:

$ jps -l
7858 app.jar // This is our app
7899 sun.tools.jps.Jps

现在,如果我们将jcmd与适当的pid一起使用,VM.native_memory会使JVM打印出有关本机分配的信息:

$ jcmd 7858 VM.native_memory

让我们逐节分析NMT输出。

3.2. 总分配

NMT报告全部保留和提交的内存如下:

Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB

保留内存表示我们的应用程序可能使用的内存总量。相反,提交的内存表示我们的应用程序现在使用的内存量。

尽管分配了300MB的堆,我们的应用程序的总预留内存几乎是1.7 GB,远远超过它。类似地,提交的内存大约为440 MB,这再次远远超过300 MB。

在整体了解之后,NMT报告每个分配源的内存分配。所以,让我们深入探讨每个来源。

3.3. Heap(堆)

NMT按我们的预期报告堆分配:

Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)

300 MB的保留和已提交内存,与我们的堆大小设置相匹配。

3.4. Metaspace(元空间)

这是NMT关于加载类的元数据的报告:

Class (reserved=1091407KB, committed=45815KB)
(classes #6566)
(malloc=10063KB #8519)
(mmap: reserved=1081344KB, committed=35752KB)

几乎保留了1 GB,45 MB保留加载6566个类。

3.5. Thread(线程)

这是关于线程分配的NMT报告:

Thread (reserved=37018KB, committed=37018KB)
(thread #37)
(stack: reserved=36864KB, committed=36864KB)
(malloc=112KB #190)
(arena=42KB #72)

总共有36 MB的内存被分配给37个线程的堆栈 - 每个堆栈大约1 MB。 JVM在创建时将内存分配给线程,因此保留和提交的分配是相等的。

3.6. Code Cache(代码缓冲区)

让我们看看NMT对JIT生成和缓存的汇编指令的报告:

Code (reserved=251549KB, committed=14169KB)
(malloc=1949KB #3424)
(mmap: reserved=249600KB, committed=12220KB)

目前,正在缓存大约13 MB的代码,这个数量可能会达到245 MB。

3.7. GC

以下是有关G1 GC内存使用情况的NMT报告:

GC (reserved=61771KB, committed=61771KB)
(malloc=17603KB #4501)
(mmap: reserved=44168KB, committed=44168KB)

我们可以看到,保留和已提交都接近60 MB,致力于帮助G1。

让我们来看看更简单的GC的内存使用情况,比如Serial GC:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar

Serial GC 几乎使用不到1 MB:

GC (reserved=1034KB, committed=1034KB)
(malloc=26KB #158)
(mmap: reserved=1008KB, committed=1008KB)

显然,我们不能仅仅因为其内存使用而选择GC算法,因为串行GC的暂停回收本质可能会导致性能下降。但是,还有几个GC可供选择,它们各自平衡内存和性能。

3.8. Symbol(符号)

以下是有关符号分配的NMT报告,例如字符串表和常量池:

Symbol (reserved=10148KB, committed=10148KB)
(malloc=7295KB #66194)
(arena=2853KB #1)

将近10 MB分配给符号。

3.9. 随着时间的推移的NMT

NMT允许我们跟踪内存分配如何随时间变化。首先,我们应该将应用程序的当前状态标记为基线:

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded

然后,过了一会儿,我们可以将当前的内存使用情况与该基线(baseline)进行比较:

$ jcmd <pid> VM.native_memory summary.diff

NMT使用+和 - 符号将告诉我们在此期间内存使用情况如何变化:

Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
- Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB) - Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated

保留和提交的总内存分别增加了3 MB和6 MB。可以很容易地发现内存分配的其他波动。

3.10. 详细的NMT

NMT可以提供非常详细的有关整个存储空间映射的信息。要启用此详细报告,我们应使用 -XX:NativeMemoryTracking =detail 信息调整标志。

4. 结束语

在本文中,我们列举了JVM中本机内存分配的不同使用者。然后,我们学习了如何检查正在运行的应用程序以监视其本机分配。借助以上这些,我们可以更有效地调整应用程序以及运行时环境的大小。

原文:https://www.baeldung.com/native-memory-tracking-in-jvm

作者:Ali Dehghani

译者:Emma

JVM中的本机内存跟踪的更多相关文章

  1. JVM中,对象在内存中的布局

    在hotSpot虚拟机中,对象在内存中的布局可以分成对象头.实例数据.对齐填充三部分. 对象头:主要包括: 1.对象自身的运行行元数据,比如哈希码.GC分代年龄.锁状态标志等,这部分长度在32位虚拟机 ...

  2. 改变JVM中的参数以提高Eclipse的运行速度

    首先建立评估体系,将workspace里所有的项目close掉,关闭eclipse.优化的用例就是启动eclipse,open一个项目,eclipse会自动build这个项目,保证没有感觉到明显的卡, ...

  3. jvm中的年轻代 老年代 持久代 gc

    虚拟机中的共划分为三个代:年轻代(Young Generation).老年代(Old Generation)和持久代(Permanent Generation).其中持久代主要存放的是Java类的类信 ...

  4. 【转载】java项目中经常碰到的内存溢出问题: java.lang.OutOfMemoryError: PermGen space, 堆内存和非堆内存,写的很好,理解很方便

    Tomcat Xms Xmx PermSize MaxPermSize 区别 及 java.lang.OutOfMemoryError: PermGen space 解决 解决方案 在 catalin ...

  5. JVM(一)Java内存模型

    前言 对于从事C.C++程序开发的开发人员来说,在开始使用对象之前,他们都需要使用new关键字为对象申请内存空间,在使用完对象之后,也需要使用delete关键字来释放对象占用的内存空间.对于Java程 ...

  6. 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native ...

  7. JVM系列一(Java内存区域和对象创建).

    一.JVM 内存区域 堆 - Heap 线程共享,JVM中最大的一块内存,此内存的唯一目的就是存放对象实例,Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆"( ...

  8. JVM中堆的介绍

    一.堆的概述 一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域,堆在JVM启动的时候创建,其空间大小也被创建,是JVM中最大的一块内存空间,所有线程共享Java堆,物理上不连续的逻辑上连 ...

  9. 3、JVM中的对象

    1.对象的创建 A  a = new A() A:引用的类型 a::引用的名称 new A():创建一个A类对象 当创建一个对象时,具体创建过程是什么呢? (1)JVM遇到new的字节码指令后,检查类 ...

随机推荐

  1. Android面HTTP协议发送get要求

    /** * 采纳get办法要求 * * @param username * @param password * @return null表示求得的路径有问题,text返回请求得到的数据 */ publ ...

  2. JPA示例项(J采纳PA的hibernate实现版本号)

    (1).JPA介绍: JPA全名Java Persistence API ,Java坚持API这是Sun公司Java EE 5规范中提出的Java持久化接口. JPA吸取了眼下Java持久化技术的长处 ...

  3. Delphi中返回类型为string的函数的一个陷阱(不是很懂)

    如果类的一个成员函数的返回值是string类型,需要注意一个问题 其返回值可能是错误的 例如函数的实现如下 function GetString( s: string ): string;begin  ...

  4. 在2005年,Unicode 的第十万个字符被采纳且认可成为标准之一(超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现)

    在计算机科学领域中,Unicode(统一码.万国码.单一码.标准万国码)是业界的一种标准,它可以使电脑得以体现世界上数十种文字的系统.Unicode 是基于通用字符集(Universal Charac ...

  5. python 编码转换 专题

    主要介绍了python的编码机制,unicode, utf-8, utf-16, GBK, GB2312,ISO-8859-1 等编码之间的转换. 常见的编码转换分为以下几种情况: 自动识别 字符串编 ...

  6. linux 用蓝牙和手机通信

    加载模块: # modprobe hci_usb    # modprobe rfcomm    # hciconfig hci0 up # hciconfig hci0 up 查看状况: # hci ...

  7. QDialog在hide()之后,就被销毁的原因

    一路跟踪源码,关键就是这两句: int QDialog::exec() { Q_D(QDialog); if (d->eventLoop) { qWarning("QDialog::e ...

  8. 【Python】设备重启测试

    ①添加读取键盘输入功能,方便测试者选择压测次数! Python提供了 input() 内置函数从标准输入读入一行文本,默认的标准输入是键盘. input 可以接收一个Python表达式作为输入,并将运 ...

  9. 快速写入Xml文件

    我们在做一些操作的时候会需要生成日志,Xml文件就是我们常用的一种日志文件. 普通操作Xml文件的代码遇到大数据量的话就很慢了. 用这个生成Xml文件的话,即使数据量很大,也很快 private vo ...

  10. RDLC报表 分组排序设置

    原文:RDLC分组排序设置 RDLC中反复设置表的排序字段,设置数据源排序后绑定均无效. 需要在分组字段右击组属性中,修改对应的排序字段,即可.