Java堆棧內(nèi)存與堆外內(nèi)存
- 堆棧內(nèi)存
堆棧內(nèi)存指的是堆內(nèi)存和棧內(nèi)存:堆內(nèi)存是GC管理的內(nèi)存,棧內(nèi)存是線程內(nèi)存。
堆內(nèi)存結(jié)構(gòu):

還有一個更細致的結(jié)構(gòu)圖(包括MetaSpace還有code cache):
注意在Java8以后PermGen被MetaSpace代替,運行時可自動擴容,并且默認是無限大

我們看下面一段代碼來簡單理解下堆棧的關(guān)系:
public static void main(String[] args) {
Object o = new Object();
}
其中new Object()是在堆上面分配,而Object o這個變量,是在main這個線程棧上面。
應(yīng)用程序所有的部分都使用堆內(nèi)存,然后棧內(nèi)存通過一個線程運行來使用。
不論對象什么時候創(chuàng)建,他都會存儲在堆內(nèi)存中,棧內(nèi)存包含它的引用。棧內(nèi)存只包含原始值變量好和堆中對象變量的引用。
存儲在堆中的對象是全局可以被訪問的,然而棧內(nèi)存不能被其他線程所訪問。
通過JVM參數(shù)-Xmx我們可以指定最大堆內(nèi)存大小,通過-Xss我們可以指定每個線程線程棧占用內(nèi)存大小
- 堆外內(nèi)存
2.1. 廣義的堆外內(nèi)存
除了堆棧內(nèi)存,剩下的就都是堆外內(nèi)存了,包括了jvm本身在運行過程中分配的內(nèi)存,codecache,jni里分配的內(nèi)存,DirectByteBuffer分配的內(nèi)存等等
2.2. 狹義的堆外內(nèi)存 - DirectByteBuffer
而作為java開發(fā)者,我們常說的堆外內(nèi)存溢出了,其實是狹義的堆外內(nèi)存,這個主要是指java.nio.DirectByteBuffer在創(chuàng)建的時候分配內(nèi)存,我們這篇文章里也主要是講狹義的堆外內(nèi)存,因為它和我們平時碰到的問題比較密切
為啥要使用堆外內(nèi)存。通常因為:
在進程間可以共享,減少虛擬機間的復(fù)制
對垃圾回收停頓的改善:如果應(yīng)用某些長期存活并大量存在的對象,經(jīng)常會出發(fā)YGC或者FullGC,可以考慮把這些對象放到堆外。過大的堆會影響Java應(yīng)用的性能。如果使用堆外內(nèi)存的話,堆外內(nèi)存是直接受操作系統(tǒng)管理( 而不是虛擬機 )。這樣做的結(jié)果就是能保持一個較小的堆內(nèi)內(nèi)存,以減少垃圾收集對應(yīng)用的影響。
在某些場景下可以提升程序I/O操縱的性能。少去了將數(shù)據(jù)從堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的步驟。
- JNI調(diào)用與內(nèi)核態(tài)及用戶態(tài)
內(nèi)核態(tài):cpu可以訪問內(nèi)存的所有數(shù)據(jù),包括外圍設(shè)備,例如硬盤,網(wǎng)卡,cpu也可以將自己從一個程序切換到另一個程序。
用戶態(tài):只能受限的訪問內(nèi)存,且不允許訪問外圍設(shè)備,占用cpu的能力被剝奪,cpu資源可以被其他程序獲取。
系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源,內(nèi)核為上層應(yīng)用提供訪問的接口
我們舉個例子,文件讀?。籎ava本身并不能讀取文件,因為用戶態(tài)沒有權(quán)限訪問外圍設(shè)備。需要通過系統(tǒng)調(diào)用切換內(nèi)核態(tài)進行讀取。
目前,JAVA的IO方式有基于流的傳統(tǒng)IO還有基于塊的NIO方式(雖然文件讀取其實不是嚴格意義上的NIO,哈哈)。面向流意味著從流中一次可以讀取一個或多個字節(jié),拿到讀取的這些做什么你說了算,這里沒有任何緩存(這里指的是使用流沒有任何緩存,接收或者發(fā)送的數(shù)據(jù)是緩存到操作系統(tǒng)中的,流就像一根水管從操作系統(tǒng)的緩存中讀取數(shù)據(jù))而且只能順序從流中讀取數(shù)據(jù),如果需要跳過一些字節(jié)或者再讀取已經(jīng)讀過的字節(jié),你必須將從流中讀取的數(shù)據(jù)先緩存起來。面向塊的處理方式有些不同,數(shù)據(jù)是先被 讀/寫到buffer中的,根據(jù)需要你可以控制讀取什么位置的數(shù)據(jù)。這在處理的過程中給用戶多了一些靈活性,然而,你需要額外做的工作是檢查你需要的數(shù)據(jù)是否已經(jīng)全部到了buffer中,你還需要保證當(dāng)有更多的數(shù)據(jù)進入buffer中時,buffer中未處理的數(shù)據(jù)不會被覆蓋。
我們這里只分析基于塊的NIO方式,在JAVA中這個塊就是ByteBuffer。
- Linux下零拷貝原理
大部分web服務(wù)器都要處理大量的靜態(tài)內(nèi)容,而其中大部分都是從磁盤文件中讀取數(shù)據(jù)然后寫到socket中。我們以這個過程為例子,來看下不同模式下Linux工作流程
4.1. 普通Read/Write模式
涉及的代碼抽象:
//從文件中讀取,存入tmp_buf
read(file, tmp_buf, len);
//將tmp_buf寫入socket
write(socket, tmp_buf, len);
看上去很簡單的步驟但是經(jīng)過了很多復(fù)制:
當(dāng)調(diào)用 read 系統(tǒng)調(diào)用時,通過 DMA(Direct Memory Access)將數(shù)據(jù) copy 到內(nèi)核模式
然后由 CPU 控制將內(nèi)核模式數(shù)據(jù) copy 到用戶模式下的 buffer 中
read 調(diào)用完成后,write 調(diào)用首先將用戶模式下 buffer 中的數(shù)據(jù) copy 到內(nèi)核模式下的 socket buffer 中
最后通過 DMA copy 將內(nèi)核模式下的 socket buffer 中的數(shù)據(jù) copy 到網(wǎng)卡設(shè)備中傳送。
從上面的過程可以看出,數(shù)據(jù)白白從內(nèi)核模式到用戶模式走了一圈,浪費了兩次 copy(第一次,從kernel模式拷貝到user模式;第二次從user模式再拷貝回kernel模式,即上面4次過程的第2和3步驟。),而這兩次 copy 都是 CPU copy,即占用CPU資源
4.2. sendfile模式

