详解JVM中的本机内存跟踪

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

(0)

相关推荐

  • 详解JVM中的本机内存跟踪

    1.概述 有没有想过为什么Java应用程序通过众所周知的-Xms和-Xmx调优标志消耗的内存比指定数量多得多?出于各种原因和可能的优化,JVM可以分配额外的本机内存.这些额外的分配最终会使消耗的内存超出-Xmx限制. 在本教程中,我们将列举JVM中的一些常见内存分配源,以及它们的大小调整标志,然后学习如何使用本机内存跟踪监视它们. 2.原生分配 堆通常是Java应用程序中最大的内存使用者,但还有其他人.除了堆之外,JVM还从本机内存中分配出一个相当大的块来维护类的元数据,应用程序代码,JIT生成

  • 详解jvm中的标量替换

    概述 通常在java中创建一个对象,大家都认为是在堆中创建. 在jdk6开始有逃逸分析,标量替换等技术,关于在堆中创建对象不再绝对. 关于标量替换,通过以下几点进行概述: 逃逸分析 标量替换是什么 测试标量替换 逃逸分析 逃逸分析是一种分析技术,分析对象的动态作用域,供其他优化措施提供依据.比如分析一个对象不会逃逸到方法之外或线程之外,其它优化措施(栈上分配,标量替换等)根据逃逸程度进行优化. 逃逸分析示例 public class EscapeAnalysis { public Person

  • 详解JVM中的GC调优

    那些GC的默认值 其实GC或者说JVM的参数非常非常的多,有控制内存使用的: 有控制JIT的: 有控制分代比例的,也有控制GC并发的: 当然,大部分的参数其实并不需要我们自行去调整,JVM会很好的动态帮我们设置这些变量的值. 如果我们不去设置这些值,那么对GC性能比较有影响的参数和他们的默认值有哪些呢? GC的选择 我们知道JVM中的GC有很多种,不同的GC选择对java程序的性能影响还是比较大的. 在JDK9之后,G1已经是默认的垃圾回收器了. 我们看一下G1的调优参数. G1是基于分代技术的

  • 详解Jvm中时区设置方式

    这篇文章memo一下Jvm中关于时区设定的基础操作. Java的时区设定 这里列出如下三种方式 方式 说明 TimeZone.setDefault方式 通过java的utils下的TimeZone进行动态设定 user.timezone传递方式 运行时通过传递-Duser.timezone=xxx进行设定 TZ环境变量方式 通过export的TZ环境变量进行设定 TimeZone.setDefault方式 Sample代码如下: sh-4.2# cat TestTimeZone.java imp

  • 详解JVM 中的StringTable

    是什么 字符串常量池是 JVM中的一个重要结构,用于存储JVM运行时产生的字符串.在JDK7之前在方法区中,存储的是字符串常量.而字符串常量池在 JDK7开始移入堆中,随之而来的是除了存储字符串常量外,还可以存储字符串引用(因为在堆中,引用堆中的字符串常量很方便,所以可以存储引用).这使得很多字符串的操作在 JDK7中和在之前的版本中执行是不同的结果.这也是为什么字符串相关的问题是如此具有迷惑性的原因之一. 底层 String:在 JDK9之前,String底层是使用 char数组来存储字符串数

  • 详解JVM系列之内存模型

    1. 内存模型和运行时数据区 这一章学习java虚拟机内存模型(Java Virtual machine menory model),可以这样理解,jvm运行时数据库是一种规范,而JVM内存模型是对改规范的实现 java虚拟机重点存储数据的是堆和方法区,所以本章节也重点从这两个方面进行比较详细描述.堆和方法区是内存共享的,而java虚拟机栈.Native方法栈.程序计数器是线程私有的 2.思维导图和图例 一个是非堆区(方法区),方法区也一般被称之为"永久代".另外一个是堆区,分为you

  • 详解Java中一维、二维数组在内存中的结构

    前言 我们知道在Java中数组属于引用数据类型,它整个数组的数组元素既可以是基本数据类型的(如 byte \ int \ short \ long \ float \ double \ char \ boolean 这些),也可以是引用数据类型的.当它的数组元素是基本数据类型时,这个数组就是一个一维数组:当它的数组元素是引用数据类型时,它就是一个多维数组.比如,在一个数组中它的某个元素值其实是一个一维数组,而其他不同的元素也各自包含了一个一维数组,我们就把这个包含很多个一维数组的数组叫做二维数组

  • 详解Java中的内存屏障

    为什么要有内存屏障 这个是为了解决因为cpu,高速缓存,主内存出现的时候,导致的可见性和重序性问题,什么问题呢,看下面 我们都知道计算机运算任务需要CPU和内存相互配合共同完成,其中CPU负责逻辑计算,内存负责数据存储.CPU要与内存进行交互,如读取运算数据.存储运算结果等.由于内存和CPU的计算速度有几个数量级的差距,为了提高CPU的利用率,现代处理器结构都加入了一层读写速度尽可能接近CPU运算速度的高速缓存来作为内存与CPU之间的缓冲:将运算需要使用的数据复制到缓存中,让CPU运算可以快速进

  • 一文详解C++中动态内存管理

    目录 前言 1.C/C++程序的内存开辟 2.C语言中动态内存管理方式:malloc/calloc/realloc/free 2.1malloc.calloc.realloc区别? 3.C++内存管理方式 3.1 new/delete操作内置类型 3.2 new和delete操作自定义类型 3.3new和malloc处理失败 4.operator new与operator delete函数 4.1 operator new与operator delete函数 4.1.1 我们看看operator

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

随机推荐