4年前 (2020-11-15)  jvm |   抢沙发  4063 
文章评分 1 次,平均分 5.0

Java堆内存并不占JVM进程内存分配的100%。在JVM进程中有许多种类的非堆内存,当对它们进行汇总时,它们通常占比堆更多的RAM。可以将最大堆大小设置为512MB,-Xmx512m,并使进程总共消耗超过1GB的RAM。

如果您对学习非堆类别不感兴趣,可以跳转到改进JVM内存使用的建议,以获得一些实用的技巧。但是,如果您通读这篇文章,您将了解Java进程内存的去向以及原因。让我们看看这些目的地。

JVM内存类别

最重要的JVM内存类别有:

  • Heap -堆是存储类实例化或“对象”的地方。
  • Thread stacks 线程堆栈-每个线程都有自己的调用堆栈。堆栈存储原始局部变量和对象引用以及调用堆栈(方法调用列表)本身。当堆栈帧移出上下文时,堆栈将被清理,因此此处不执行GC。
  • Metaspace 元空间-Metaspace存储对象的类定义和其他一些元数据。
  • Code cache 代码缓存-JIT编译器将它生成的本机代码存储在代码缓存中,通过重用来提高性能。
  • Buffer pools 缓冲池-许多库和框架在堆外分配缓冲区以提高性能。这些缓冲池可以用来在Java代码和本机代码之间共享内存,或者将文件的区域映射到内存中。
  • 操作系统内存——操作系统保持Java进程的堆和堆栈独立于JVM本身管理的堆和堆栈。加载的每个本机库也会消耗内存(例如libjvm.so文件). 这个通常很小。

JConsoleVisualVM可以帮助您检查其中一些类别。但是即使是这些工具也不能很好地捕获缓冲池,因为缓冲池是非堆内存使用的最危险的来源。让我们看一个例子。

直接缓冲池 Direct Buffer Pools

将以下代码放入名为Main.java类:

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

class Main {
  public static void main(String[] args) throws Exception {
    while (true) {
      List<ByteBuffer> buffers = new ArrayList<>();
      for (int i=0; i < 60; i++) {
        buffers.add(ByteBuffer.allocateDirect(5120000));
      }
    }
  }
}

这将创建一个无限循环,该循环分配许多直接缓冲区,然后释放它们。要运行它,请执行以下命令:

$ javac Main.java
$ java -Xmx512m Main

让进程继续运行,并通过在另一个终端中执行JConsole打开JConsole。然后连接到名为“Main”的应用程序。单击Memory选项卡,您将看到堆内存是稳定的(可能远低于20 MB)。

metaspace存放什么数据

切换到“非堆”,您将看到类似的内容。但是如果你在操作系统级别检查内存,你会得到不同的结果。在Linux和Mac上,可以运行jps来查找进程ID(PID),然后运行:

$ top -pid <PID>

top命令将在MEM列中显示Java进程的总内存,如下所示:

PID    COMMAND  %CPU  TIME     #TH  #WQ  #POR MEM   PURG CMPR PGRP  PPID
69959  java     133.5 00:14.41 17/1 0    71-  432M+ 0B   0B   69959 11514

这个过程实际上消耗了432MB的内存!但我们在JConsole中看不到这一点,因为调用ByteBuffer.allocateDirect在缓冲池中分配内存。通过单击MBean选项卡,然后选择java.nio.BufferPool名为“直接”的MBean。你会看到这样的画面:

metaspace存放什么数据

直接字节缓冲区对于提高性能很重要,因为它们允许本机代码和Java代码在不复制数据的情况下共享数据。但是allocateDirect方法调用的代价很高,这意味着字节缓冲区在创建之后通常会被重用。因此,有些框架会在流程的生命周期中保留它们。

您不太可能需要自己使用allocateDirect。但使用调用此方法的框架是非常常见的。一个例子是Netty,它被Play和Ratpack等流行的web框架使用。

不过,直接内存并不是隐藏JVM内存消耗的唯一来源。另一个罪魁祸首是元空间。

观察元空间Metaspace

Metaspace包含有关JVM正在运行的应用程序的元数据。它包含类定义、方法定义和有关程序的其他信息。加载到应用程序中的类越多,元空间就越大。

在旧版本的Java中,类元数据存储在堆中,这意味着对于一般开发人员来说,它并不是那么不可见。但是随着java8中Metaspace的引入,我们必须小心地显式地观察它。

大多数Java应用程序运行时的元空间都不到100mb,但是JRuby和Scala等其他JVM语言通常最多只能使用200mb。这是因为这些语言本质上是非常大的框架。他们正在Java标准库的顶部加载一个完整的标准库。

