在咱們平常的開發過程當中,遇到問題除了普通的異常(空指針啊,數組越界啊 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
中的做用。使用非堆我以爲大部分的程序員應該還使用不到(我是暫且摸不到的),不過你們能夠了解下,增加知識準沒錯🙈🙈。最後祝你們過個好年~