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

    2021-01-21 17:09:50
     堆外内存其实并无特别之处。线程栈,应用程序代码,NIO缓存用的都是堆外内存。事实上在C或者C++中,你只能使用未托管内存,因为它们默认是没有托管堆(managed heap)的。在Java中使用托管内存或者“堆”内存是这...
  • 分析堆外内存.pdf

    2019-05-08 20:57:06
    在Linux下,使用gperftools分析对外内存泄漏。介绍了基本的安装和使用。
  • Java堆外内存泄露场景总结,包含几个常用的可能,如:JNI,NIO,AWT/Swing,Inflater&Deflater;
  • 目前支持内存、堆外内存、磁盘缓存。 功能特点: 支持二级缓存:Memory、Persistence 各个缓存可以拥有有效时间,超过时间缓存会过期 Memory 默认支持 FIFO、LRU、LFU 算法的实现 Memory 额外支持 Guava Cache、...
  • 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

    展开全文
  • 堆外内存和堆内内存

    2018-11-28 21:05:58
    堆外内存和堆内内存是相对的二个概念,其中堆内内存是我们平常工作中接触比较多的,我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式: 堆内内存 = 新生代+...

    堆内内存

    堆外内存和堆内内存是相对的二个概念,其中堆内内存是我们平常工作中接触比较多的,我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式:

    堆内内存 = 新生代+老年代+持久代

    在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

     

    堆外外存

    和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

    代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。

    作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。

    DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作

     

    堆外内存释放

    JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,用于保存堆外内存的元信息(开始地址、大小和容量等),当DirectByteBuffer被GC回收后,Cleaner对象被放入ReferenceQueue中,然后由ReferenceHandler守护线程调用unsafe.freeMemory(address),回收堆外内存。 在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。

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

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

    主动回收(推荐): 对于Sun的JDK,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行;
    基于 GC 回收:堆内的DirectByteBuffer对象被GC时,会调用cleaner回收其引用的堆外内存

     

    为什么Cleaner对象能够被放入ReferenceQueue中?

    Cleaner对象关联了一个PhantomReference引用,如果GC过程中某个对象除了只有PhantomReference引用它之外,并没有其他地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。
     

     

    堆外内存注意

    java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收。

    当我们基于GC回收时,YGC只会将将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收,如果有大量的DirectByteBuffer对象移到了old区,但是又一直没有做CMS GC或者FGC,而只进行YGC,物理内存会被慢慢耗光,触发OutOfMemoryError。

    因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

     

    System.gc的作用有哪些

    1.做一次full gc

    2.执行后会暂停整个进程。

    3.System.gc我们可以禁掉,使用-XX:+DisableExplicitGC。

    4.其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。

    5.最常见的场景是RMI/NIO下的堆外内存分配等

    注:
    如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误,在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。

    说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了。
     

     

    堆外内存优缺点

    优点

    提升了IO效率(避免了数据从用户态向内核态的拷贝).

    对垃圾回收停顿的改善因为full gc意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

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

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

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

    缺点

    分配和回收堆外内存比分配和回收堆内存耗时;(解决方案:通过对象池避免频繁地创建和销毁堆外内存)

    堆外内存的泄漏问题

    堆外内存的数据结构问题:堆外内存最大的问题就是你的数据结构变得不那么直观,如果数据结构比较复杂,就要对它进行串行化(serialization),而串行化本身也会影响性能。另一个问题是由于你可以使用更大的内存,你可能开始担心虚拟内存(即硬盘)的速度对你的影响了。

     

    为什么堆外内存能够提升IO效率?

    堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了数据从用户内向内核态的拷贝。

     

    堆外内存 VS 内存池

    内存池:主要用于两类对象:①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;②加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。

    堆外内存:它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。

     

     

    展开全文
  • 堆外内存泄漏排查

    万次阅读 2020-08-21 14:42:39
    堆外内存泄漏排查 直接内存:指的是Java应用程序通过直接方式从操作系统中申请的内存,也叫堆外内存,因为这些对象分配在Java虚拟机的堆(严格来说,应该是JVM的内存外,但是堆是这块内存中最大的)以外。 直接内存...

    堆外内存泄漏排查

    直接内存:指的是Java应用程序通过直接方式从操作系统中申请的内存,也叫堆外内存,因为这些对象分配在Java虚拟机的堆(严格来说,应该是JVM的内存外,但是堆是这块内存中最大的)以外。

    直接内存有哪些?

    • 元空间。
    • BIO中ByteBuffer分配的直接内存。
    • 使用Java的Unsafe类做一些分配本地内存的操作。
    • JNI或者JNA程序,直接操纵了本地内存,比如一些加密库、压缩解压等。

    JNI(Java Native Interface):通过使用Java本地接口(C或者C++)书写程序,可以确保代码在不同的平台上方便移植。

    JNA(Java Native Access):提供一组Java工具类用于在运行期间动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。

    JNA是建立在JNI技术基础之上的一个Java类库,它使您可以方便地使用java直接访问动态链接库中的函数。原来使用JNI,你必须手工用C写一个动态链接库,在C语言中映射Java的数据类型。而在JNA中,它提供了一个动态的C语言编写的转发器,可以自动实现Java和C的数据类型映射,你不再需要编写C动态链接库。也许这也意味着,使用JNA技术比使用JNI技术调用动态链接库会有些微的性能损失。但总体影响不大,因为JNA也避免了JNI的一些平台配置的开销。

    直接内存的优缺点

    直接内存,其实就是不受JVM控制的内存。相比于堆内存有几个优势:

    • 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作,能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
    • 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。
    • 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现。
    • 可以扩展至更大的内存空间,比如超过1TB甚至比主存还大的空间。

    直接内存有很多好处,我们还是应该要了解它的缺点:

    • 堆外内存难以控制,如果内存泄漏,那么很难排查。
    • 堆外内存相对来说,不适合存储很复杂的对象,一般简单的对象比较适合。

    直接内存的泄漏

    ByteBuffer

    public class DirectMemoryOOM {
    	
    	public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
    		ByteBuffer.allocateDirect(128 * 1024 * 1024);
    	}
    }
    

    运行时带上JVM参数-XX:MaxDirectMemorySize=100m会抛出如下异常:

    Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
    	at java.nio.Bits.reserveMemory(Bits.java:694)
    	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
    	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
    	at com.morris.jvm.heapout.DirectMemoryOOM.main(DirectMemoryOOM.java:14)
    

    如果我们没有通过-XX:MaxDirectMemorySize来指定最大的直接内存,那么默认的最大堆外内存是多少呢?一般来说,如果没有显示的设置-XX:MaxDirectMemorySize参数,通过ByteBuffer能够分配的直接内存空间大小就是堆的最大大小,也就是对应对应参数-Xmx,真的是这么样吗?

    1. VM参数配置:-XX:MaxDirectMemorySize=128m,程序会正常退出。
    2. VM参数配置:-Xmx128m,运行结果为OutOfMemoryError,这就说明了ByteBuffer能够分配的直接内存空间大小并不是-Xmx指定的大小。
    3. VM参数配置:-Xmx135m -Xmn100m -XX:SurvivorRatio=8,运行结果还是OutOfMemoryError。
    4. VM参数配置:-Xmx138m -Xmn100m -XX:SurvivorRatio=8,程序会正常退出。

    总结:没有显示的设置-XX:MaxDirectMemorySize参数,通过ByteBuffer能够分配的直接内存空间大小就是堆的最大可使用的大小。堆的最大可使用的大小=堆的最大值(-Xmx)- 一个Survivor的大小(浪费的空间),所以案例3会OOM,堆的最大的可使用的大小=135m-10m=125m,不能分配128M的对象,而在案例4中,堆的最大可使用的大小=138m-10m=128m ,刚好可以分配128M的对象,所有不会OOM。

    UnSafe

    public class UnSafeDemo {
        public static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) throws Exception {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            unsafe.allocateMemory(100 * 1024 * 1024);
        }
    }
    

    运行时加上-XX:MaxDirectMemorySize=10m参数,发现程序并没有OOM,也就是说明使用UnSafe API分配的内存不受-XX:MaxDirectMemorySize参数的控制。

    JNI

    package com.morris.jvm.heapout;
    
    import com.sun.management.OperatingSystemMXBean;
    import com.sun.net.httpserver.HttpContext;
    import com.sun.net.httpserver.HttpServer;
    
    import java.io.*;
    import java.lang.management.ManagementFactory;
    import java.net.InetSocketAddress;
    import java.util.Random;
    import java.util.concurrent.ThreadLocalRandom;
    import java.util.zip.GZIPInputStream;
    import java.util.zip.GZIPOutputStream;
    
    public class LeakDemo {
    
        /**
         * 构造随机的字符串
         */
        public static String randomString(int strLength) {
            Random rnd = ThreadLocalRandom.current();
            StringBuilder ret = new StringBuilder();
            for (int i = 0; i < strLength; i++) {
                boolean isChar = (rnd.nextInt(2) % 2 == 0);
                if (isChar) {
                    int choice = rnd.nextInt(2) % 2 == 0 ? 65 : 97;
                    ret.append((char) (choice + rnd.nextInt(26)));
                } else {
                    ret.append(rnd.nextInt(10));
                }
            }
            return ret.toString();
        }
    
        //复制方法
        public static int copy(InputStream input, OutputStream output) throws IOException {
            long count = copyLarge(input, output);
            return count > 2147483647L ? -1 : (int) count;
        }
    
        //复制方法
        public static long copyLarge(InputStream input, OutputStream output) throws IOException {
            byte[] buffer = new byte[4096];
            long count = 0L;
    
            int n;
            for (; -1 != (n = input.read(buffer)); count += (long) n) {
                output.write(buffer, 0, n);
            }
    
            return count;
        }
    
        //解压
        public static String decompress(byte[] input) throws Exception {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
            return new String(out.toByteArray());
        }
    
        //压缩
        public static byte[] compress(String str) throws Exception {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(bos);
            try {
                gzip.write(str.getBytes());
                gzip.finish();
                byte[] b = bos.toByteArray();
                return b;
            } finally {
                try {
                    gzip.close();
                } catch (Exception ex) {
                }
                try {
                    bos.close();
                } catch (Exception ex) {
                }
            }
        }
    
        private static OperatingSystemMXBean osmxb = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
    
        //通过MXbean来判断获取内存使用率(系统)
        public static int memoryLoad() {
            double totalvirtualMemory = osmxb.getTotalPhysicalMemorySize();
            double freePhysicalMemorySize = osmxb.getFreePhysicalMemorySize();
    
            double value = freePhysicalMemorySize / totalvirtualMemory;
            int percentMemoryLoad = (int) ((1 - value) * 100);
            return percentMemoryLoad;
        }
    
    
        private static volatile int RADIO = 60;
    
        public static void main(String[] args) throws Exception {
    
            //构造1kb的随机字符串
            int BLOCK_SIZE = 1024;
            String str = randomString(BLOCK_SIZE / Byte.SIZE);
            //字符串进行压缩
            byte[] bytes = compress(str);
            for (; ; ) {
                int percent = memoryLoad();
                if (percent > RADIO) { // 如果操作系统内存使用率达到阈值,则等待1s
                    System.out.println("memory used >" + RADIO + "  hold 1s");
                    Thread.sleep(1000);
                } else {
                    //不断对字符串进行解压
                    decompress(bytes);
                    Thread.sleep(1);
                }
            }
        }
    }
    

    演示程序的简单说明:程序将会申请1kb的随机字符串,然后持续解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到60%的时候,我们将挂起程序,这样方便用工具来分析。

    启动参数:java -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10M LeakDemo

    参数说明:

    • -XX:+PrintGC:打印GC日志。
    • -Xmx1G:限制堆的最大值为1G。
    • -Xmn1G:限制堆的最小值为1G。
    • -XX:MaxMetaspaceSize=10M:限制元空间的最大值为10M。
    • -XX:MaxDirectMemorySize=10M:限制直接内存的最大值为10M。
    • -XX:+AlwaysPreTouch:如果不设置这个参数,JVM的内存只有真正在使用的时候,才会分配给它。如果设置这个参数,在JVM启动的时候,就把它所有的内存在操作系统分配了。在堆比较大的时候,会加大启动时间,但在这个场景中,我们为了减少内存动态分配的影响,把这个参数添加上。

    这个程序很快就打印了以下的显示,这个证明操作系统内存使用率,达到了60%。

    Java HotSpot(TM) 64-Bit Server VM warning: MaxNewSize (1048576k) is equal to or greater than the entire heap (1048576k).  A new max generation size of 1048064k will be used.
    memory used >80  hold 1s
    memory used >80  hold 1s
    memory used >80  hold 1s
    ... ...
    

    下面通过一些命令和工具分析内存泄漏的原因。

    top

    先使用top命令查看哪个进程的内存占用过高:

    # top
    top - 11:14:34 up 277 days, 50 min,  2 users,  load average: 0.00, 0.00, 0.00
    Tasks: 143 total,   1 running, 141 sleeping,   1 stopped,   0 zombie
    Cpu(s):  0.3%us,  0.3%sy,  0.0%ni, 99.3%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
    Mem:   3921488k total,  2458504k used,  1462984k free,   327152k buffers
    Swap:  4063228k total,    58672k used,  4004556k free,   434392k cached
    
      PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                                                                                         
    17238 root      20   0 4287m 1.3g  11m S  7.9 34.9   0:02.69 java                                                                                                                                                                                                             
        1 root      20   0 19360 1032  792 S  0.0  0.0   0:05.59 init   
    

    发现一个进程号为17238的java进程占用内存较高。

    top命令中几个指标说明:

    • VIRT(virtual memory usage):虚拟内存,进程申请的虚拟内存大小,包括进程使用的库、代码、数据等,假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量。
    • RES(resident memory usage):常驻内存(物理内存),如果申请100m的内存,实际使用10m,它只增长10m。
    • %MEM:进程使用的物理内存占操作系统物理内存的百分比。

    如果进程较多,可以使用top -p 17238命令,只观察这一个进程。

    # top -p 17238
    top - 11:19:04 up 277 days, 54 min,  2 users,  load average: 0.00, 0.00, 0.00
    Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
    Cpu(s):  0.2%us,  0.0%sy,  0.0%ni, 99.8%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
    Mem:   3921488k total,  3176908k used,   744580k free,   327152k buffers
    Swap:  4063228k total,    58672k used,  4004556k free,   434392k cached
    
      PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                                                                                         
    17238 root      20   0 6975m 2.0g  11m S  0.3 53.1   0:08.24 java       
    

    当控制台打印memory used >80 hold 1s时,此时进程17238占用的物理内存达到最大值2G。

    jmap

    既然java进程占用的内存较大,在jvm中占用内存最大的区域为堆,存在内存泄漏可能性最大的也是堆,所以使用jvm提供的jmap指令来查看堆的情况。

    # jmap -heap 17238
    Attaching to process ID 17238, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.241-b07
    
    using thread-local object allocation.
    Parallel GC with 2 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 1073741824 (1024.0MB)
       NewSize                  = 1073217536 (1023.5MB)
       MaxNewSize               = 1073217536 (1023.5MB)
       OldSize                  = 524288 (0.5MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 10485760 (10.0MB)
       CompressedClassSpaceSize = 2097152 (2.0MB)
       MaxMetaspaceSize         = 10485760 (10.0MB)
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 805830656 (768.5MB)
       used     = 274035832 (261.34093475341797MB)
       free     = 531794824 (507.15906524658203MB)
       34.00662781436799% used
    From Space:
       capacity = 133693440 (127.5MB)
       used     = 0 (0.0MB)
       free     = 133693440 (127.5MB)
       0.0% used
    To Space:
       capacity = 133693440 (127.5MB)
       used     = 0 (0.0MB)
       free     = 133693440 (127.5MB)
       0.0% used
    PS Old Generation
       capacity = 524288 (0.5MB)
       used     = 0 (0.0MB)
       free     = 524288 (0.5MB)
       0.0% used
    
    820 interned Strings occupying 55872 bytes.
    

    从堆的日志中可以发现,JVM的堆和元空间总共使用内存1.1G左右,说明不是堆内存泄漏。

    jstack

    既然不是堆内存泄露,那么是不是栈的内存泄漏了呢?

    # jstack 17238
    2020-08-21 11:46:21
    Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):
    
    "Attach Listener" #8 daemon prio=9 os_prio=0 tid=0x00007f0544001000 nid=0x449d waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f057c0ba000 nid=0x444f runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f057c0b7000 nid=0x444e waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f057c0b4800 nid=0x444d waiting on condition [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f057c0af800 nid=0x444c runnable [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f057c081800 nid=0x444b in Object.wait() [0x00007f055fefd000]
       java.lang.Thread.State: WAITING (on object monitor)
    	at java.lang.Object.wait(Native Method)
    	- waiting on <0x00000000c0088ee0> (a java.lang.ref.ReferenceQueue$Lock)
    	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
    	- locked <0x00000000c0088ee0> (a java.lang.ref.ReferenceQueue$Lock)
    	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
    	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
    
    "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f057c07d000 nid=0x444a in Object.wait() [0x00007f055fffe000]
       java.lang.Thread.State: WAITING (on object monitor)
    	at java.lang.Object.wait(Native Method)
    	- waiting on <0x00000000c0086c00> (a java.lang.ref.Reference$Lock)
    	at java.lang.Object.wait(Object.java:502)
    	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
    	- locked <0x00000000c0086c00> (a java.lang.ref.Reference$Lock)
    	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
    
    "main" #1 prio=5 os_prio=0 tid=0x00007f057c009000 nid=0x4446 waiting on condition [0x00007f0583977000]
       java.lang.Thread.State: TIMED_WAITING (sleeping)
    	at java.lang.Thread.sleep(Native Method)
    	at LeakDemo.main(LeakDemo.java:130)
    
    "VM Thread" os_prio=0 tid=0x00007f057c073800 nid=0x4449 runnable 
    
    "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f057c01e800 nid=0x4447 runnable 
    
    "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f057c020800 nid=0x4448 runnable 
    
    "VM Periodic Task Thread" os_prio=0 tid=0x00007f057c0bd000 nid=0x4450 waiting on condition 
    
    JNI global references: 5
    

    从栈的日志中可以发现,这个进程总共开启了10多个线程,在jvm中每个线程分配的栈的大小(-Xss)默认为1M,说明不是栈内存泄漏。

    到这里基本上可以确定应该是直接内存发生了泄漏。

    NMT

    NMT(NativeMemoryTracking)是JVM提供的用来追踪Native内存的使用情况。通过在启动参数上加入-XX:NativeMemoryTracking=detail就可以启用,然后使用jcmd命令,就可查看内存分配。

    先把程序关闭,加上-XX:NativeMemoryTracking=detail参数重新启动:

    # jcmd 17845 VM.native_memory summary
    17845:
    
    Native Memory Tracking:
    
    Total: reserved=2416216KB, committed=1117248KB
    -                 Java Heap (reserved=1048576KB, committed=1048576KB)
                                (mmap: reserved=1048576KB, committed=1048576KB) 
     
    -                     Class (reserved=1059970KB, committed=8066KB)
                                (classes #414)
                                (malloc=3202KB #280) 
                                (mmap: reserved=1056768KB, committed=4864KB) 
     
    -                    Thread (reserved=12388KB, committed=12388KB)
                                (thread #13)
                                (stack: reserved=12336KB, committed=12336KB)
                                (malloc=38KB #64) 
                                (arena=14KB #22)
     
    -                      Code (reserved=249713KB, committed=2649KB)
                                (malloc=113KB #447) 
                                (mmap: reserved=249600KB, committed=2536KB) 
     
    -                        GC (reserved=40417KB, committed=40417KB)
                                (malloc=3465KB #112) 
                                (mmap: reserved=36952KB, committed=36952KB) 
     
    -                  Compiler (reserved=137KB, committed=137KB)
                                (malloc=6KB #31) 
                                (arena=131KB #5)
     
    -                  Internal (reserved=3290KB, committed=3290KB)
                                (malloc=3258KB #1328) 
                                (mmap: reserved=32KB, committed=32KB) 
     
    -                    Symbol (reserved=1373KB, committed=1373KB)
                                (malloc=917KB #92) 
                                (arena=456KB #1)
     
    -    Native Memory Tracking (reserved=177KB, committed=177KB)
                                (malloc=114KB #1618) 
                                (tracking overhead=63KB)
     
    -               Arena Chunk (reserved=174KB, committed=174KB)
                                (malloc=174KB) 
    
    

    可惜的是,这个名字让人振奋的工具并不能如它描述的一样,看到我们这种泄漏的场景。上面日志中这点小小的空间,是不能和2GB的内存占用相比的。

    perf

    下面介绍一个神器perf,它除了能够进行一些性能分析,它还能帮助我们找到相应的native调用,这么突出的堆外内存使用问题,肯定能找到相应的调用函数。

    安装:yum install -y perf

    当java进程启动时,我们使用命令perf record -g -p 2747开启监控栈函数调用,当程序内存使用的阈值增加到80%,使用Ctrl+C结束perf命令,这时会在当前目录下生成一个文件perf.data。

    然后执行perf report -i perf.data查看报告:

    Samples: 64K of event 'cpu-clock', Event count (approx.): 16226750000                                                                                                                                                                                                         
      Children      Self  Command  Shared Object       Symbol                                                                                                                                                                                                                     
    +   69.64%     0.05%  java     [kernel.kallsyms]   [k] system_call_fastpath
    +   42.45%     0.08%  java     libc-2.17.so        [.] __GI___libc_read
    +   42.22%     0.03%  java     [kernel.kallsyms]   [k] sys_read
    +   42.14%     0.13%  java     [kernel.kallsyms]   [k] vfs_read
    +   41.66%     0.08%  java     [kernel.kallsyms]   [k] proc_reg_read
    +   36.66%     0.20%  java     [kernel.kallsyms]   [k] seq_read
    +   36.22%     0.33%  java     [kernel.kallsyms]   [k] meminfo_proc_show
    +   28.52%    28.51%  java     [kernel.kallsyms]   [k] get_vmalloc_info
    +   13.59%     0.00%  java     [unknown]           [k] 0x702f006f666e696d
    +   13.35%     0.07%  java     libc-2.17.so        [.] __fopen_internal
    +   13.04%     0.07%  java     libc-2.17.so        [.] __GI___libc_open
    +   12.88%     0.04%  java     [kernel.kallsyms]   [k] sys_open
    +   12.78%     0.17%  java     [kernel.kallsyms]   [k] do_sys_open
    +   11.80%     0.04%  java     [kernel.kallsyms]   [k] do_filp_open
    +   11.70%     0.09%  java     [kernel.kallsyms]   [k] path_openat
    +    8.40%     0.48%  java     [kernel.kallsyms]   [k] do_last
    +    7.95%     0.00%  java     [kernel.kallsyms]   [k] page_fault
    +    7.95%     0.01%  java     [kernel.kallsyms]   [k] do_page_fault
    +    7.81%     1.41%  java     [kernel.kallsyms]   [k] __do_page_fault
    +    6.60%     0.32%  java     libjvm.so           [.] JavaCalls::call_helper
    +    6.33%     0.12%  java     [kernel.kallsyms]   [k] seq_printf
    +    6.30%     0.00%  java     libpthread-2.17.so  [.] start_thread
    +    6.21%     0.05%  java     [kernel.kallsyms]   [k] seq_vprintf
    +    6.08%     0.00%  java     perf-2747.map       [.] 0x00007fb799150574
    +    6.02%     0.00%  java     perf-2747.map       [.] 0x00007fb799121c46
    +    5.91%     0.35%  java     [kernel.kallsyms]   [k] handle_mm_fault
    +    5.78%     1.05%  java     [kernel.kallsyms]   [k] vsnprintf
    +    5.74%     0.07%  java     libc-2.17.so        [.] __GI___munmap
    +    5.71%     0.08%  java     libzip.so           [.] Java_java_util_zip_Inflater_inflateBytes
    +    5.68%     0.00%  java     libjli.so           [.] JavaMain
    +    5.68%     0.00%  java     libjvm.so           [.] jni_CallStaticVoidMethod
    +    5.68%     0.00%  java     libjvm.so           [.] jni_invoke_static
    +    5.58%     0.03%  java     [kernel.kallsyms]   [k] sys_munmap
    +    5.39%     0.03%  java     [kernel.kallsyms]   [k] vm_munmap
    +    5.21%     0.25%  java     [kernel.kallsyms]   [k] do_munmap
    +    5.14%     0.07%  java     [kernel.kallsyms]   [k] proc_root_lookup
    +    5.12%     0.01%  java     [kernel.kallsyms]   [k] lookup_real
    +    5.09%     0.03%  java     [kernel.kallsyms]   [k] proc_lookup
    +    4.99%     0.46%  java     [kernel.kallsyms]   [k] proc_lookup_de
    +    4.95%     0.24%  java     libc-2.17.so        [.] __GI___libc_close
    +    4.74%     0.00%  java     perf-2747.map       [.] 0x00007fb799146471
    +    4.67%     1.35%  java     libzip.so           [.] inflate
    +    4.47%     0.15%  java     libjvm.so           [.] JVM_Sleep
    +    4.45%     0.31%  java     [kernel.kallsyms]   [k] __alloc_pages_nodemask
    +    4.19%     0.00%  java     perf-2747.map       [.] 0x00007fb7990007a7
    +    4.15%     0.09%  java     [kernel.kallsyms]   [k] alloc_pages_vma
    +    4.06%     0.06%  java     [kernel.kallsyms]   [k] do_notify_resume
    +    4.06%     0.00%  java     [kernel.kallsyms]   [k] int_signal
    +    3.99%     0.09%  java     [kernel.kallsyms]   [k] task_work_run
    +    3.94%     0.26%  java     libjvm.so           [.] os::sleep
    +    3.89%     0.02%  java     [kernel.kallsyms]   [k] ____fput
    

    一些JNI程序或者JDK内的模块,都会调用相应的本地函数,在Linux上,这些函数库的后缀都是so,windows下函数库的后缀是dll。

    我们依次浏览所有可疑的资源,发现了“libzip.so”,还发现了不少相关的调用。搜索zip(输入 / 进入搜索模式),结果如下:

    Samples: 64K of event 'cpu-clock', Event count (approx.): 16226750000                                                                                                                                                                                                         
      Children      Self  Comm  Shared Ob Symbol                                                                                                                                                                                                                                 
    +    5.71%     0.08%  java  libzip.so  [.] Java_java_util_zip_Inflater_inflateBytes                                                                                                                                                                                          
         0.23%     0.04%  java  libzip.so  [.] Java_java_util_zip_Inflater_init                                                                                                                                                                                                  
    +   42.22%     0.03%  java  [kernel.kallsyms]   [k] sys_read    
    

    我们发现这些本地调用都是由java.util.zip.Inflater#inflateBytes()方法产生的,然后在代码中追踪哪些地方调用了这个方法或者类,一步一步分析。

    内存泄漏的原因

    GZIPInputStream使用Inflater申请堆外内存、我们没有调用close()方法来主动释放。如果忘记关闭,Inflater对象的生命会延续到下一次GC,在此过程中,堆外内存会一直增长,而GC迟迟没有发生。

        public static String decompress(byte[] input) throws Exception {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
            return new String(out.toByteArray());
        }
    

    只需要将上面的代码改成如下即可:

        public static String decompress(byte[] input) throws Exception {
    
            try (
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(input))
            ) {
                copy(gzip, out);
                return new String(out.toByteArray());
            }
        }
    

    总结

    对堆外内存划分为3块:

    • 元空间:主要是方法区和常量池的存储之地,可以使用“MaxMetaspaceSize”参数来限制它的大小。
    • 直接内存:主要是通过DirectByteBuffer申请的内存,可以使用“MaxDirectMemorySize”参数来限制它的大小。
    • 其他堆外内存:主要是指使用了Unsafe或者其他JNI手段直接申请的内存。这种情况,就没有任何参数能够阻挡它们,要么靠它自己去释放一些内存,要么等待操作系统对它的审判了。
    展开全文
  • JVM——堆外内存详解

    千次阅读 多人点赞 2018-12-04 23:22:29
    内存是好东西,我们常听堆内存,很多人却不知道还有一个堆外内存。 那这两个都是个啥玩意呢?且让本帅博主今天给你好好说道说道。 一、堆内内存 那什么东西是堆内存呢?我们来看看官方的说法。 “Java 虚拟机...

    内存是好东西,我们常听堆内存,很多人却不知道还有一个堆外内存。

    那这两个都是个啥玩意呢?且让本帅博主今天给你好好说道说道。

    一、堆内内存

    那什么东西是堆内存呢?我们来看看官方的说法。

    “Java 虚拟机具有一个堆(Heap),堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”

    也就是说,平常我们老遇见的那位,JVM启动时分配的,就叫作堆内存(即堆内内存)。

    对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

    此外,堆的内存不需要是连续空间,因此堆的大小没有具体要求,既可以固定,也可以扩大和缩小。

    我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式:

    堆内内存 = 新生代+老年代+持久代

     如下图:

    在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描。

    注意:在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

    二、堆外内存

    显然,看名字就知道堆外内存与堆内内存是相对应的:Java 虚拟机管理堆之外的内存,称为非堆内存,即堆外内存。

    换句话说:堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

    那堆外内存都有哪些东西呢?

    Java 虚拟机具有一个由所有线程共享的方法区。方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。

    方法区在逻辑上属于堆,但 Java 虚拟机实现可以选择不对其进行回收或压缩。与堆类似,方法区的内存不需要是连续空间,因此方法区的大小可以固定,也可以扩大和缩小。。

    除了方法区外,Java 虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT 编译器需要内存来存储从 Java 虚拟机代码转换而来的本机代码,从而获得高性能。

    下面我们来看看堆外内存如何申请与释放。

    三、堆外内存的申请和释放

    JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请。

    底层通过unsafe.allocateMemory(size)实现,Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。

    现在我们先看看在JVM层面是如何实现堆外内存申请的。

     可以发现,unsafe.allocateMemory(size)的最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。

    看完堆外内存申请的底层实现,想必大家对它的实现就有了一些基础了解。

    接下来我们再看看DirectByteBuffer()的构造方法。

    其构造方法如下:

    DirectByteBuffer(int cap) {                 
            super(-1, 0, cap, cap);
            //内存是否按页分配对齐
            boolean pa = VM.isDirectMemoryPageAligned();
            //获取每页内存大小
            int ps = Bits.pageSize();
            //分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量
            long size = Math.max(1L, (long)cap + (pa ? ps : 0));
            //用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
            Bits.reserveMemory(size, cap);
            long base = 0;
            try {
               //在堆外内存的基地址,指定内存大小
                base = unsafe.allocateMemory(size);
            } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
            }
            unsafe.setMemory(base, size, (byte) 0);
            //计算堆外内存的基地址
            if (pa && (base % ps != 0)) {
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));
            } else {
                address = base;
            }
            cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
            att = null;
        }
    

    从上面的代码我们可以知道,在Cleaner 内部中通过一个列表,维护了针对每一个 directBuffer 的一个回收堆外内存的线程对象(Runnable),而回收操作就是发生在 Cleaner 的 clean() 方法中。

    Cleaner源码如下:

    private Cleaner(Object var1, Runnable var2) {
            super(var1, dummyQueue);
            this.thunk = var2;
        }
        public static Cleaner create(Object var0, Runnable var1) {
            return var1 == null ? null : add(new Cleaner(var0, var1));
        }
        public void clean() {
            if (remove(this)) {
                try {
                    this.thunk.run(); //此处会调用Deallocator,见下个类
                } catch (final Throwable var2) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        public Void run() {
                            if (System.err != null) {
                                (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                            }
                            System.exit(1);
                            return null;
                        }
                    });
                }
            }
        }

    Deallocator的源码如下:

    private static class Deallocator implements Runnable  {
        private static Unsafe unsafe = Unsafe.getUnsafe();
        private long address;
        private long size;
        private int capacity;
        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
        public void run() {
            if (address == 0) {
                return;
            }
            unsafe.freeMemory(address);//unsafe提供的方法释放内存
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }

    四、堆外内存的回收机制

    上文说到,“unsafe.allocateMemory(size)的最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。”。那岂不是每一次申请堆外内存的时候,都需要在代码中显式释放吗?

    æç¬ä¸æ¯è¿æ ·ç

    很明显,并不是这样的,这种情况的出现对于Java这门语言来说显然不够合理。那既然JVM不会管理这些堆外内存,它们又是怎么回收的呢? 

     这里就要祭出大杀器了:DirectByteBuffer。

    JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

    当初始化一块堆外内存时,对象的引用关系如下:

    其中firstCleaner的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

    如果该DirectByteBuffer对象在一次GC中被回收了,即

    此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

    Cleaner对象的clean方法主要有两个作用:

    1. 把自身从Cleaner链表删除,从而在下次GC时能够被回收
    2. 释放堆外内存

    源码如下:

    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
    

    看到这里,可能有人会想,如果JVM一直没有执行FGC的话,无效的Cleaner对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,无效内存就会很大,那怎么办? 

    这个倒不用担心,那些大神们当然早就考虑到这一种情况了。

    其实,在初始化DirectByteBuffer对象时,会自动去判断,如果堆外内存的环境很友好,那么就申请堆外内存;如果当前堆外内存的条件很苛刻时(即有很多无效内存没有得到释放),这时候就会主动调用System.gc()强制执行FGC,从而释放那些无效内存。

    为了避免这种悲剧的发生,也可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

    源码如下:

    当然,源程序毕竟不是万能的,做项目的时候经常有千奇百怪的情况出现。

    比如很多线上环境的JVM参数有-XX:+DisableExplicitGC,导致了System.gc()等于一个空函数,根本不会触发FGC,因此在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。这一点在使用Netty框架时需要格外注意。

    总而言之,不论是什么东西,都不是绝对安全的。对于各类代码,我们都得多加留心。

    五、System.gc的作用有哪些
    使用了System.gc的作用是什么?

    • 做一次full gc
    • 执行后会暂停整个进程。
    • System.gc我们可以禁掉,使用-XX:+DisableExplicitGC,
      其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。
    • 最常见的场景是RMI/NIO下的堆外内存分配等

    注:
    如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误(这种情况在中有提及),在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。

    说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了。

     

     六、使用堆外内存的优点

    当然,任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。 所以,还是那句话,使用的时候要多留心呀~

    1. 可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;
    2. 减少了垃圾回收(因为垃圾回收会暂停其他的工作。;
    3. 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现(堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。);
    4. 它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据
    5. 站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的

    好啦,以上就是关于堆外内存的相关知识总结啦,如果大家有什么不明白的地方或者发现文中有描述不好的地方,欢迎大家留言评论,我们一起学习呀。

     

    Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

    参考文章:

    https://www.jianshu.com/p/50be08b54bee

    https://www.jianshu.com/p/35cf0f348275

    https://www.cnblogs.com/moonandstar08/p/5107648.html

    展开全文
  • HBase堆外内存测试

    2018-12-09 15:28:05
    介绍HBase堆外内存测试
  • 堆外内存与堆内内存详解

    万次阅读 多人点赞 2018-05-07 17:14:42
    堆外内存一直是Java业务开发人员难以企及的隐藏领域,究竟他是干什么的,以及如何更好的使用呢?那就请跟着我进入这个世界吧。一、什么是堆外内存1、堆内内存(on-heap memory)回顾堆外内存和堆内内存是相对的二个...
  • Spark内存管理之堆内/堆外内存原理详解

    千次阅读 多人点赞 2019-09-27 10:47:06
    堆外内存(Off-heap Memory)3. Execution 内存和 Storage 内存动态调整3.1 动态调整策略4. Task内存申请流程5. 内存分配示例5.1 只用了堆内内存5.2 堆内内存+堆外内存总结 概要 介绍Spark内存管理中,涉及到的**堆...
  • Java netty获取堆外内存占用

    千次阅读 2020-10-20 09:57:05
    在使用了netty的系统中,有时会出现内存泄露的问题,我们就需要去监控这个堆外内存的占用,以排查是否是堆外泄露,下面的代码就是查看堆外内存的。可以写个定时任务,持续调用doReport方法来获取堆外内存。 netty...
  • Java 堆外内存管理

    2019-05-13 11:32:48
    1.什么是堆外内存? 2.堆外内存有什么优势? 3.堆外内存的使用 3.1堆外内存的设置 3.2堆外内存的创建 3.3堆外内存的回收 3.3.1自动回收 3.3.2手动回收 1.什么是堆外内存堆外内存和堆内内存是两个相对的...
  • 一次堆外内存泄露的排查过程

    千次阅读 2019-03-07 09:35:41
    转载自一次堆外内存泄露的排查过程 https://blog.csdn.net/moakun/article/details/85377879 最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级能力...
  • 堆外内存

    千次阅读 2017-09-06 20:06:51
    堆外内存定义  创建Java.nio.DirectByteBuffer时分配的内存。 堆外内存优缺点  优点: 提升了IO效率(避免了数据从用户态向内核态的拷贝);减少了GC次数(节约了大量的堆内内存)。  缺点:分配和回收...
  • 1.前言 在执行Spark的应用程序时,Spark集群会启动Driver和Executor两种JVM进程,前者为...由于Driver的内存管理相对来说较为简单,本文主要对Executor的内存管理进行分析,下文中的Spark内存均特指Executor的内存
  • Java堆外内存泄露分析

    千次阅读 2019-02-28 16:12:10
    说明:...而top出来显示java占用内存是几个G,那么可能想到了是堆外内存泄漏。 需要安装google-perftools工具进行分析 1.先安装g++ 不然编译会报错:g++: comma...
  • jvm堆外内存--DirectByteBuffer

    千次阅读 2018-06-17 13:07:19
    jvm的堆外内存本来是高贵而神秘的东西,只在一些缓存方案实战级别才会出现。但自从用了Netty等高并发IO框架后,就变成了天天与堆外内存打交道,在mina,netty等nio框架中屡见不鲜。堆外内存的优点是能减少IO过程的...
  • 记一次堆外内存溢出排查过程

    万次阅读 2018-09-30 12:58:14
    服务器发布/重启后,进程占用内存 21%(3g),观察进程占用内存,以一天4%左右的速度增长,一定时间过后,java 进程内存增长到接近 90%,服务器报警。此时 old 区占用 50%,未达到 CMS GC 阈值,因此不会触发 CMS GC,...
  • Heap是内存中动态分配对象居住的地方。 如果使用new一个对象,它就被分配在堆内存上。 这是相对于Stack,如果你有一个局部变量则它是位于Stack栈内存空间。 BigMemory是用来避免GC对的开销,从几MB或GB大。 ...
  • Spark 堆外内存

    千次阅读 2018-09-26 09:29:18
    1.堆外内存有哪些 前面提到spark中的堆内存溢出,除了堆内存,还有堆外内存。该部分内存主要用于程序的共享库、Perm Space、 线程Stack和一些Memory mapping等, 或者类C方式allocate object. 堆外内存在Spark中...
  • 今天我想要大家的问题是:如何监控和诊断JVM堆内和堆外内存使用? 概述 了解JVM内存方法有很多,具体能力范围也有区别,简单总结如下: 可以使用综合性的图形化工具,如JConsole、 VisualVM(注意,从Oracle JDK...
  • JVM堆外内存问题排查

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

    千次阅读 2019-07-09 15:56:35
    背景 为了更好地实现对项目...笔者被叫去帮忙查看原因,发现配置了4G内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPre...
  • NIO详解(六):Java堆外内存

    千次阅读 2019-03-20 21:52:30
    堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外...
  • 堆外内存和堆内内存  堆外内存又称为直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域.一直以来是Javaer们难以关注的一片领域,今天我们就一起探索一下这片区域究竟...
  • 堆内内存: 1.一般情况下,一个新的对象创建在JVM内的堆上,并为其分配内存空间。堆空间由JVM垃圾回收器管理,称为堆内内存(on...1.和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内
  • netty 堆外内存泄露排查思路

    千次阅读 2019-03-15 15:47:52
    这篇文章对于排查使用了 netty 引发的堆外内存泄露问题,有一定的通用性,希望对你有所启发 背景 最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级...
  • JAVA直接内存(堆外内存

    千次阅读 2019-03-25 13:57:05
    堆外内存介绍

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 193,807
精华内容 77,522
关键字:

堆外内存