要演示这一点,请启动一个Scala REPL并在其中执行一些命令,如下所示(您可以通过运行sdk install Scala来使用SDKMAN安装Scala):

$ scala
Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_111).
Type in expressions for evaluation. Or try :help.

scala> val abcde = List('a', 'b', 'c', 'd', 'e')
abcde: List[Char] = List(a, b, c, d, e)

REPL保持运行状态,并在另一个终端中执行jconsole以打开jconsole。然后选择“MainGenericRunner”进程并连接到它。单击“内存”选项卡并从下拉列表中选择“非堆内存”。您将看到该进程已经消耗了将近70MB的非堆内存。

metaspace存放什么数据

这些内存的大部分被元空间(可能是40到50 MB)消耗,您可以在VisualVM中看到:

metaspace存放什么数据

这种情况下,您通常无能为力,但您需要小心,以确保堆设置为元空间留出足够的空间。不过,在某些情况下,您将需要以比VisualVM或JConsole所能提供的更精细的粒度来分解非堆内存。幸运的是,有一些工具可以做到这一点。

使用本机内存跟踪

本机内存跟踪(NMT)是一个JVM特性,用于跟踪内部内存使用情况。要启用它,请将以下选项添加到用于运行应用程序的java命令中:

-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

例如,可以运行先前创建的主类:

$ java -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics Main

然后获取Java进程的PID,并使用jcmd打印出进程的本机内存使用情况:

$ jcmd <PID> VM.native_memory summary
Native Memory Tracking:

Total: reserved=3554519KB, committed=542799KB
-                 Java Heap (reserved=2097152KB, committed=372736KB)
                            (mmap: reserved=2097152KB, committed=372736KB)

-                     Class (reserved=1083039KB, committed=38047KB)
                            (classes #5879)
                            (malloc=5791KB #6512)
                            (mmap: reserved=1077248KB, committed=32256KB)

-                    Thread (reserved=22654KB, committed=22654KB)
                            (thread #23)
                            (stack: reserved=22528KB, committed=22528KB)
                            (malloc=68KB #116)
                            (arena=58KB #44)

-                      Code (reserved=251925KB, committed=15585KB)
                            (malloc=2325KB #3622)
                            (mmap: reserved=249600KB, committed=13260KB)

-                        GC (reserved=82398KB, committed=76426KB)
                            (malloc=5774KB #182)
                            (mmap: reserved=76624KB, committed=70652KB)

-                  Compiler (reserved=139KB, committed=139KB)
                            (malloc=9KB #128)
                            (arena=131KB #3)

-                  Internal (reserved=6127KB, committed=6127KB)
                            (malloc=6095KB #7439)
                            (mmap: reserved=32KB, committed=32KB)

-                    Symbol (reserved=9513KB, committed=9513KB)
                            (malloc=6724KB #60789)
                            (arena=2789KB #1)

-    Native Memory Tracking (reserved=1385KB, committed=1385KB)
                            (malloc=121KB #1921)
                            (tracking overhead=1263KB)

-               Arena Chunk (reserved=186KB, committed=186KB)
                            (malloc=186KB)

jcmd工具打印每个内存类别的详细分配信息。但它不捕获直接或映射的缓冲池。元空间,主要由“类”类别表示。

通常,jcmd转储本身只起到中等作用。更常见的做法是通过运行jcmd <PID> VM.native_memory summary.diff

这是调试内存问题的一个很好的工具,但对于在生产应用程序上被动地收集遥测数据来说,它不是一个好工具。为此,您需要使用Heroku Java代理读取内存日志记录。

改进JVM内存使用的建议

以下是一些改善JVM进程内存占用的一般建议:

  1. 将最大线程堆栈大小设置为更小。-Xss512k很常见,但是在64位JVM上可以低到-Xss256k
  2. 查找未关闭的IO流。确保在finally子句中可能关闭了任何InputStream或OutputStream对象。不这样做会导致堆外内存泄漏,就像文章:https://javakk.com/160.html 所描述的那样。
  3. 将最大堆大小(-Xmx)设置为较低。太多的应用程序运行的堆远远大于它们需要的堆。堆太大实际上会影响性能,因为它会导致GC变慢,然后在运行时强制它超时工作(通常会导致长时间的暂停)。
  4. 调整glibc,方法是将MALLOC_ARENA_MAX设置为低于默认值的值,该值是CPU内核数的8倍。将此变量设置为“2”或“1”会导致内存池更少,内存可能更少,但这可能会降低性能。

更多关于metaspace存储什么的文章请参考这篇:https://javakk.com/395.html

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/831.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册