通過 sendfile 傳送文件只需要一次系統(tǒng)調(diào)用,當(dāng)調(diào)用 sendfile 時:
首先通過 DMA copy 將數(shù)據(jù)從磁盤讀取到 kernel buffer 中
然后通過 CPU copy 將數(shù)據(jù)從 kernel buffer copy 到 sokcet buffer 中
最終通過 DMA copy 將 socket buffer 中數(shù)據(jù) copy 到網(wǎng)卡 buffer 中發(fā)送 sendfile 與 read/write 方式相比,少了 一次模式切換一次 CPU copy。但是從上述過程中也可以發(fā)現(xiàn)從 kernel buffer 中將數(shù)據(jù) copy 到socket buffer 是沒必要的。
4.3. sendfile模式改進
Linux2.4 內(nèi)核對sendFile模式進行了改進:

改進后的處理過程如下:
DMA copy 將磁盤數(shù)據(jù) copy 到 kernel buffer 中 2.向 socket buffer 中追加當(dāng)前要發(fā)送的數(shù)據(jù)在 kernel buffer 中的位置和偏移量
DMA gather copy 根據(jù) socket buffer 中的位置和偏移量直接將 kernel buffer 中的數(shù)據(jù) copy 到網(wǎng)卡上。
經(jīng)過上述過程,數(shù)據(jù)只經(jīng)過了 2 次 copy 就從磁盤傳送出去了。(事實上這個 Zero copy 是針對內(nèi)核來講的,數(shù)據(jù)在內(nèi)核模式下是 Zero-copy 的)。
當(dāng)前許多高性能 http server 都引入了 sendfile 機制,如 nginx,lighttpd 等。
- Java零拷貝實現(xiàn)的變化
Zero-Copy技術(shù)省去了將操作系統(tǒng)的read buffer拷貝到程序的buffer,以及從程序buffer拷貝到socket buffer的步驟,直接將read buffer拷貝到socket buffer. Java NIO中的FileChannal.transferTo()方法就是這樣的實現(xiàn)
public void transferTo(long position,long count,WritableByteChannel target);
transferTo()方法將數(shù)據(jù)從一個channel傳輸?shù)搅硪粋€可寫的channel上,其內(nèi)部實現(xiàn)依賴于操作系統(tǒng)對zero copy技術(shù)的支持。在unix操作系統(tǒng)和各種linux的發(fā)型版本中,這種功能最終是通過sendfile()系統(tǒng)調(diào)用實現(xiàn)。下邊就是這個方法的定義:
include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在內(nèi)核為2.4或者以上版本的linux系統(tǒng)上,socket緩沖區(qū)描述符將被用來滿足這個需求。這個方式不僅減少了內(nèi)核用戶態(tài)間的切換,而且也省去了那次需要cpu參與的復(fù)制過程。 從用戶角度來看依舊是調(diào)用transferTo()方法,但是其本質(zhì)發(fā)生了變化:
調(diào)用transferTo方法后數(shù)據(jù)被DMA從文件復(fù)制到了內(nèi)核的一個緩沖區(qū)中。
數(shù)據(jù)不再被復(fù)制到socket關(guān)聯(lián)的緩沖區(qū)中了,僅僅是將一個描述符(包含了數(shù)據(jù)的位置和長度等信息)追加到socket關(guān)聯(lián)的緩沖區(qū)中。DMA直接將內(nèi)核中的緩沖區(qū)中的數(shù)據(jù)傳輸給協(xié)議引擎,消除了僅剩的一次需要cpu周期的數(shù)據(jù)復(fù)制。
5.3 對于JAVA普通字節(jié)流IO與NIOFileChannel實現(xiàn)的零拷貝性能:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
public class FileCopyTest {
/**
* 通過字節(jié)流的方式復(fù)制文件
* @param fromFile 源文件
* @param toFile 目標(biāo)文件
* @throws FileNotFoundException 未找到文件異常
*/
public static void fileCopyNormal(File fromFile, File toFile) throws FileNotFoundException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = new BufferedInputStream(new FileInputStream(fromFile));
outputStream = new BufferedOutputStream(new FileOutputStream(toFile));
//用戶態(tài)緩沖有1kB這么大,不算小了
byte[] bytes = new byte[1024];
int i;
//讀取到輸入流數(shù)據(jù),然后寫入到輸出流中去,實現(xiàn)復(fù)制
while ((i = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 用filechannel進行文件復(fù)制
*
* @param fromFile 源文件
* @param toFile 目標(biāo)文件
*/
public static void fileCopyWithFileChannel(File fromFile, File toFile) {
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
FileChannel fileChannelInput = null;
FileChannel fileChannelOutput = null;
try {
fileInputStream = new FileInputStream(fromFile);
fileOutputStream = new FileOutputStream(toFile);
//得到fileInputStream的文件通道
fileChannelInput = fileInputStream.getChannel();
//得到fileOutputStream的文件通道
fileChannelOutput = fileOutputStream.getChannel();
//將fileChannelInput通道的數(shù)據(jù),寫入到fileChannelOutput通道
fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
if (fileChannelInput != null) {
fileChannelInput.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
if (fileChannelOutput != null) {
fileChannelOutput.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
File fromFile = new File("D:/readFile.txt");
File toFile = new File("D:/outputFile.txt");
//預(yù)熱
fileCopyNormal(fromFile, toFile);
fileCopyWithFileChannel(fromFile, toFile);
//計時
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
fileCopyNormal(fromFile, toFile);
}
System.out.println("fileCopyNormal time: " + (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
fileCopyWithFileChannel(fromFile, toFile);
}
System.out.println("fileCopyWithFileChannel time: " + (System.currentTimeMillis() - start));
}
}
測試結(jié)果:
fileCopyNormal time: 14271
fileCopyWithFileChannel time: 6632
差了一倍多的時間(文件大小大概8MB),如果文件更大這個差距應(yīng)該更加明顯。
- DirectBuffer分配
Java中NIO的核心緩沖就是ByteBuffer,所有的IO操作都是通過這個ByteBuffer進行的;Bytebuffer有兩種: 分配HeapByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(int capacity);
分配DirectByteBuffer
ByteBuffer buffer = ByteBuffer.allocateDirect(int capacity);
6.1. 為何HeapByteBuffer會多一次拷貝?
6.1.1. FileChannel的force api說明
FileChannel的force方法: FileChannel.force()方法將通道里尚未寫入磁盤的數(shù)據(jù)強制寫到磁盤上。出于性能方面的考慮,操作系統(tǒng)會將數(shù)據(jù)緩存在內(nèi)存中,所以無法保證寫入到FileChannel里的數(shù)據(jù)一定會即時寫到磁盤上。要保證這一點,需要調(diào)用force()方法。 force()方法有一個boolean類型的參數(shù),指明是否同時將文件元數(shù)據(jù)(權(quán)限信息等)寫到磁盤上。
6.1.2. FileChannel和SocketChannel依賴的IOUtil源碼解析
無論是FileChannel還是SocketChannel,他們的讀寫方法都依賴IOUtil的相同方法,我們這里來看下: IOUtil.java
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
//如果是DirectBuffer,直接寫
if (var1 instanceof DirectBuffer) {
return writeFromNativeBuffer(var0, var1, var2, var4);
} else {
//非DirectBuffer
//獲取已經(jīng)讀取到的位置
int var5 = var1.position();
//獲取可以讀到的位置
int var6 = var1.limit();
assert var5 <= var6;
//申請一個源buffer可讀大小的DirectByteBuffer
int var7 = var5 <= var6 ? var6 - var5 : 0;
ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
int var10;
try {
var8.put(var1);
var8.flip();
var1.position(var5);
//通過DirectBuffer寫
int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
if (var9 > 0) {
var1.position(var5 + var9);
}
var10 = var9;
} finally {
//回收分配的DirectByteBuffer
Util.offerFirstTemporaryDirectBuffer(var8);
}
return var10;
}
}
//讀的方法和寫類似,這里省略
6.1.3. 為何一定要復(fù)制到DirectByteBuffer來讀寫(系統(tǒng)調(diào)用)
首先,先說一點,執(zhí)行native方法的線程,被認為是處于SafePoint,所以,會發(fā)生 NIO 如果不復(fù)制到 DirectByteBuffer,就會有 GC 發(fā)生重排列對象內(nèi)存的情況
傳統(tǒng) BIO 是面向 Stream 的,底層實現(xiàn)可以理解為寫入的是 byte 數(shù)組,調(diào)用 native 方法寫入 IO,傳的參數(shù)是這個數(shù)組,就算GC改變了內(nèi)存地址,但是拿這個數(shù)組的引用照樣能找到最新的地址,,對應(yīng)的方法時是:(bio 也是會申請臨時的直接內(nèi)存的)FileOutputStream.write
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;
但是NIO,為了提升效率,傳的是內(nèi)存地址,省去了一次間接應(yīng)用,但是就必須用 DirectByteBuffer 防止內(nèi)存地址改變,對應(yīng)的是 NativeDispatcher.write
abstract int write(FileDescriptor fd, long address, int len)
throws IOException;
那為何內(nèi)存地址會改變呢?GC會回收無用對象,同時還會進行碎片整理,移動對象在內(nèi)存中的位置,來減少內(nèi)存碎片。DirectByteBuffer不受GC控制。如果不用DirectByteBuffer而是用HeapByteBuffer,如果在調(diào)用系統(tǒng)調(diào)用時,發(fā)生了GC,導(dǎo)致HeapByteBuffer內(nèi)存位置發(fā)生了變化,但是內(nèi)核態(tài)并不能感知到這個變化導(dǎo)致系統(tǒng)調(diào)用讀取或者寫入錯誤的數(shù)據(jù)。所以一定要通過不受GC影響的HeapByteBuffer
假設(shè)我們要從網(wǎng)絡(luò)中讀入一段數(shù)據(jù),再把這段數(shù)據(jù)發(fā)送出去的話,采用Non-direct ByteBuffer的流程是這樣的:
網(wǎng)絡(luò) –> 臨時的DirectByteBuffer –> 應(yīng)用 Non-direct ByteBuffer –> 臨時的Direct ByteBuffer –> 網(wǎng)絡(luò)
這種方式是直接在堆外分配一個內(nèi)存(即,native memory)來存儲數(shù)據(jù), 程序通過JNI直接將數(shù)據(jù)讀/寫到堆外內(nèi)存中。因為數(shù)據(jù)直接寫入到了堆外內(nèi)存中,所以這種方式就不會再在JVM管控的堆內(nèi)再分配內(nèi)存來存儲數(shù)據(jù)了,也就不存在堆內(nèi)內(nèi)存和堆外內(nèi)存數(shù)據(jù)拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外內(nèi)存地址傳給JNI的I/O的函數(shù)就好了。
采用Direct ByteBuffer的流程是這樣的:
網(wǎng)絡(luò) –> 應(yīng)用 Direct ByteBuffer –> 網(wǎng)絡(luò)
可以看到,除開構(gòu)造和析構(gòu)臨時Direct ByteBuffer的時間外,起碼還能節(jié)約兩次內(nèi)存拷貝的時間。那么是否在任何情況下都采用Direct Buffer呢?
不是。對于大部分應(yīng)用而言,兩次內(nèi)存拷貝的時間幾乎可以忽略不計,而構(gòu)造和析構(gòu)DirectBuffer的時間卻相對較長。在JVM的實現(xiàn)當(dāng)中,某些方法會緩存一部分臨時Direct ByteBuffer,意味著如果采用Direct ByteBuffer僅僅能節(jié)約掉兩次內(nèi)存拷貝的時間, 而無法節(jié)約構(gòu)造和析構(gòu)的時間。就用Sun的實現(xiàn)來說,write(ByteBuffer)和read(ByteBuffer)方法都會緩存臨時Direct ByteBuffer,而write(ByteBuffer[])和read(ByteBuffer[])每次都生成新的臨時Direct ByteBuffer。
6.2. ByteBuffer創(chuàng)建
6.2.1. ByteBuffer創(chuàng)建HeapByteBuffer
分配在堆上的,直接由Java虛擬機負責(zé)垃圾收集,你可以把它想象成一個字節(jié)數(shù)組的包裝類
6.2.2. DirectByteBuffer
這個類就沒有HeapByteBuffer簡單了
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
Bits.reserveMemory(size, cap) 方法
static void reserveMemory(long size, int cap) {
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
在DirectByteBuffer中,首先向Bits類申請額度,Bits類有一個全局的totalCapacity變量,記錄著全部DirectByteBuffer的總大小,每次申請,都先看看是否超限,堆外內(nèi)存的限額默認與堆內(nèi)內(nèi)存(由-Xmx 設(shè)定)相仿,可用 -XX:MaxDirectMemorySize 重新設(shè)定。
如果不指定,該參數(shù)的默認值為Xmx的值減去1個Survior區(qū)的值。 如設(shè)置啟動參數(shù)-Xmx20M -Xmn10M -XX:SurvivorRatio=8,那么申請20M-1M=19M的DirectMemory
如果已經(jīng)超限,會主動執(zhí)行Sytem.gc(),期待能主動回收一點堆外內(nèi)存。System.gc()會觸發(fā)一個full gc,當(dāng)然前提是你沒有顯示的設(shè)置-XX:+DisableExplicitGC來禁用顯式GC。并且你需要知道,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行。然后休眠一百毫秒,看看totalCapacity降下來沒有,如果內(nèi)存還是不足,就拋出OOM異常。如果額度被批準,就調(diào)用大名鼎鼎的sun.misc.Unsafe去分配內(nèi)存,返回內(nèi)存基地址
所以,一般的框架里面,會在啟動時申請一大塊DirectByteBuffer,然后自己做內(nèi)存管理
最后,創(chuàng)建一個Cleaner,并把代表清理動作的Deallocator類綁定 – 降低Bits里的totalCapacity,并調(diào)用Unsafe調(diào)free去釋放內(nèi)存。
6.2.3. ByteBuffer回收
HeapByteBuffer就不要說了,GC就幫忙處理了。這兒主要說下DirectByteBuffer 存在于堆內(nèi)的DirectByteBuffer對象很小,只存著基地址和大小等幾個屬性,和一個Cleaner,但它代表著后面所分配的一大段內(nèi)存,是所謂的冰山對象。

其中first是Cleaner類的靜態(tài)變量,Cleaner對象在初始化時會被添加到Clener鏈表中,和first形成引用關(guān)系,ReferenceQueue是用來保存需要回收的Cleaner對象。
如果該DirectByteBuffer對象在一次GC中被回收了

此時,只有Cleaner對象唯一保存了堆外內(nèi)存的數(shù)據(jù)(開始地址、大小和容量),在下一次Full GC時,把該Cleaner對象放入到ReferenceQueue中,并觸發(fā)clean方法。
快速回顧一下堆內(nèi)的GC機制,當(dāng)新生代滿了,就會發(fā)生young gc;如果此時對象還沒失效,就不會被回收;撐過幾次young gc后,對象被遷移到老生代;當(dāng)老生代也滿了,就會發(fā)生full gc。
這里可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小,只要熬過了young gc,即使已經(jīng)失效了也能在老生代里舒服的呆著,不容易把老生代撐爆觸發(fā)full gc,如果沒有別的大塊頭進入老生代觸發(fā)full gc,就一直在那耗著,占著一大片堆外內(nèi)存不釋放。
這時,就只能靠前面提到的申請額度超限時觸發(fā)的system.gc()來救場了。但這道最后的保險其實也不很好,首先它會中斷整個進程,然后它讓當(dāng)前線程睡了整整一百毫秒,而且如果gc沒在一百毫秒內(nèi)完成,它仍然會無情的拋出OOM異常。還有,萬一,萬一大家迷信某個調(diào)優(yōu)指南設(shè)置了-DisableExplicitGC禁止了system.gc(),那就不好玩了。
所以,堆外內(nèi)存還是自己主動點回收更好,比如Netty就是這么做的
- 查看DirectBuffer使用情況的方法:
7.1. 進程內(nèi)獲取:
MBeanServer mbs = ManagementFactory. getPlatformMBeanServer() ;
ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct" ) ;
MBeanInfo info = mbs.getMBeanInfo(objectName) ;
for(MBeanAttributeInfo i : info.getAttributes()) {
System.out .println(i.getName() + ":" + mbs.getAttribute(objectName , i.getName()));
}
7.2. 遠程進程
JMX獲取 如果目標(biāo)機器沒有啟動JMX,那么添加jvm參數(shù):
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremotAe.ssl=false
重啟進程 然后本機通過JMX連接訪問:
String jmxURL = "service:jmx:rmi:///jndi/rmi://10.125.6.204:9999/jmxrmi" ;
JMXServiceURL serviceURL = new JMXServiceURL(jmxURL);
Map map = new HashMap() ;
String[] credentials = new String[] { "monitorRole" , "QED" } ;
map.put( "jmx.remote.credentials" , credentials) ;
JMXConnector connector = JMXConnectorFactory. connect(serviceURL , map);
MBeanServerConnection mbsc = connector.getMBeanServerConnection() ;
ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct" ) ;
MBeanInfo mbInfo = mbsc.getMBeanInfo(objectName) ;
for(MBeanAttributeInfo i : mbInfo.getAttributes()) {
System.out .println(i.getName() + ":" + mbsc.getAttribute(objectName , i.getName()));
}