了解NonHeap吗?

4,469 阅读7分钟

在我们日常的开发过程中,遇到问题除了普通的异常(空指针啊,数组越界啊 and so on),我们遇到的比较大的问题无非就是OOM,频繁FullGC或者是多线程方面的问题(这块我说不上话🌚),我们大都数产生的问题也都是与JVM相关的,而今日则谈谈与它有关联的另外一个地方。

NonHeap

身为一个java开发者,我们首先熟悉的是JVM(尽管对里面的各种各种回收算法还不算很清晰),它帮我们管理着各个对象(是的,我们都有对象🤔)的生命周期,助于程序能够正常的运行下去。但是还有一块区域与它隔岸相望->非堆内存(如下图)。

我们可以清晰的看出NonHeap在程序中的位置(以上画图并不代表他们在内存中所占的空间比例情况)。

作用

我们能确定的是堆里面的东西是我们去自己操作的,而NonHeap就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

本地起来一个小的Demo,我们通过Arthas可以去查看堆空间与非堆空间的情况,以及划分的区域。

使用方面

普通的开发者应该是用不到的(像我这样🌚🌚🌚),高级以上的开发应该会使用到,因为他们知道如何使一个普通的程序变得不普通。

JAVA中,可以通过UnsafeNIO包下的ByteBuffer来操作非堆内存。

Unsafe

一看这名字就知道不安全😹,不过也的确不怎么安全。它位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法。内部API大多数是对系统内存直接操作的,这会提高我们程序的运行效率等等,但是也同样会很容易发生错误,他里面操作类似于C语言一样的指针操作,会增加了程序相关指针问题的风险。

我们可以稍微👻康康👻其中部分方法:

// 分配内存 , 相当于 C++ 的 malloc 函数
public native long allocateMemory(long bytes);
// 扩充内存
public native long reallocateMemory(long address, long bytes);
// 释放内存
public native void freeMemory(long address);
// 在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
// 内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
// 获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有 : getInt,getDouble,getLong,getChar 等
public native Object getObject(Object o, long offset);
// 为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有 :putInt,putDouble,putLong,putChar 等
public native void putObject(Object o, long offset, Object x);
// 获取给定地址的 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果为确定的)
public native byte getByte(long address);
// 为给定地址设置 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

除了以上直接操作内存相关的方法,还有一些用于CAS的方法,j.u.c底下的并发集合操作以及相关的锁操作其实大部分都是调用了Unsafe里面的方法来控制。

DirectByteBuffer

DirectByteBufferJava用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 NettyNIO框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑也是由 Unsafe 提供的堆外内存 API 来实现,其构造函数就可以直接分配内存。

相关API康康:

// 分配size大小内存
 unsafe.allocateMemory(size);
 // 从base位置开始初始化size大小内存
 unsafe.setMemory(base, size, (byte) 0);

回收方面

我们了解到Heap的回收都是依赖的是jvm各个区域的回收算法实现,那么非堆的回收是如何进行呢?以及什么情况下去进行呢?

目前了解到的两种方式:

  1. 其垃圾回收依赖于代码显式调用System.gc()。
  2. 依赖垃圾回收追踪对象 Cleaner 实现堆外内存释放。

第一种暂且不过多讨论了。 第二种这里提一提:

我们通过DirectByteBuffer源码查看下当前的类结构,主要注意的是当前对象里面包含了一个Deallocator私有静态内部类以及私有成员属性Cleaner

    private final Cleaner cleaner;

    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) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address); //释放内存
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }
    

从上面我们可以大概知道最后进行非堆内存的回收肯定是静态内部类进行操作的。同时也与成员变量有关系,那么他是怎么进行操作的呢?

这里我们要注意的就是Cleaner对象了。

Cleaner 继承自 Java 四大引用类型之一的虚引用 PhantomReference(我们了解到无法通过虚引用获取与之关联的对象实例,且当对象仅被虚引用引用时,在任何发生 GC的时候,其均可被回收),通常 PhantomReference 与引用队列ReferenceQueue 结合使用,可以实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。如下图所示,当某个被 Cleaner 引用的对象将被回收时,JVM 垃圾收集器会将此对象的引用放入到对象引用中的 pending 链表中,等待Reference-Handler进行相关处理。其中,Reference-Handler为一个拥有最高优先级的守护线程,会循环不断的处理 pending 链表中的对象引用,执行 Cleanerclean 方法进行相关清理工作。

所以当DirectByteBuffer 仅被 Cleaner 引用(即为虚引用)时,其可以在任意GC 时段被回收。当 DirectByteBuffer 实例对象被回收时,在 Reference-Handler线程操作中,会调用 Cleanerclean 方法根据创建 Cleaner 时传入的 Deallocator 来进行堆外内存的释放。

作用

我们了解过堆的作用,那么我们就好奇下非堆在我们的程序中占着什么样子的作用?

总结下有两点:

  1. 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM, 所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减 少回收停顿对于应用的影响。
  2. 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内 存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存 数据,都建议存储到堆外内存。

第一点说明:

我们知道jvm中的所有gc是针对于当前容器内的对象进行回收处理的,在Ygc阶段,涉及到垃圾标记的过程,从GCRoot开始标记,一旦扫描到引用到了老年代的对象则中断本次扫描,加速Ygc的进度,但是Ygc阶段中的old-gen sacnning阶段则用于扫描被老年代引用的对象,那么一旦老年代过大,则Ygc所需要的时间就过长(时间与大小成正比),则不利于当前程序的垃圾回收。所以一旦引入非堆,我们就可以保持较小的堆内存规模,从而保证gc的正常进行。

第二点说明:

这里面涉及的主要关于服务器的用户态以及内核态,我们了解到在服务器上面操作的一个文件传输出去,会涉及到用户态转内核态,然后内核态转用户态等等步骤,其中有些操作是消耗cpu资源的(从内存地址缓存区读取以及写入),我们就会其中的操作是可以省略的,我们可以直接将文件从磁盘到内存地址缓存区,然后再到套接字缓冲区,这就是所谓的零拷贝技术。

结尾

以上部分就是简单的说下非堆在java中的作用。使用非堆我觉得大部分的程序员应该还使用不到(我是暂且摸不到的),不过大家可以了解下,增长知识准没错🙈🙈。最后祝大家过个好年~