• 记录一次堆外内存和堆内存的问题


           昨天同事告诉我说,线上服务分配的内存 -Xss  大小是2G左右,但是使用top命令查看发现内存远远大于2g。将近4g了。为何堆外内存占这么大的内存空间。我刚开始也是有点一头雾水,然后搜索了下,记录下这个问题。

           我以测试环境模拟了下这个问题,首先进入k8s服务器里面。找到自己的服务,top了下。测试环境我们分配的堆-Xss 是512M 。但是使用top 发现VSZ 虚拟内存是 2G 多。

        

      ps -ef | grep java | grep -v grep   看服务器的堆内存:

      

     严重的不匹配,而且堆外内存特别大。那么堆外内存被那些所占用呢?

     搜索了下,答案如下:

    2.1. 元空间(Metaspace)

    为了维护加载类的元数据,JVM使用了一个专门的非堆区域,该区域称为元空间。在Java8之前,此区域等效于永久代。元空间或者永久代包含了加载类的元数据而非其实例,实例都保存在堆中。

    这里值得强调的是:堆大小的配置不会影响元空间。因为元数据是非堆区域。为了限制元空间的大小,我们使用如下的标志:

    • -XX:MetaspaceSize和*-XX:MaxMetaspaceSize*以设置最小和最大的元空间大小。
    • 在Java8之前,-XX:PermSize和*-XX:MaxPermSize*是设置最小和最大的永久代大小。

    2.2. 线程

    最重要的内存数据区域之一是JVM的栈,每一条线程都有一个独立维护的栈。栈保存本地变量和部分的结果,扮演方法调用的重要角色。

    默认的线程栈的大小是平台无关的,但是在大多数64位操作系统中,它的大小大约是1MB。该大小是通过*-Xss*标志设置。

    相比于其他的数据区域,**只要线程的数量没限制,栈的内存分配通常是不受约束的。**同样值得说明的是JVM自身需要一些线程去执行一些内部操作如GC和即时编译。

    2.3. 代码缓存

    为了在不同的平台上运行JVM字节码,它需要将其转换为机器码。JIT编译器负责其编译操作。

    **当JVM编译字节码到汇编指令时,它保存这些指令在一个特殊的非堆区域,该区域称为CodeCache。*JVM可以像管理其他区域一样管理CodeCache。-XX:InitialCodeCacheSize-XX:ReservedCodeCacheSize*标记决定了CodeCache的初始值和可能的最大值。

    2.4. 垃圾回收

    JVM有很多GC算法以针对不同的情形。所有的这些GC都有一个共同的特点:那就是它们需要一些非堆的结构来运行它们的任务。这些内部结构会消耗更多的本地内存。

    2.5. 符号(Symbols)

    让我们从String开始,这是在应用和库代码中使用最多的一种数据类型。因为它们的特殊性,它们通常会占据堆的很大一部分。如果很多的字符串共享同样的内容,则堆内存中的很大一部分都浪费了。

    为了解决堆空间,我们保存每一个String的唯一版本然后让其它的变量引用这个版本。该过程称为*String Interning***。因为JVM只能intern编译时间字符串常量,我们能够手动调用intern()方法在我们想去intern的字符串上。

    JVM stores interned strings in a special native fixed-sized hashtable called the String Table, also known as the *String Pool*. We can configure the table size (i.e. the number of buckets) via the -XX:StringTableSize tuning flag.

    **JVM有一个本地固定大小的哈希表(该表称为String Table)来保存内部字符串,这个表也称为String Pool。**我们能够通过-XX:StringTableSize标志配置表的大小(例如桶的数量)。

    除了String table,还有另外一个称为运行时常量池的内部区域。JVM使用该区域保存常量,例如编译时的数字字面量或者必须在运行时保存的方法和字段引用。

    2.6. 本地字节缓存

    JVM通常是自己做本地分配,到有时开发者想直接分配本地内存。通常使用的方法是通过JNI调用malloc或者NIO的直接ByteBuffers.

    (https://blog.csdn.net/JimFire/article/details/122060919)

       线程、代码缓存、垃圾回收、编译时候的常量池、本地字节缓存,而从jdk1.8之后,堆外内存还包括metaspace元空间等。那么怎么查看堆外内存占用的大小呢? jdk1.8之后,需要再启动时候添加参数:

       使得可以本地内存追踪:-XX:NativeMemoryTracking=off|summary|detail。默认地,NMT是关闭的

       比如:如果通过docker进行部署,则可以在启动配置里面加上如下参数:

        $ java -XX:NativeMemoryTracking=detail -Xms512m -Xmx512m -XX:+UseG1GC -jar yourApp.jar

      启动后,就可以借助jcmd来进行查看了。

      

       $ jcmd <pid> VM.native_memory

      比如top或者jps -l 后,可以读取到我们的pid 。然后输入 

       $ jcmd 7VM.native_memory

     

     会输出些下面的内容:

    Total: reserved=3132MB, committed=2731MB
    - Java Heap (reserved=2048MB, committed=2048MB)
    (mmap: reserved=2048MB, committed=2048MB)

    - Class (reserved=367MB, committed=123MB)
    (classes #19243)
    (malloc=3MB #38310)
    (mmap: reserved=364MB, committed=121MB)

    - Thread (reserved=241MB, committed=241MB)
    (thread #240)
    (stack: reserved=240MB, committed=240MB)
    (malloc=1MB #1213)

    - Code (reserved=261MB, committed=107MB)
    (malloc=17MB #23058)
    (mmap: reserved=244MB, committed=90MB)

    - GC (reserved=144MB, committed=144MB)
    (malloc=36MB #53273)
    (mmap: reserved=108MB, committed=108MB)

    - Compiler (reserved=1MB, committed=1MB)
    (malloc=1MB #2321)

    - Internal (reserved=30MB, committed=30MB)
    (malloc=30MB #37662)

    - Symbol (reserved=27MB, committed=27MB)
    (malloc=23MB #243827)
    (arena=4MB #1)

    - Native Memory Tracking (reserved=6MB, committed=6MB)
    (tracking overhead=6MB)

    - Arena Chunk (reserved=4MB, committed=4MB)
    (malloc=4MB)

    - Unknown (reserved=4MB, committed=0MB)
    (mmap: reserved=4MB, committed=0MB)
     
     
    说明:
     

    NMT打印我们的堆内存,确实像我们前面设置的一样:

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

    3.4. 元数据区(Metaspace)

    下面是加载的类的类元数据的NMT信息:

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

    大约1GB的保留和45MB的使用空间区加载6566个类。

    3.5. 线程

    下面是线程的内存分配信息:

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

    总共,36MB的内存被分配到了37个线程——大约每个栈有1MB的内存。JVM分配内存到线程是在JVM创建之初,所以保留内存和使用内存是相等的。

    3.6. Code Cache

    让我们看看JIT产生的汇编指令所占的空间:

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

    当前,大约13MB的空间被会缓存了,并且能使用的空间大约在245MB。

    3.7. GC

    下面是NMT报告的G1GC的内存使用情况:

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

    我们能看到,大约60MB的空间是G1可以保留和使用的。

    SerialGC仅仅使用1MB:

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

    明显地,我们不应该挑选一个GC算法仅仅是因为内存使用,作为SerialGC的STW(stop-the-world)属性可能导致性能下降。然而,有其他几种GC可供选择,并且它们在不同情况下能够平衡内存和性能。

    3.8. 符号(Symbol)

    下面是NMT打印有关符号的分配,如同String table和常量池:

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

    大约10MB被分配给符号使用。

          另外就是发现线上服务器这几天总是cpu飙到80%以上,可以通过arthas 的  heapdump /tmp/dump.hprof 命令生成dump文件,然后借助Jprofile进行dump文件的分析。这个稍后再做整理

        

  • 相关阅读:
    linux gcc安装
    重装win7后如何恢复ubuntu引导
    Eclipse搭建Android开发环境(安装ADT,Android4.4.2)
    mysql变量使用总结
    最快得到MYSQL两个表的差集
    mysqldb
    更改时间 (时分秒)
    使用命令转移文件
    报喜啦~过了!
    Jmeter接口测试示例
  • 原文地址:https://www.cnblogs.com/thinkingandworkinghard/p/16552864.html
Copyright © 2020-2023  润新知