在咱们平常的开发过程当中,遇到问题除了普通的异常(空指针啊,数组越界啊 and so on),咱们遇到的比较大的问题无非就是
OOM
,频繁FullGC
或者是多线程方面的问题(这块我说不上话🌚),咱们大都数产生的问题也都是与JVM
相关的,而今日则谈谈与它有关联的另一个地方。java
身为一个
java
开发者,咱们首先熟悉的是JVM
(尽管对里面的各类各类回收算法还不算很清晰),它帮咱们管理着各个对象(是的,咱们都有对象🤔)的生命周期,助于程序可以正常的运行下去。可是还有一块区域与它隔岸相望->非堆内存(以下图)。程序员
咱们能够清晰的看出NonHeap
在程序中的位置(以上画图并不表明他们在内存中所占的空间比例状况)。算法
咱们能肯定的是堆里面的东西是咱们去本身操做的,而
NonHeap
就是JVM
留给本身用的,因此方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每一个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。数组
本地起来一个小的Demo,咱们经过Arthas
能够去查看堆空间与非堆空间的状况,以及划分的区域。缓存
普通的开发者应该是用不到的(像我这样🌚🌚🌚),高级以上的开发应该会使用到,由于他们知道如何使一个普通的程序变得不普通。安全
在JAVA
中,能够经过Unsafe
和NIO
包下的ByteBuffer
来操做非堆内存。服务器
一看这名字就知道不安全😹,不过也的确不怎么安全。它位于
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
是Java
用于实现堆外内存的一个重要类,一般用在通讯过程当中作缓冲池,如在Netty
等NIO
框架中应用普遍。DirectByteBuffer 对于堆外内存的建立、使用、销毁等逻辑也是由Unsafe
提供的堆外内存API
来实现,其构造函数就能够直接分配内存。
相关API
康康:
// 分配size大小内存
unsafe.allocateMemory(size);
// 从base位置开始初始化size大小内存
unsafe.setMemory(base, size, (byte) 0);
复制代码
咱们了解到
Heap
的回收都是依赖的是jvm
各个区域的回收算法实现,那么非堆的回收是如何进行呢?以及什么状况下去进行呢?
目前了解到的两种方式:
第一种暂且不过多讨论了。 第二种这里提一提:
咱们经过
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
链表中的对象引用,执行Cleaner
的clean
方法进行相关清理工做。
因此当DirectByteBuffer
仅被 Cleaner
引用(即为虚引用)时,其能够在任意GC
时段被回收。当 DirectByteBuffer
实例对象被回收时,在 Reference-Handler
线程操做中,会调用 Cleaner
的 clean
方法根据建立 Cleaner
时传入的 Deallocator
来进行堆外内存的释放。
咱们了解过堆的做用,那么咱们就好奇下非堆在咱们的程序中占着什么样子的做用?
总结下有两点:
第一点说明:
咱们知道
jvm
中的全部gc
是针对于当前容器内的对象进行回收处理的,在Ygc
阶段,涉及到垃圾标记的过程,从GCRoot
开始标记,一旦扫描到引用到了老年代的对象则中断本次扫描,加速Ygc
的进度,可是Ygc
阶段中的old-gen sacnning
阶段则用于扫描被老年代引用的对象,那么一旦老年代过大,则Ygc
所须要的时间就过长(时间与大小成正比),则不利于当前程序的垃圾回收。因此一旦引入非堆,咱们就能够保持较小的堆内存规模,从而保证gc
的正常进行。
第二点说明:
这里面涉及的主要关于服务器的用户态以及内核态,咱们了解到在服务器上面操做的一个文件传输出去,会涉及到用户态转内核态,而后内核态转用户态等等步骤,其中有些操做是消耗
cpu
资源的(从内存地址缓存区读取以及写入),咱们就会其中的操做是能够省略的,咱们能够直接将文件从磁盘到内存地址缓存区,而后再到套接字缓冲区,这就是所谓的零拷贝技术。
以上部分就是简单的说下非堆在
java
中的做用。使用非堆我以为大部分的程序员应该还使用不到(我是暂且摸不到的),不过你们能够了解下,增加知识准没错🙈🙈。最后祝你们过个好年~