一、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 GCHeap Configuration:MinHeapFreeRatio = 40MaxHeapFreeRatio = 70MaxHeapSize      = 2147483648 (2048.0MB)NewSize          = 16777216 (16.0MB)MaxNewSize       = 33554432 (32.0MB)OldSize          = 50331648 (48.0MB)NewRatio         = 7SurvivorRatio    = 8PermSize         = 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.OutOfMemoryErrorat 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 GCprivate byte[] bytes = null;public RevisedObjectInHeap(){address = unsafe.allocateMemory(2 * 1024 * 1024);bytes = new byte[1024 * 1024];}@Overrideprotected 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堆外内存:堆外内存回收方法相关推荐

  1. java 查看堆外内存占用_如何监控和诊断JVM堆内和堆外内存使用?

    上一讲我介绍了 JVM 内存区域的划分,总结了相关的一些概念,今天我将结合 JVM 参数.工具等方面,进一步分析 JVM 内存结构,包括外部资料相对较少的堆外部分. 今天我要问你的问题是,如何监控和诊 ...

  2. java.nio.DirectByteBuffer管理堆外内存

    堆外内存 堆外内存是相对于堆内内存的一个概念.堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一 ...

  3. 浅谈Java堆内内存、堆外内存、直接内存、用户空间和内核空间

    我们都知道Java中大多数的对象都存在于堆内存中,那什么是堆外内存.直接内存?它们又分别用来做什么?分布在用户空间还是内核空间? 首先,有个前置知识点,所谓的Java程序,其实可以理解为是用C/C++ ...

  4. 堆外内存(直接内存)

    HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷贝得到直接内存,再做下一步操作(考虑细节 ...

  5. Spark 内存管理堆内和堆外内存规划_大数据培训

    堆内和堆外内存规划 作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM 的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存.同时, ...

  6. 堆内存和堆外内存(又名直接内存)优缺点

    堆内存根据生命周期进行分而治之,分区之后可以提高JVM垃圾收集的效率,更好地回收为了更好地分配. 如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常. http ...

  7. 46栈内存溢出、内存区域(程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区、直接内存、内存溢出)与内存溢出(对象实例化分析)

    46.什么情况下会发生栈内存溢出 46.1.Java 内存区域与内存溢出 46.1.1.内存区域 46.1.1.1.程序计数器 46.1.1.2.Java 虚拟机栈 46.1.1.3.本地方法栈 46 ...

  8. java堆内与堆外数据交互_Java:汇总堆外数据

    java堆内与堆外数据交互 探索如何以最小的垃圾回收影响和最大的内存利用率创建堆​​外聚合. 使用Java Map,List和Object创建大型聚合通常会产生大量堆内存开销. 这也意味着,一旦聚合超 ...

  9. Java中的紧凑堆外结构/组合

    在上一篇文章中,我详细介绍了代码对主内存的访问方式的含义. 从那时起,我对使用Java可以做什么以实现更可预测的内存布局有很多疑问. 有些模式可以使用数组支持的结构来应用,我将在另一篇文章中讨论. 这 ...

  10. java堆内存 数据结构_JAVA内存区域

    首先解释下内存溢出和内存泄露之间的区别,为后面的学习做些铺垫: 1.内存溢出和内存泄露的区别和联系 内存溢出 out of memory:是指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你 ...

最新文章

  1. 绿色版mysql使用方法
  2. Win2003配置邮件服务器
  3. 开发日记-20190821 关键词 读书笔记《掌控习惯》DAY 1
  4. Dogleg“狗腿”最优化算法
  5. spring-aop相关概念
  6. 获取组件的方式(方法)
  7. Ubuntu下安装和配置Apache及Apache2
  8. 数字图像处理--引入齐次坐标
  9. springmvc如何使用视图解析器_SpringMVC相关面试题
  10. “阿里云开放平台俱乐部”首站启航
  11. jrtplib使用笔记
  12. 安川机器人PROFINET 配置流程
  13. C#中使用Windows API控制阿里旺旺自动登录 的程序源代码
  14. 服务器lsass系统错误,lsass.exe-系统错误,终结点格式无效 windows2003服务器出错
  15. 商业智能BI的价值,可视化报表等于商业智能BI吗?
  16. padStart()和padEnd()
  17. 开启「浏览器多线程下载」选项
  18. python起笔落笔_书法讲究的是起笔和落笔落是什么意思
  19. java求质因数算法
  20. Android_自定义控件之喜马拉雅6.6.21.3播放进度条

热门文章

  1. xp安全模式下如何修复计算机,xp系统电脑安全模式进不去的处理方法
  2. otf和ctf的意义_北京邮电大学出版社
  3. 快70倍!新一代JS构建工具:ESBuild SWC浅析
  4. 【阿尼亚不会CTF】第六届”蓝帽杯“全国大学生网络安全技能大赛—线上初赛部分writeup
  5. JavaWeb自学笔记,ServletAPI编程常用接口和类
  6. 【微信小程序】黑马优购--05商品详情
  7. unity 谷歌广告介入_Unity为开发人员发布Google广告
  8. mysql数据库引擎(show engines)
  9. 用批处理命令批量ping一个网段的IP
  10. 一篇文章搞懂「低保真原型与高保真原型」