您的位置 首页 java

Java堆外内存你分的清楚吗?不信来试试

Java的内存管理一直是一个很火的话题,今天聊一聊平常比较少关注的堆外内存,也叫直接内存,

不懂不影响生活,懂了就很高级,有木有!!!

看下图:

1、堆外内存是个啥

堆外内存也叫直接内存,因为这部分内存就是机器的物理内存,够直接吧。

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。

使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。

这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

举个通俗一点的例子:

假如操作系统就是你所在小区,

小区的居民就是不同的jvm或者其他的进程(程序)。

有天你想装修,装修材料放在家里,也就是自己的空间,随便玩,放哪里你自己管理就好了,对应到虚拟机就是你常规理解的内存,包括heap,栈等等

但是把装修材料放到家里不方便,而且还要运进来,所以你想我能不能放在公共区域,比如在楼下找块空地放装修材料,这部分就是你要申请使用的堆外内存,也就是机器的物理内存,需要使用native 申请空间。

2.堆外内存的优缺点

优点:

  1 减少了垃圾回收的工作,理论上能减小GC暂停时间,因为堆外内存的释放不受虚拟机管理(虚拟机只是释放句柄,而真正的内存是操作系统释放)

  2 省去了不必要的内存复制,实现zero copy,数据不需要再native memory和jvm memory中来回copy。

缺点️:

  1 堆外内存难以控制,在发生内存泄漏的时候不易排查。谨慎使用。

  2 堆外内存相对来说,不适合存储很复杂的对象。一般则是放大块的内存。格式需要自己定义。

3、堆外内存的控制参数

可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的大小。超过此内存则会报错。

堆外内存可以通过java.nio的ByteBuffer来创建,调用allocateDirect方法申请即可,

这是合法的途径

4、几个常用的类

4.1 unsafe

Unsafe类操作堆外内存

sun.misc.Unsafe提供了一组方法来进行堆外内存的分配,重新分配,以及释放。

 public native long allocateMemory(long size); —— 分配一块内存空间。
public native long reallocateMemory(long address, long size); —— 重新分配一块内存,把数据从address指向的缓存中拷贝到新的内存块。
public native void freeMemory(long address); —— 释放内存。
public class UnsafeTest {
 
public static void main(String[] args) throws Exception {
    //最简单的使用方式是基于反射获取Unsafe实例
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    long address = unsafe.allocateMemory(1024);
    unsafe.freeMemory(address);
}
}
复制代码  

注意:直接使用unsafe 会报错,因为Unsafe不让使用,会产生安全问题

4.2 DirectByteBuffer

ByteBuffer.allocateDirect(1024);

JVM在堆内只保存堆外内存的引用,用DirectByteBuffer对象来表示,类似指针的概念。

每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象。

这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。一般在full gc的时候回收

当DirectByteBuffer对象在某次YGC中被回收,只有Cleaner对象知道堆外内存的地址。

当下一次FGC执行时,Cleaner对象会将自身Cleaner链表上删除,并触发clean方法清理堆外内存。

此时,堆外内存将被回收,Cleaner对象也将在下次YGC时被回收。

如果JVM一直没有执行FGC的话,无法触发Cleaner对象执行clean方法,从而堆外内存也一直得不到释放。

5、内存泄漏的分析

当堆外内存占用较多时,可以尝试强制执行Full Gc看堆外内存是否有下降,这样会调用cleaner 释放堆外内存,如果有下降就很有可能时DirectByteBuffer的原因了。

最好是在启动参数上增加-XX:MaxDirectMemorySize=x[m|g],

例如-XX:MaxDirectMemorySize=500m

最大分配的堆外内存500M,超过这个范围就是OOM。在不设置的时候默认是机器的内存

sun.misc.VM.java里面是如何设置默认的directmemorysize的

 public static long maxDirectMemory() {  
    if (booted)  
        return directMemory;  
    Properties p = System.getProperties();  
    String s = (String)p.remove("sun.nio.MaxDirectMemorySize");  
    System.setProperties(p);  
    if (s != null) {  
        if (s.equals("-1")) {  
            // -XX:MaxDirectMemorySize not given, take default
            // 翻译以下 如果不设置  XX:MaxDirectMemorySize 取默认值就是最大内存
            directMemory = Runtime.getRuntime().maxMemory();  
        } else {  
            long l = Long.parseLong(s);  
            if (l > -1)  
                directMemory = l;  
        }  
    }  
    return directMemory;  
} 
复制代码  

6、应用场景

使用堆外内存有两大场景

6.1 和nio有关的一些API需要使用堆外内存 操作系统内核直接把数据写到堆外内存里,不需要像普通API一样,操作系统内核缓存一份,程序读的时候再复制一份到程序空间,减少了复制的过程,提高了性能,Netty 就是这样做的。

6.2 为业专门调优的时候 根据业务管理堆外开辟一块内存,然后自己管理。

比如一个连接来了批量申请1M给1000个小对象用,连接断开了直接批量释放这1M内存,(如果对于GC那可是管理1000个对象的负担,GC不知道这1000个对象用完可以直接批量释放)因为写代码的人最懂内存里发生了什么事情,所以可以自己写代码用最高效的办法去使用内存。

7、其他知识

使用堆外内存的两种途径,一种是上面的方式

还有一种是JNI写java的c/c++扩展,jni 自己管理内存,和jvm 不关联,只要使用就行

总结: 堆外内存是直接对物理内存的使用,这部分的操作可以直接理解为指针的操作,使用堆外内存扩展了jvm的边界,

缺点就是离开了jvm的管理,对于对内存管理不是很熟悉的同学可能会有风险,

这是一把双刃剑,用的好则无敌,用不好则万劫不复,

在使用之前一定要了解,不然只会埋下深坑。

文章来源:智云一二三科技

文章标题:Java堆外内存你分的清楚吗?不信来试试

文章地址:https://www.zhihuclub.com/184247.shtml

关于作者: 智云科技

热门文章

网站地图