精华内容
下载资源
问答
  • Java堆外内存使用

    千次阅读 2018-10-24 21:49:21
    JVM内部会把所有内存分成Java使用的堆内存和Native使用的内存,它们之间是不能共享的,就是说当你的Native内存用完了时,如果Java堆又有空闲内存,这时Native会重新向Jvm申请,而不是直接使用Java堆内存。...

    个人博客请访问 http://www.x0100.top 

    JVM内部会把所有内存分成Java使用的堆内存和Native使用的内存,它们之间是不能共享的,就是说当你的Native内存用完了时,如果Java堆又有空闲内存,这时Native会重新向Jvm申请,而不是直接使用Java堆内存。

    使用堆外内存,就是为了能直接分配和释放内存,提高效率。JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。

    使用ByteBuffer分配本地内存则非常简单,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)即可。

    C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中我们需要手动释放获取的堆外内存吗?

    我们一起来看看NIO中提供的ByteBuffer

    我们将最大堆外内存设置成40M,运行这段代码会发现:程序可以一直运行下去,不会报OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,会发现程序频繁的进行垃圾回收活动。那么DirectByteBuffer究竟是如何释放堆外内存的?

    我们修改下JVM的启动参数,重新运行之前的代码:

     

    与之前的JVM启动参数相比,增加了-XX:+DisableExplicitGC,这个参数作用是禁止代码中显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。

     

    显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。

    我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。

    Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

    Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。

    使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。

    堆外内存的好处是:

    (1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

    (2)理论上能减少GC暂停时间;

    (3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

    (4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

    站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的

    基于GC的回收

    存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的Cleaner,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。

    更多精彩内容扫描下方二维码进入网站。。。。。

    关注微信公众号。。。。。

     

    展开全文
  • Java堆外内存增长问题排查Case

    千次阅读 2018-11-27 20:00:53
    Java堆外内存增长问题排查Case 最近排查一个线上java服务常驻内存异常高的问题,大概现象是:java堆Xmx配置了8G,但运行一段时间后常驻内存RES从5G逐渐增长到13G #补图#,导致机器开始swap从而服务整体变慢。 由于...

    Java堆外内存增长问题排查Case

    最近排查一个线上java服务常驻内存异常高的问题,大概现象是:java堆Xmx配置了8G,但运行一段时间后常驻内存RES从5G逐渐增长到13G #补图#,导致机器开始swap从而服务整体变慢。
    由于Xmx只配置了8G但RES常驻内存达到了13G,多出了5G堆外内存,经验上判断这里超出太多不太正常。

    前情提要–JVM内存模型

    开始逐步对堆外内存进行排查,首先了解一下JVM内存模型。根据JVM规范,JVM运行时数据区共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
    o_dc695f48-4189-4fc7-b950-ed25f6c1521708518830

    • 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。虚拟机栈除了上述错误外,还有另一种错误,那就是当申请不到空间时,会抛出 OutOfMemoryError。
    • 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。本地方法栈也可以抛出StackOverflowError和OutOfMemoryError。
    • PC 寄存器,也叫程序计数器。可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。倘若当前线程执行的是 JAVA 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。
    • 堆内存。堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。
    • 方法区也是所有线程共享。主要用于存储类的信息、常量池、静态变量、及时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
       

    前情提要–PermGen(永久代)和 Metaspace(元空间)

    PermGen space 和 Metaspace是HotSpot对于方法区的不同实现。在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类名,父类名,类的类型,访问修饰符,字段信息,方法信息,静态变量,常量,类加载器的引用,类的引用。在HotSpot JDK 1.8之前这些类元数据信息存放在一个叫永久代的区域(PermGen space),永久代一段连续的内存空间。在JDK 1.8开始,方法区实现采用Metaspace代替,这些元数据信息直接使用本地内存来分配。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

    堆外内存

    java 8下是指除了Xmx设置的java堆(java 8以下版本还包括MaxPermSize设定的持久代大小)外,java进程使用的其他内存。主要包括:DirectByteBuffer分配的内存,JNI里分配的内存,线程栈分配占用的系统内存,jvm本身运行过程分配的内存,codeCache,java 8里还包括metaspace元数据空间。

    分析java堆

    由于现象是RES比较高,先看一下java堆是否有异常。把java堆dump下来仔细排查一下,jmap -histo:live pid,发现整个堆回收完也才几百兆,远不到8G的Xmx的上限值,GC日志看着也没啥异常。基本排查java堆内存泄露的可能性。

    分析DirectByteBuffer的占用

    DirectByteBuffer简单了解

    由于服务使用的RPC框架底层采用了Netty等NIO框架,会使用到DirectByteBuffer这种“冰山对象”,先简单排查一下。关于DirectByteBuffer先介绍一下:JDK 1.5之后ByteBuffer类提供allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现,会调用malloc方法进行内存分配。实际上,在java堆里是维护了一个记录堆外地址和大小的DirectByteBuffer的对象,所以GC是能通过操作DirectByteBuffer对象来间接操作对应的堆外内存,从而达到释放堆外内存的目的。但如果一旦这个DirectByteBuffer对象熬过了young GC到达了Old区,同时Old区一直又没做CMS GC或者Full GC的话,这些“冰山对象”会将系统物理内存慢慢消耗掉。对于这种情况JVM留了后手,Bits给DirectByteBuffer前首先需要向Bits类申请额度,Bits类维护了一个全局的totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限(堆外内存的限额默认与堆内内存Xmx设定相仿),如果已经超限,会主动执行Sytem.gc(),System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存。但如果启动时通过-DisableExplicitGC禁止了System.gc(),那么这里就会出现比较严重的问题,导致回收不了DirectByteBuffer底下的堆外内存了。所以在类似Netty的框架里对DirectByteBuffer是框架自己主动回收来避免这个问题。

    DirectByteBuffer为什么要用堆外内存

    DirectByteBuffer是直接通过native方法使用malloc分配内存,这块内存位于java堆之外,对GC没有影响;其次,在通信场景下,堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。所以DirectByteBuffer一般用于通信过程中作为缓冲池来减少内存拷贝。当然,由于直接用malloc在OS里申请一段内存,比在已申请好的JVM堆内内存里划一块出来要慢,所以在Netty中一般用池化的 PooledDirectByteBuf 对DirectByteBuffer进行重用进一步提升性能。

    如何排查DirectByteBuffer的使用情况

    JMX提供了监控direct buffer的MXBean,启动服务时开启-Dcom.sun.management.jmxremote.port=9527 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=10.79.40.147,JMC挂上后运行一段时间,此时Xmx是8G的情况下整体RES逐渐增长到13G,MBean里找到java.nio.BufferPool下的direct节点,查看direct buffer的情况,发现总共才213M。为了进一步排除,在启动时通过-XX:MaxDirectMemorySize来限制DirectByteBuffer的最大限额,调整为1G后,进程整体常驻内存的增长并没有限制住,因此这里基本排除了DirectByteBuffer的嫌疑。

    使用NMT排查JVM原生内存使用

    Native Memory Tracking(NMT)使用

    NMT是Java7U40引入的HotSpot新特性,可用于监控JVM原生内存的使用,但比较可惜的是,目前的NMT不能监控到JVM之外或原生库分配的内存。java进程启动时指定开启NMT(有一定的性能损耗),输出级别可以设置为“summary”或“detail”级别。如:

    -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail
    

    开启后,通过jcmd可以访问收集到的数据。

    jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff 
    

    如:jcmd 11 VM.native_memory,输出如下:

    Native Memory Tracking:
    
    Total: reserved=12259645KB(保留内存), committed=11036265KB (提交内存)
    堆内存使用情况,保留内存和提交内存和Xms、Xmx一致,都是8G。
    -                 Java Heap (reserved=8388608KB, committed=8388608KB)
                                (mmap: reserved=8388608KB, committed=8388608KB)
    用于存储类元数据信息使用到的原生内存,总共12045个类,整体实际使用了79M内存。
    -                     Class (reserved=1119963KB, committed=79751KB)
                                (classes #12045)
                                (malloc=1755KB #29277)
                                (mmap: reserved=1118208KB, committed=77996KB)
    总共2064个线程,提交内存是2.1G左右,一个线程1M,和设置Xss1m相符。
    -                    Thread (reserved=2130294KB, committed=2130294KB)
                                (thread #2064)
                                (stack: reserved=2120764KB, committed=2120764KB)
                                (malloc=6824KB #10341)
                                (arena=2706KB #4127)
    JIT的代码缓存,12045个类JIT编译后代码缓存整体使用79M内存。
    -                      Code (reserved=263071KB, committed=79903KB)
                                (malloc=13471KB #15191)
                                (mmap: reserved=249600KB, committed=66432KB)
    GC相关使用到的一些堆外内存,比如GC算法的处理锁会使用一些堆外空间。118M左右。
    -                        GC (reserved=118432KB, committed=118432KB)
                                (malloc=93848KB #453)
                                (mmap: reserved=24584KB, committed=24584KB)
    JAVA编译器自身操作使用到的一些堆外内存,很少。
    -                  Compiler (reserved=975KB, committed=975KB)
                                (malloc=844KB #1074)
                                (arena=131KB #3)
    Internal:memory used by the command line parser, JVMTI, properties等。
    -                  Internal (reserved=117158KB, committed=117158KB)
                                (malloc=117126KB #44857)
                                (mmap: reserved=32KB, committed=32KB)
    Symbol:保留字符串(Interned String)的引用与符号表引用放在这里,17M左右
    -                    Symbol (reserved=17133KB, committed=17133KB)
                                (malloc=13354KB #145640)
                                (arena=3780KB #1)
    NMT本身占用的堆外内存,4M左右
    -    Native Memory Tracking (reserved=4402KB, committed=4402KB)
                                (malloc=396KB #5287)
                                (tracking overhead=4006KB)
    不知道啥,用的很少。
    -               Arena Chunk (reserved=272KB, committed=272KB)
                                (malloc=272KB)
    其他未分类的堆外内存占用,100M左右。
    -                   Unknown (reserved=99336KB, committed=99336KB)
                                (mmap: reserved=99336KB, committed=99336KB)
    
    • 保留内存(reserved):reserved memory 是指JVM 通过mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用,且保证了逻辑地址的连续性,能简化指针运算。

    • 提交内存(commited):committed memory 是JVM向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,仍然会page faults,但是跟 reserved 不同,完全内核处理像什么也没发生一样。

    这里需要注意的是:由于malloc/mmap的lazy allocation and paging机制,即使是commited的内存,也不一定会真正分配物理内存。

    malloc/mmap is lazy unless told otherwise. Pages are only backed by physical memory once they're accessed.
    

    Tips:由于内存是一直在缓慢增长,因此在使用NMT跟踪堆外内存时,一个比较好的办法是,先建立一个内存使用基线,一段时间后再用当时数据和基线进行差别比较,这样比较容易定位问题。

    jcmd 11 VM.native_memory baseline
    

    同时pmap看一下物理内存的分配,RSS占用了10G。

    pmap -x 11 | sort -n -k3
    

    运行一段时间后,做一下summary级别的diff,看下内存变化,同时再次pmap看下RSS增长情况。

    jcmd 11 VM.native_memory summary.diff
    Native Memory Tracking:
    
    Total: reserved=13089769KB +112323KB, committed=11877285KB +117915KB
    
    -                 Java Heap (reserved=8388608KB, committed=8388608KB)
                                (mmap: reserved=8388608KB, committed=8388608KB)
    
    -                     Class (reserved=1126527KB +2161KB, committed=85771KB +2033KB)
                                (classes #12682 +154)
                                (malloc=2175KB +113KB #37289 +2205)
                                (mmap: reserved=1124352KB +2048KB, committed=83596KB +1920KB)
    
    -                    Thread (reserved=2861485KB +94989KB, committed=2861485KB +94989KB)
                                (thread #2772 +92)
                                (stack: reserved=2848588KB +94576KB, committed=2848588KB +94576KB)
                                (malloc=9169KB +305KB #13881 +460)
                                (arena=3728KB +108 #5543 +184)
    
    -                      Code (reserved=265858KB +1146KB, committed=94130KB +6866KB)
                                (malloc=16258KB +1146KB #18187 +1146)
                                (mmap: reserved=249600KB, committed=77872KB +5720KB)
    
    -                        GC (reserved=118433KB +1KB, committed=118433KB +1KB)
                                (malloc=93849KB +1KB #487 +24)
                                (mmap: reserved=24584KB, committed=24584KB)
    
    -                  Compiler (reserved=1956KB +253KB, committed=1956KB +253KB)
                                (malloc=1826KB +253KB #2098 +271)
                                (arena=131KB #3)
    
    -                  Internal (reserved=203932KB +13143KB, committed=203932KB +13143KB)
                                (malloc=203900KB +13143KB #62342 +3942)
                                (mmap: reserved=32KB, committed=32KB)
    
    -                    Symbol (reserved=17820KB +108KB, committed=17820KB +108KB)
                                (malloc=13977KB +76KB #152204 +257)
                                (arena=3844KB +32 #1)
    
    -    Native Memory Tracking (reserved=5519KB +517KB, committed=5519KB +517KB)
                                (malloc=797KB +325KB #9992 +3789)
                                (tracking overhead=4722KB +192KB)
    
    -               Arena Chunk (reserved=294KB +5KB, committed=294KB +5KB)
                                (malloc=294KB +5KB)
    
    -                   Unknown (reserved=99336KB, committed=99336KB)
                                (mmap: reserved=99336KB, committed=99336KB
    

    发现这段时间pmap看到的RSS增长了3G多,但NMT观察到的内存增长了不到120M,还有大概2G多常驻内存不知去向,因此也基本排除了由于JVM自身管理的堆外内存的嫌疑。

    排查Metaspace元空间的堆外内存占用

    由于线上使用的是JDK8,前面提到,JDK8里的元空间实际上使用的也是堆外内存,默认没有设置元空间大小的情况下,元空间最大堆外内存大小和Xmx是一致的。JMC连上后看下内存tab下metaspace一栏的内存占用情况,发现元空间只占用不到80M内存,也排除了它的可能性。实在不放心的话可以通过-XX:MaxMetaspaceSize设置元空间使用堆外内存的上限。

    gdb分析内存块内容

    上面提到使用pmap来查看进程的内存映射,pmap命令实际是读取了/proc/pid/maps和/porc/pid/smaps文件来输出。发现一个细节,pmap取出的内存映射发现很多64M大小的内存块。这种内存块逐渐变多且占用的RSS常驻内存也逐渐增长到reserved保留内存大小,内存增长的2G多基本上也是由于这些64M的内存块导致的,因此看一下这些内存块里具体内容。

    strace挂上监控下内存分配和回收的系统调用:

    strace -o /data1/weibo/logs/strace_output2.txt -T -tt -e mmap,munmap,mprotect -fp 12
    

    看内存申请和释放的情况:

    cat ../logs/strace_output2.txt | grep mprotect | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5
    
    cat ../logs/strace_output2.txt | grep mmap | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5
    
    cat ../logs/strace_output2.txt | grep munmap | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5
    

    配合pmap -x 10看一下实际内存分配情况:

    找一块内存块进行dump:

    gdb --batch --pid 11 -ex "dump memory a.dump 0x7fd488000000 0x7fd488000000+56124000"
    

    简单分析一下内容,发现绝大部分是乱码的二进制内容,看不出什么问题。
    strings a.dump | less
    或者: hexdump -C a.dump | less
    或者: view a.dump

    没啥思路的时候,随便搜了一下发现貌似很多人碰到这种64M内存块的问题(比如这里),了解到glibc的内存分配策略在高版本有较大调整:

    «从glibc 2.11(为应用系统在多核心CPU和多Sockets环境中高伸缩性提供了一个动态内存分配的特性增强)版本开始引入了per thread arena内存池,Native Heap区被打散为sub-pools ,这部分内存池叫做Arena内存池。也就是说,以前只有一个main arena,目前是一个main arena(还是位于Native Heap区) + 多个per thread arena,多个线程之间不再共用一个arena内存区域了,保证每个线程都有一个堆,这样避免内存分配时需要额外的锁来降低性能。main arena主要通过brk/sbrk系统调用去管理,per thread arena主要通过mmap系统调用去分配和管理。»

    «一个32位的应用程序进程,最大可创建 2 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为1MB,一个64位的应用程序进程,最大可创建 8 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为64MB»

    ptmalloc2内存分配和释放

    «当某一线程需要调用 malloc()分配内存空间时, 该线程先查看线程私有变量中是否已经存在一个分配区,如果存在, 尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果失败, 该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经加锁,那么 malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。用户 free 掉的内存并不是都会马上归还给系统,ptmalloc2 会统一管理 heap 和 mmap 映射区域中的空闲的chunk,当用户进行下一次分配请求时, ptmalloc2 会首先试图在空闲的chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。»

    ptmalloc2的内存收缩机制

    «业务层调用free方法释放内存时,ptmalloc2先判断 top chunk 的大小是否大于 mmap 收缩阈值(默认为 128KB),如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的 128KB 空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配 请求;如果为非主分配区,会进行 sub-heap 收缩,将 top chunk 的一部分返回给操 作系统,如果 top chunk 为整个 sub-heap,会把整个 sub-heap 还回给操作系统。做 完这一步之后,释放结束,从 free() 函数退出。可以看出,收缩堆的条件是当前 free 的 chunk 大小加上前后能合并 chunk 的大小大于 64k,并且要 top chunk 的大 小要达到 mmap 收缩阈值,才有可能收缩堆。»

    ptmalloc2的mmap分配阈值动态调整

    «M_MMAP_THRESHOLD 用于设置 mmap 分配阈值,默认值为 128KB,ptmalloc 默认开启 动态调整 mmap 分配阈值和 mmap 收缩阈值。当用户需要分配的内存大于 mmap 分配阈值,ptmalloc 的 malloc()函数其实相当于 mmap() 的简单封装,free 函数相当于 munmap()的简单封装。相当于直接通过系统调用分配内存, 回收的内存就直接返回给操作系统了。因为这些大块内存不能被 ptmalloc 缓存管理,不能重用,所以 ptmalloc 也只有在万不得已的情况下才使用该方式分配内存。»

    业务特性和ptmalloc2内存分配的gap

    当前业务并发较大,线程较多,内存申请时容易造成锁冲突申请多个arena,另外该服务涉及到图片的上传和处理,底层会比较频繁的通过JNI调用ImageIO的图片读取方法(com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage),经常会向glibc申请10M以上的buffer内存,考虑到ptmalloc2的lazy回收机制和mmap分配阈值动态调整默认打开,对于这些申请的大内存块,使用完后仍然会停留在arena中不会归还,同时也比较难得到收缩的机会去释放(当前回收的chunk和top chunk相邻,且合并后大于64K)。因此在这种较高并发的多线程业务场景下,RES的增长也是不可避免。

    如何优化解决

    三种方案:

    第一种:控制分配区的总数上限。默认64位系统分配区数为:cpu核数*8,如当前环境16核系统分配区数为128个,每个64M上限的话最多可达8G,限制上限后,后续不够的申请会直接走mmap分配和munmap回收,不会进入ptmalloc2的buffer池。
    所以第一种方案调整一下分配池上限个数到4:

    export MALLOC_ARENA_MAX=4
    

    第二种:之前降到ptmalloc2默认会动态调整mmap分配阈值,因此对于较大的内存请求也会进入ptmalloc2的内存buffer池里,这里可以去掉ptmalloc的动态调整功能。可以设置 M_TRIM_THRESHOLD,M_MMAP_THRESHOLD,M_TOP_PAD 和 M_MMAP_MAX 中的任意一个。这里可以固定分配阈值为128K,这样超过128K的内存分配请求都不会进入ptmalloc的buffer池而是直接走mmap分配和munmap回收(性能上会有损耗,当前环境大概10%)。:

    export MALLOC_MMAP_THRESHOLD_=131072
    export MALLOC_TRIM_THRESHOLD_=131072
    export MALLOC_TOP_PAD_=131072
    export MALLOC_MMAP_MAX_=65536   
    

    第三种:使用tcmalloc来替代默认的ptmalloc2。google的tcmalloc提供更优的内存分配效率,性能更好,ThreadCache会阶段性的回收内存到CentralCache里。 解决了ptmalloc2中arena之间不能迁移导致内存浪费的问题。

    tcmalloc安装使用

    1.实现原理

    perf-tools实现原理是:在java应用程序运行时,当系统分配内存时调用malloc时换用它的libtcmalloc.so,也就是TCMalloc会自动替换掉glibc默认的malloc和free,这样就能做一些统计。使用TCMalloc(Thread-Caching Malloc)与标准的glibc库的malloc相比,TCMalloc在内存的分配上效率和速度要高,==了解更多TCMalloc

    2. 安装和使用

    2.1 前置工具的安装

    yum -y install gcc make
    yum -y install gcc gcc-c++
    yum -y perl
    

    2.2 libunwind

    使用perf-tools的TCMalloc,在64bit系统上需要先安装libunwind(http://download.savannah.gnu.org/releases/libunwind/libunwind-1.2.tar.gz,只能是这个版本),这个库为基于64位CPU和操作系统的程序提供了基本的堆栈辗转开解功能,其中包括用于输出堆栈跟踪的API、用于以编程方式辗转开解堆栈的API以及支持C++异常处理机制的API,32bit系统不需安装。

    tar zxvf libunwind-1.2.tar.gz
    ./configure
    make
    make install
    make clean
    

    2.3 perf-tools

    https://github.com/gperftools/gperftools下载相应的google-perftools版本。

    tar zxvf google-perftools-2.7.tar.gz
    ./configure
    make
    make install
    make clean
    #修改lc_config,加入/usr/local/lib(libunwind的lib所在目录)
    echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf 
    #使libunwind生效
    ldconfig
    

    2.3.1 关于etc/ld.so.conf

    这个文件记录了编译时使用的动态链接库的路径。默认情况下,编译器只会使用/lib和/usr/lib这两个目录下的库文件。
    如果你安装了某些库,比如在安装gtk+-2.4.13时它会需要glib-2.0 >= 2.4.0,辛苦的安装好glib后没有指定 –prefix=/usr 这样glib库就装到了/usr/local下,而又没有在/etc/ld.so.conf中添加/usr/local/lib。
    库文件的路径如 /usr/lib 或 /usr/local/lib 应该在 /etc/ld.so.conf 文件中,这样 ldd 才能找到这个库。在检查了这一点后,要以 root 的身份运行 /sbin/ldconfig。
    将/usr/local/lib加入到/etc/ld.so.conf中,这样安装gtk时就会去搜索/usr/local/lib,同样可以找到需要的库

    2.3.2 关于ldconfig

    ldconfig的作用就是将/etc/ld.so.conf列出的路径下的库文件 缓存到/etc/ld.so.cache 以供使用
    因此当安装完一些库文件,(例如刚安装好glib),或者修改ld.so.conf增加新的库路径后,需要运行一下/sbin/ldconfig
    使所有的库文件都被缓存到ld.so.cache中,如果没做,即使库文件明明就在/usr/lib下的,也是不会被使用的

    2.4 为perf-tools添加线程目录

    mkdir /data1/weibo/logs/gperftools/tcmalloc/heap
    chmod 0777 /data1/weibo/logs/gperftools/tcmalloc/heap
    

    2.5 修改tomcat启动脚本

    catalina.sh里添加:

    ldconfig
    export LD_PRELOAD=/usr/local/lib/libtcmalloc.so
    export HEAPPROFILE=/data1/weibo/logs/gperftools/tcmalloc/heap
    

    修改后重启tomcat的容器。

    2.5.1 关于LD_PRELOAD

    LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。更多关于LD_PRELOAD

    经验证上面三种方式都能有效解决常驻内存持续增长的问题。

    参考:

    展开全文
  • Java堆外内存使用

    千次阅读 2014-12-31 15:20:35
    最近经常有人问我在Java使用堆外(off heap)内存的好处与用途何在。我想其他面临几样选择的人应该也会对这个答案感兴趣吧。 堆外内存其实并无特别之处。线程栈,应用程序代码,NIO缓存用的都是堆外内存。事实上...

    最近经常有人问我在Java中使用堆外(off heap)内存的好处与用途何在。我想其他面临几样选择的人应该也会对这个答案感兴趣吧。

    堆外内存其实并无特别之处。线程栈,应用程序代码,NIO缓存用的都是堆外内存。事实上在C或者C++中,你只能使用未托管内存,因为它们默认是没有托管堆(managed heap)的。在Java中使用托管内存或者“堆”内存是这门语言的一个特性。注意:Java并非唯一这么做的语言。

    new Object() vs 对象池 vs 堆外内存

    new Object()

    在Java 5.0以前,对象池一度非常流行。那个时候创建对象的开销是非常昂贵的。然而,从Java 5.0以后,对象创建及垃圾回收已经变得非常廉价了,开发人员发现性能得到了提升后,便简化了代码,废弃了对象池,需要的时候就去创建新的对象就好了。在Java 5.0以前,几乎所有对象,包括对象池本身,都通过对象池来提升性能,而在5.0以后,只有那些特别昂贵的对象才有必要池化了,比方说线程,Socket,以及数据库连接。

    对象池

    在低时延领域它仍是有一定的用武之处的,由于可变对象的循环使用减轻了CPU缓存的压力,进而使得性能得到了提升。这些对象的生命周期和结构都必须尽可能简单,但这么做之后你会发现系统性能及抖动都会得到大幅度的改善。

    还有一个领域也比较适合使用对象池,譬如需要加载海量数据且其中包含许多冗余对象时。使用对象池能显著减少内存的使用量以及需要GC的对象数,进而换来更短的GC时间以及更高的吞吐量。

    这类对象池通常都会设计得比较轻量级,而非简单地使用一个同步的HashMap,因此它们仍是有存在的价值的。

    StringInterner类来作一个例子。你可以将一个包含你想要的文本的可重复使用的可变StringBuilder作为参数传给它,它会返回你一个匹配的字符串。直接传递String对象的效率会很低,因为你已经把这个对象创建出来了。StringBuilder则是可以重复使用的。

    注意:这个结构有一个很有意思的特性就是它不需要额外的线程安全的机制,比方说volatile或者synchronized,仅需Java所保障的最低限度的线程安全就足够了。你能正确地访问到String内部的final字段,顶多就是读到了不一致的引用而已。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    publicclass StringInterner {
        privatefinal String[] interner;
        privatefinal int mask;
        publicStringInterner(intcapacity) {
            intn = Maths.nextPower2(capacity, 128);
            interner = newString[n];
            mask = n - 1;
        }
     
        privatestatic boolean isEqual(@NullableCharSequence s, @NotNullCharSequence cs) {
            if(s == null)returnfalse;
            if(s.length() != cs.length()) returnfalse;
            for(inti = 0; i < cs.length(); i++)
                if(s.charAt(i) != cs.charAt(i))
                    returnfalse;
            returntrue;
        }
     
        @NotNull
        publicString intern(@NotNullCharSequence cs) {
            longhash = 0;
            for(inti = 0; i < cs.length(); i++)
                hash = 57* hash + cs.charAt(i);
            inth = (int) Maths.hash(hash) & mask;
            String s = interner[h];
            if(isEqual(s, cs))
                returns;
            String s2 = cs.toString();
            returninterner[h] = s2;
        }
    }


    堆外内存的使用

    使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。

    堆外内存的好处是:

    1. 可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间。
    2. 理论上能减少GC暂停时间。
    3. 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现。
    4. 它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据。

    站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的。

    堆外内存及测试

    高性能计算领域最大的一个难点在于重现那些隐蔽的BUG,并证实问题已经得到修复。通过将输入事件及数据以持久化的形式存储到堆外内存中,你可以将你的关键系统变成一系列的复杂状态机。(简单的情况下只有一个状态机)。这样的话在测试环境便能够复现出生产环境出现的行为及性能问题了。

    许多投行都通过这项技术来可靠地重现当天系统对某个事件的响应,并分析出该事件之所以这么处理的原因。更为重要的是,你能够立即证明线上的故障已经得到了解决,而不是发现一个问题后,寄希望于它就是引发线上故障的根源。确定性的行为还伴随着确定性的性能。

    你可以在测试环境中按照真实的时间来回放事件,由此得到的时延分布也必定是生产环境中所出现的。由于硬件的不同,一些系统的抖动可能难以复现,不过这在数据分析的角度而言已经相当接近真实的情况了。为了避免出现花一整天的时间来回话前一天的数据的情况,你还可以增加一个阈值,比方说,如果两个事件的间隔超过10ms的话你可以就只等待10ms。这样你能够在一个小时内根据实际的时间来回放出一天的事件,来检查下你的改动是否对时延分布有所改善。

    这样做是否就损失了“一次编译,处处执行”的好处了?

    一定程度上来讲是这样的,但其实的影响比你想像的要小得多。越接近处理器,你就更依赖于处理器或者操作系统的行为。所幸的是,绝大多数系统使用的都是AMD/Intel的CPU,甚至是ARM处理器在底层上也越来越与这两家兼容了。操作系统之间也存在差别,因此相对于Windows而言,这项技术更适合在Linux系统上使用。如果你是在Mac OS X或者Windows上开发,然后生产环境是部署在Linux上的话,就一点问题都没有了。我们在Higher Frequency Trading中也是这么做的。

    使用堆外内存会引入什么新的问题

    天下没有免费的午餐,堆外内存也不例外。最大的问题在于你的数据结构变得有些别扭。要么就是需要一个简单的数据结构以便于直接映射到堆外内存,要么就使用复杂的数据结构并序列化及反序列化到内存中。很明显使用序列化的话会比较头疼且存在性能瓶颈。使用序列化比使用堆对象的性能还差。

    在金融领域,许多高频率的数据都是扁平的简单结构,全部由基础类型组成,非常适合映射到堆外内存。然而,并非所有的应用程序都是这样的,可能会有一些嵌套得很深的数据结构,比如说图,你还不得不将这些对象缓存在堆上。

    另外一个问题就是JVM会制约到你对操作系统的使用。你不用再担心JVM会给系统造成过重的负载。使用堆外内存后,某些限制已经不复存在了,你可以使用比主存还大的数据结构,不过如果你这么做的话又得考虑一下使用的是什么磁盘子系统了。比如说,你肯定不会希望分页到一块只有80 IOPS(Input/Ouput Operations per Second,每秒的IO操作)的HDD硬盘上,最好是IOPS能到80,000的SSD硬盘,当然了,1000x的话更好。

    OpenHFT能做些什么?

    OpenHFT包含许多类库,它们向你屏蔽了使用本地内存来存储数据的细节。这些数据结构都是持久化的,使用它们不会产生垃圾或者只有很少。使用了它的应用程序可以运行一整天也没有一次Minor GC.

    Chronicle Queue——持久化的事件队列。支持同一台机器上多个JVM的并发写,以及多台机器间的并发读。微秒级的延迟,并能持续保持每秒上百万消息的吞吐量。

    Chronicle Map——kv表的本地或持久化存储。它能在同一台机器的不同JVM间共享,数据是通过UDP或者TCP来复制的,并通过TCP来进行远程访问。微秒级的延迟,单台机器能保持每秒百万级的读写操作。

    Thread Affinity ——将关键线程绑定到独立的CPU核或者逻辑CPU上,以减少系统抖动。抖动可以减小到原来的千分之一。

    使用哪个API?

    如果你需要记录每个事件的话 ——> Chronicle Queue

    如果你只需要某个唯一主键最近的一条结果 ——> Chronicle Map

    如果你更关心那20微秒的抖动的话 ——> Thread Affinity

    总结

    堆外内存是把双刃剑。它的价值你已经看到了,可以和别的实现可伸缩性的方案进行下比较。与在堆缓存或者消息队列,甚至是进程外的数据库中进行分区/分片相比,使用堆外内存要更为简单高效。不仅如此,以前用来提升性能的某些技巧也已经不再需要了。比如说,堆外内存可以支持操作系统的同步写,就不再需要异步去执行了,那样还会面临数据丢失的风险。不过最大的好处应该就是启动时间了,生产环境下的系统的重启速度会大大缩短,映射1 TB的数据只需要10毫秒,同时你还能在测试环境按生产环境的顺序复现每一个事件,以还原线上现场。通过它你可以建立起一个可靠的质量体系。

    文章转自:http://it.deepinmind.com/jvm/2014/12/26/heap-vs-heap-memory-usage.html

    展开全文
  • Java堆外内存:堆外内存回收方法

    千次阅读 2019-10-24 17:36:17
    一、JVM内存的分配及垃圾回收  对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下:  新生代:一般来说新创建的对象都分配在这里。  年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面...

    一、JVM内存的分配及垃圾回收

      对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下:

      新生代:一般来说新创建的对象都分配在这里。

      年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面。年老代中的对象保存的时间更久。

      永久代:这里面存放的是class相关的信息,一般是不会进行垃圾回收的。

    JVM垃圾回收

      由于JVM会替我们执行垃圾回收,因此开发者根本不需要关心对象的释放。但是如果不了解其中的原委,很容易内存泄漏,只能两眼望天了!

      垃圾回收,大致可以分为下面几种:

      Minor GC:当新创建对象,内存空间不够的时候,就会执行这个垃圾回收。由于执行最频繁,因此一般采用复制回收机制。

      Major GC:清理年老代的内存,这里一般采用的是标记清除+标记整理机制。

      Full GC:有的说与Major GC差不多,有的说相当于执行minor+major回收,那么我们暂且可以认为Full GC就是全面的垃圾回收吧。

    二、堆外内存溢出

    从nio时代开始,可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)即可,如下:

    ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);

    像Memcached等等很多缓存框架都会使用堆外内存,以提高效率,反复读写,去除它的GC的影响。可以通过指定JVM参数来确定堆外内存大小限制(有的VM默认是无限的,比如JRocket,JVM默认是64M): 

    -XX:MaxDirectMemorySize=512m

    对于这种direct buffer内存不够的时候会抛出错误: 

    java.lang.OutOfMemoryError: Direct buffer memory

    对于heap的OOM我们可以通过执行jmap -heap来获取堆内内存情况,例如以下输出取自我上周定位的一个问题: 

    复制代码

    using parallel threads in the new generation.
    using thread-local object allocation.
    Concurrent Mark-Sweep GC
     
    Heap Configuration:
       MinHeapFreeRatio = 40
       MaxHeapFreeRatio = 70
       MaxHeapSize      = 2147483648 (2048.0MB)
       NewSize          = 16777216 (16.0MB)
       MaxNewSize       = 33554432 (32.0MB)
       OldSize          = 50331648 (48.0MB)
       NewRatio         = 7
       SurvivorRatio    = 8
       PermSize         = 16777216 (16.0MB)
       MaxPermSize      = 67108864 (64.0MB)
     
    Heap Usage:
    New Generation (Eden + 1 Survivor Space):
       capacity = 30212096 (28.8125MB)
       used     = 11911048 (11.359260559082031MB)
       free     = 18301048 (17.45323944091797MB)
       39.42476549789859% used
    Eden Space:
       capacity = 26869760 (25.625MB)
       used     = 11576296 (11.040016174316406MB)
       free     = 15293464 (14.584983825683594MB)
       43.08298994855183% used
    From Space:
       capacity = 3342336 (3.1875MB)
       used     = 334752 (0.319244384765625MB)
       free     = 3007584 (2.868255615234375MB)
       10.015510110294118% used
    To Space:
       capacity = 3342336 (3.1875MB)
       used     = 0 (0.0MB)
       free     = 3342336 (3.1875MB)
       0.0% used
    concurrent mark-sweep generation:
       capacity = 2113929216 (2016.0MB)
       used     = 546999648 (521.6595153808594MB)
       free     = 1566929568 (1494.3404846191406MB)
       25.875968024844216% used
    Perm Generation:
       capacity = 45715456 (43.59765625MB)
       used     = 27495544 (26.22179412841797MB)
       free     = 18219912 (17.37586212158203MB)
       60.144962788952604% used

    复制代码

    可见堆内存都是正常的,重新回到业务日志里寻找异常,发现出现在堆外内存的分配上: 

    java.lang.OutOfMemoryError
     at sun.misc.Unsafe.allocateMemory(Native Method)
     at java.nio.DirectByteBuffer.(DirectByteBuffer.java:101)
     at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
     at com.schooner.MemCached.SchoonerSockIOPool$TCPSockIO.(Unknown Source)

    对于这个参数分配过小的情况下造成OOM,不妨执行jmap -histo:live看看(也可以用JConsole之类的外部触发GC),因为它会强制一次full GC,如果堆外内存明显下降,很有可能就是堆外内存过大引起的OOM。

    BTW,如果在执行jmap命令时遇到:

    Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

    这个算是JDK的一个bug(链接),只要是依赖于SA(Serviceability Agent)的工具,比如jinfo/jstack/jmap都会存在这个问题,但是Oracle说了“won’t fix”……

    Ubuntu 10.10 and newer has a new default security policy that affects Serviceability commands. 
    This policy prevents a process from attaching to another process owned by the same UID if 
    the target process is not a descendant of the attaching process.

    不过它也是给了解决方案的,需要修改/etc/sysctl.d/10-ptrace.conf:

    kernel.yama.ptrace_scope = 0

    堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。当然,如果你是64位系统,你需要先安装libunwind库

    最后,JDK存在一些direct buffer的bug(比如这个这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。

    三、堆外内存回收

    3.1、ByteBuffer的堆外内存回收

     由前面的文章可知,堆外内存分配很简单,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)即可。很像C语言。在C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中我们需要手动释放获取的堆外内存吗?在谈到堆外内存优点时提到“可以无限使用到1TB”,既然可以无限使用,那么会不会用爆内存呢?这个是很有可能的...所以堆外内存的垃圾回收也很重要。

    由于堆外内存并不直接控制于JVM,因此只能等到full GC的时候才能垃圾回收!(direct buffer归属的的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间。前提是没有关闭DisableExplicitGC

    先看一个示例:(堆外内存回收演示)

    复制代码

    /**
         * @VM args:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
         * -XX:+DisableExplicitGC //增加此参数一会儿就会内存溢出java.lang.OutOfMemoryError: Direct buffer memory
         */
        public static void TestDirectByteBuffer() {
            List<ByteBuffer> list = new ArrayList<ByteBuffer>();
            while(true) {
                ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
                //list.add(buffer);
            }
        }

    复制代码

    通过NIO的ByteBuffer使用堆外内存,将堆外内存设置为40M:

    场景一:不禁用FullGC下的system.gc

    运行这段代码会发现:程序可以一直运行下去,不会报OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,会发现程序频繁的进行垃圾回收活动。

    结果省略。

    场景二:同时JVM完全忽略系统的GC调用

    与之前的JVM启动参数相比,增加了-XX:+DisableExplicitGC,这个参数作用是禁止显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。结果如下:

    显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险

      从DirectByteBuffer的源码也可以分析出来,ByteBuffer.allocateDirect()会调用Bits.reservedMemory()方法,在该方法中显示调用了System.gc()用户内存回收,如果-XX:+DisableExplicitGC打开,则让System.gc()无效,内存无法有效回收,导致OOM。

         我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。

    Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

    ByteBuffer与Unsafe使用堆外内存在回收时的不同:

    Direct ByteBuffer分配出去的直接内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存

    GC是如何回收ByteBuffer分配的“直接内存”的,看下面的源码

      DirectByteBuffer 类有一个内部的静态类 Deallocator,这个类实现了 Runnable 接口并在 run() 方法内释放了内存,源码如下:

     

    那这个 Deallocator 线程是哪里调用了呢?这里就用到了 Java 的虚引用(PhantomReference),Java 虚引用允许对象被回收之前做一些清理工作。在 DirectByteBuffer 的构造方法中创建了一个 Cleaner:

    cleaner = Cleaner.create(this /* 这个是 DirectByteBuffer 对象的引用 */, 
    new Deallocator(address, cap) /* 清理线程 */); 

    DirectByteBuffer中Deallocator线程如何创建

     

    而 Cleaner 类继承了 PhantomReference 类,并且在自己的 clean() 方法中启动了清理线程,当 DirectByteBuffer 被 GC 之前 cleaner 对象会被放入一个引用队列(ReferenceQueue),JVM 会启动一个低优先级线程扫描这个队列,并且执行 Cleaner 的 clean 方法来做清理工作。

    根据上面的源码分析,我们可以想到堆外内存回收的几张方法:

    1. Full GC,一般发生在年老代垃圾回收以及调用System.gc的时候,但这样不一顶能满足我们的需求。
    2. 调用ByteBuffer的cleaner的clean(),内部还是调用System.gc(),所以一定不要-XX:+DisableExplicitGC

    复制代码

    package xing.test;
    
    import java.nio.ByteBuffer;
    import sun.nio.ch.DirectBuffer;
    
    public class NonHeapTest {
        public static void clean(final ByteBuffer byteBuffer) {  
            if (byteBuffer.isDirect()) {  
               ((DirectBuffer)byteBuffer).cleaner().clean();  
            }  
      }  
        
        public static void sleep(long i) {  
            try {  
                  Thread.sleep(i);  
             }catch(Exception e) {  
                  /*skip*/  
             }  
        }  
        public static void main(String []args) throws Exception {  
               ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 200);  
               System.out.println("start");  
               sleep(5000);  
               clean(buffer);//执行垃圾回收
    //         System.gc();//执行Full gc进行垃圾回收
               System.out.println("end");  
               sleep(5000);  
        }  
    }

    复制代码

    这样就能手动的控制回收堆外内存了!其中sun.nio其实是java.nio的内部实现。所以你可能不能通过eclipse的自动排错找到这个包,直接复制

    import sun.nio.ch.DirectBuffer;

    显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险

         我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。

    Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

    Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。

    3.2、正确释放Unsafe分配的堆外内存

            虽然第3种情况的ObjectInHeap存在内存泄露,但是这个类的设计是合理的,它很好的封装了直接内存,这个类的调用者感受不到直接内存的存在。那怎么解决ObjectInHeap中的内存泄露问题呢?可以覆写Object.finalize(),当堆中的对象即将被垃圾回收器释放的时候,会调用该对象的finalize。由于JVM只会帮助我们管理内存资源,不会帮助我们管理数据库连接,文件句柄等资源,所以我们需要在finalize自己释放资源。

    import sun.misc.Unsafe;
    
    public class RevisedObjectInHeap
    {
    	private long address = 0;
    
    	private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();
    
    	// 让对象占用堆内存,触发[Full GC
    	private byte[] bytes = null;
    
    	public RevisedObjectInHeap()
    	{
    		address = unsafe.allocateMemory(2 * 1024 * 1024);
    		bytes = new byte[1024 * 1024];
    	}
    
    	@Override
    	protected void finalize() throws Throwable
    	{
    		super.finalize();
    		System.out.println("finalize." + bytes.length);
    		unsafe.freeMemory(address);
    	}
    
    	public static void main(String[] args)
    	{
    		while (true)
    		{
    			RevisedObjectInHeap heap = new RevisedObjectInHeap();
    			System.out.println("memory address=" + heap.address);
    		}
    	}
    
    }
    

    我们覆盖了finalize方法,手动释放分配的堆外内存。如果堆中的对象被回收,那么相应的也会释放占用的堆外内存。这里有一点需要注意下

    // 让对象占用堆内存,触发[Full GC
    private byte[] bytes = null;

    这行代码主要目的是为了触发堆内存的垃圾回收行为,顺带执行对象的finalize释放堆外内存。如果没有这行代码或者是分配的字节数组比较小,程序运行一段时间后还是会报OutOfMemoryError。这是因为每当创建1个RevisedObjectInHeap对象的时候,占用的堆内存很小(就几十个字节左右),但是却需要占用2M的堆外内存。这样堆内存还很充足(这种情况下不会执行堆内存的垃圾回收),但是堆外内存已经不足,所以就不会报OutOfMemoryError。

    参考资料

    监控使用的directBuffer大小:http://stackoverflow.com/questions/3908520/looking-up-how-much-direct-buffer-memory-is-available-to-java

    《应用DirectBuffer提升系统性能》http://www.tbdata.org/archives/801

    《Java 的 DirectBuffer 是什么东西?》http://www.simaliu.com/archives/274.html

    展开全文
  • Java堆外内存泄露分析

    千次阅读 2019-02-28 16:12:10
    查看堆内存占用正常,jvm垃圾回收也没有异常。而top出来显示java占用内存是几个G,那么可能想到了是堆内存泄漏。 需要安装google-perftools工具进行分析 1.先安装g++ 不然编译会报错:g++: comma...
  • NIO详解(六):Java堆外内存

    千次阅读 2019-03-20 21:52:30
    内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域...
  • JAVA直接内存堆外内存

    千次阅读 2019-03-25 13:57:05
    堆外内存介绍
  • 请教个问题,谁搞过,java堆外内存监控啊?直接调用Unsafe类分配的内存。怎么监控这部分内存占用。
  • Java堆外内存增长问题排查

    千次阅读 2019-09-18 12:23:51
    最近排查一个线上java服务常驻内存异常高的问题,大概现象是:java堆Xmx配置了8G,但运行一段时间后常驻内存RES从5G逐渐增长到13G #补图#,导致机器开始sw...
  • java堆外内存泄漏

    万次阅读 2016-07-14 13:53:01
    java堆外内存泄漏 最近有个系统在做压力测试, 环境配置: 4核CPU 8g内存 jdk1.6.0_25,jvm配置-server -Xms2048m -Xmx2048m 出现问题如下 执行并发300人,压测持续1个小时内存使用率从20%上升到100%,tps从1100多...
  • 一、JVM内存的分配及垃圾回收  对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下:  新生代:一般来说新创建的对象都分配在这里。  年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面...
  • JAVA堆内存、堆外内存

    千次阅读 2018-07-15 21:08:07
    使用方式:使用未公开的Unsafe和NIO下的ByteBuffer堆外内存的回收机制Direct Memory是受GC控制的,例如ByteBufferbb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆占用1k的内存Java堆内只会占用...
  • 堆内存完全由JVM负责分配和释放,如果程序没有缺陷代码导致内存泄露,那么就不会遇到java.lang.OutOfMemoryError这个错误。 使用内存,就是为了能直接分配和释放内存,提高效率。JDK5.0之后,代码中能直接操作...
  • Java分配的非空对象都是由java虚拟机的垃圾收集器管理的,这一部分称为内存,虚拟机会定期对垃圾内存进行回收,在某些特定的时间点,它会进行一次彻底的回收(full gc)。彻底回收时,垃圾收集器...
  • 查看Java进程的堆内存: [root@~]# jmap -heap 21945 Attaching to process ID 21945, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.131-b11 ...
  • gperftools安装与使用分析java堆外内存

    千次阅读 2020-02-23 13:54:38
    yum install gperftools libunwind export LD_PRELOAD=/usr/lib64/libtcmalloc.so export HEAPPROFILE=/DATA1/admin_tmp/gzip /bin/pprof --text /usr/bin/java /DATA1/admin_tmp/gzip.0001.heap
  • JAVA堆外内存设置

    千次阅读 2017-03-02 11:14:18
    fer用于生成一块cap大小的堆外内存   java.nio.Bits   // These methods should be called whenever direct memory is allocated or // freed. They allow the user to control the amount of direct ...
  • Java堆外内存管理

    千次阅读 2017-09-11 16:55:29
    Java堆外内存管理   1、JVM可以使用的内存分外2种:堆内存和堆内存: 堆内存完全由JVM负责分配和释放,如果程序没有缺陷代码导致内存泄露,那么就不会遇到java.lang.OutOfMemoryError这个错误。 使用内存...
  • Netty之Java堆外内存扫盲

    千次阅读 2016-06-01 17:34:32
    Netty之Java堆外内存回收,Cleaner
  • Java堆外内存排查小结

    万次阅读 2018-06-29 11:42:02
    简介JVM堆外内存难排查但经常会出现问题,这可能是目前最全的JVM堆外内存排查思路。通过本文,你应该了解:pmap 命令gdb 命令perf 命令内存 RSS、VSZ的区别java NMT起因这几天遇到一个比较奇怪的问题,觉得有必要和...
  • 在稍微了解Java内存分区的时候,大多数文章都是出自深入理解jvm这本书,上来就是给你分了 程序计数器,Java虚拟机栈,本地方法栈,,方法区,还有个直接内存,还说方法区里面有个常量池。在写这本书的时候,jdk还...
  • 这些工具具体使用起来相对比较直观,直接连接到Java进程,然后就可以在图形化界面里掌握内存使用情况。 以JConsole为例,其内存页面可以显示常见的堆内存和各种堆部分使用状态。 也可以使用命令行工具进行运行时...
  • JAVA使用堆外内存导致swap飙高

    千次阅读 2017-08-18 18:02:32
    JAVA使用堆外内存导致swap飙高
  • java进程占用内存 约等于 Java永久代 + Java堆(新生代和老年代) + 线程栈+ Java NIO,其它部分占用内存较小, 详细可以参考这篇文章 https://my.oschina.net/haitaohu/blog/1830582 第一步:获取异常进程...
  • 记一次堆外内存溢出排查过程

    万次阅读 2018-09-30 12:58:14
    服务器发布/重启后,进程占用内存 21%(3g),观察进程占用内存,以一天4%左右的速度增长,一定时间过后,java 进程内存增长到接近 90%,服务器报警。此时 old 区占用 50%,未达到 CMS GC 阈值,因此不会触发 CMS GC,...
  •  使用Java的一大好处就是自动内存管理,程序员不用太关心内存的是否,JVM的Garbage Collector(GC)帮我们找到不被引用的垃圾对象并清除掉。但是有得必有失,我们也失去了自己管理内存的可能性。【个人观点:大多数...
  • 分析Java堆内存溢出的原因

    千次阅读 2020-10-21 15:39:39
    1、堆溢出   一般来说,绝大部分Java的内存溢出都属于堆溢出。原因是因为大量对象占据了堆空间,这些对象都持有强引用...直接内存的申请速度一般比堆内存慢,但是访问速度快于堆内存。 对于那些可复用的,并且会被经
  • Java堆内存初始大小

    千次阅读 2019-06-28 10:08:19
    Java堆内存初始大小 大厂面试题: 1、JVM垃圾回收时候如何确定垃圾?是否知道什么是GC Roots 2、你说你做过JVM参数调优和参数配置,请问如何盘点查看JVM系统默认值 3、你平时工作中用过的JVM常用基本配置参数有...
  • JVM堆外内存问题排查

    万次阅读 2018-07-15 11:35:07
    JVM 堆内存一般分析的比较多,本篇谈谈堆内存问题排查,通常我们需要排查堆内存的原因是系统整个内存使用飙高,但是堆内内存使用正常。这时候就需要分析堆内存了 堆内存组成 通常JVM的参数我们会配置 -...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 434,340
精华内容 173,736
关键字:

java堆外内存使用

java 订阅