了解NonHeap吗?

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

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中的做用。使用非堆我以为大部分的程序员应该还使用不到(我是暂且摸不到的),不过你们能够了解下,增加知识准没错🙈🙈。最后祝你们过个好年~

相关文章
相关标签/搜索