我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼
文章目錄
- 一、DirectByteBuffer 架構
- 二、DirectByteBuffer 實戰 Demo
- 三、DirectByteBuffer 源碼剖析
- 3.1 allocateDirect 方法
- 3.2 構造方法 DirectByteBuffer(int)
- 3.3 reserveMemory 方法
- 3.3.1 tryReserveMemory 方法
- 3.3.2 getJavaLangRefAccess 方法
- 3.3.3 tryHandlePendingReference 方法
- 3.3.4 System.gc()
- 3.3.5 獲取內存的9次嘗試
- 3.4 allocateMemory 方法
- 3.5 unreserveMemory 方法
- 3.6 構造 Cleaner對象
- 四、總結
- 五、番外篇
- Clean 直接看上一章 【JAVA Reference】Cleaner 源碼剖析(三)
一、DirectByteBuffer 架構
1.1 代碼UML
- DirectByteBuffer對象本身其實是很小的,但是它後面可能關聯了一個非常大的堆外內存,因此我們通常稱之爲冰山對象.
1.2 申請內存Flow圖
二、DirectByteBuffer 實戰 Demo
2.1 使用 ByteBuffer.allocateDirect 申請堆外內存
// @VM args:-XX:MaxDirectMemorySize=40m
public class _07_00_TestDirectByteBuffer {
public static void main(String[] args) {
while(true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
}
}
}
結果: 設置堆內存最大40m,但是代碼完全不停歇,毫無對內存滿的事情發生。
- 爲什麼不會 OOM 呢?
把 GC 信息打印出來 : @VM -verbose:gc -XX:+PrintGCDetails
...
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0041255 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 665K->64K(38400K)] 1382K->780K(125952K), 0.0009704 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0042594 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 665K->96K(38400K)] 1382K->812K(125952K), 0.0008948 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 96K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 812K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0033712 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 665K->64K(38400K)] 1382K->780K(125952K), 0.0014349 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0147563 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 1996K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 6% used [0x0000000795580000,0x0000000795773370,0x0000000797600000)
from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
ParOldGen total 87552K, used 716K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 0% used [0x0000000740000000,0x00000007400b3350,0x0000000745580000)
Metaspace used 3154K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 341K, capacity 388K, committed 512K, reserved 1048576K
- 結論: JVM 不斷地進行 Full GC,因此不會造成內存不足。
2.2 加上 -XX:+DisableExplicitGC 後
- 這句話的效果是:禁止 System.gc(),也就是禁止主動調用垃圾回收
輸出:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at indi.sword.util.basic.reference._07_00_TestDirectByteBuffer.main(_07_00_TestDirectByteBuffer.java:12)
Heap
PSYoungGen total 38400K, used 6017K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 18% used [0x0000000795580000,0x0000000795b60680,0x0000000797600000)
from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
to space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
ParOldGen total 87552K, used 0K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 0% used [0x0000000740000000,0x0000000740000000,0x0000000745580000)
Metaspace used 3154K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
- 結論:禁止 System.gc(),JVM 內存不足了,爲什麼會這樣呢???
那麼肯定是 allocateDirect 有調用 System.gc()。
三、DirectByteBuffer 源碼剖析
3.1 allocateDirect 方法
- ByteBuffer#allocateDirect 方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
3.2 構造方法 DirectByteBuffer(int)
- DirectByteBuffer#DirectByteBuffer(int) 方法
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));
//=== 3.3 告訴內存管理器要分配內存
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 3.4 分配直接內存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
// 3.5 通知 bits 釋放內存
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;
}
// 3.6 創建Cleaner!!!! 重點講這個
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
3.3 reserveMemory 方法
- Bits#reserveMemory 方法
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
// 初始化maxMemory,就是使用-XX:MaxDirectMemorySize指定的最大直接內存大小
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
//=== 3.3.1 第一次先採取最樂觀的方式直接嘗試告訴Bits要分配內存
if (tryReserveMemory(size, cap)) {
return;
}
// 內存獲取失敗 往下走。。。
// === 3.3.2 獲取 JavaLangRefAccess
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
/*
tryHandlePendingReference方法:消耗 pending 隊列,1. 丟到 Enqueue隊列,2. 調用 cleaner.clean() 方法釋放內存。
=== 3.3.3 嘗試多次獲取
失敗:tryHandlePendingReference方法的 pending 隊列完盡
成功:釋放了空間,tryReserveMemory 成功
*/
while (jlra.tryHandlePendingReference()) { // 這個地方返回 false ,也就是 pending 隊列完盡,就返回 false
if (tryReserveMemory(size, cap)) {
return;
}
}
// trigger VM's Reference processing
//=== 3.3.4 Full GC 看到沒有!!!! System.gc()在這
System.gc();
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
// === 3.3.5 9次循環,不斷延遲要求分配內存
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
// 不斷*2,按照1ms,2ms,4ms,...,256ms的等待間隔嘗試9次分配內存
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) { //最多循環9次, final int MAX_SLEEPS = 9;
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1; // 左邊移動一位,也就 * 2
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
3.3.1 tryReserveMemory 方法
- Bits#tryReserveMemory 方法
// -XX:MaxDirectMemorySize限制的是總cap,而不是真實的內存使用量,(在頁對齊的情況下,真實內存使用量和總cap是不同的)
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
long totalCap;
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
3.3.2 getJavaLangRefAccess 方法
- SharedSecrets.getJavaLangRefAccess()
這個是個啥呀?
public static JavaLangRefAccess getJavaLangRefAccess() {
return javaLangRefAccess;
}
看下 set 方法的引用(Idea中 Fn + Option + 7 )
原來是存在與 Reference 類的 static 塊裏面
看下:【JAVA Reference】ReferenceQueue 與 Reference 源碼剖析(二)# Reference的static代碼塊
3.3.3 tryHandlePendingReference 方法
- JavaLangRefAccess#tryHandlePendingReference 方法
package sun.misc;
public interface JavaLangRefAccess {
/**
* Help ReferenceHandler thread process next pending
* {@link java.lang.ref.Reference}
*
* @return {@code true} if there was a pending reference and it
* was enqueue-ed or {@code false} if there was no
* pending reference
*/
boolean tryHandlePendingReference();
}
- 看註釋,該方法協助 ReferenceHandler內部線程進行下一個 pending 的處理,內部主要是希望遇到 Cleaner,然後調用 c.clean(); 進行堆外內存的釋放。
- 從上一個方法== 3.3.2 getJavaLangRefAccess 方法 ==可以知道,該方法已經被 @Override,具體實現是 === java.lang.ref.Reference#tryHandlePending 方法 ===
while (jlra.tryHandlePendingReference()) { // 這個地方返回 false ,也就是 pending 隊列沒有了,就返回 false
if (tryReserveMemory(size, cap)) {
return;
}
}
while 能夠把當前全部 pending 隊列中的 reference 都消化掉,要麼Enqueue,要麼Cleaner去進行 clean() 操作。
=== 3.3.3 while 死循環嘗試申請內存
tryHandlePendingReference方法:消耗 pending 隊列,1. 丟到 Enqueue隊列,2. 調用 cleaner.clean() 方法釋放內存。
失敗:tryHandlePendingReference方法的 pending 隊列完盡
成功:釋放了空間,tryReserveMemory 成功
只有這樣消耗光了 pending,纔會往下走 === 3.3.4 System.gc() == ;
3.3.4 System.gc()
- 假如上述的步驟還是沒能釋放內存的話,那麼將會觸發 Full GC。但我們知道,調用System.gc()並不能夠保證full gc馬上就能被執行。所以在後面代碼中,會進行最多MAX_SLEEPS = 9次嘗試,看是否有足夠的可用堆外內存來分配堆外內存。並且每次嘗試之前,都對延遲(*2)等待時間,已給JVM足夠的時間去完成full gc操作。
- 這個地方要注意:如果設置了-XX:+DisableExplicitGC,將會禁用顯示GC,這會使System.gc()調用無效。
3.3.5 獲取內存的9次嘗試
- 如果9次嘗試後依舊沒有足夠的可用堆外內存來分配本次堆外內存,則 throw new OutOfMemoryError(“Direct buffer memory”);
3.4 allocateMemory 方法
- sun.misc.Unsafe#allocateMemory
public native long allocateMemory(long bytes);
- 看下 unsafe
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer{
// Cached unsafe-access object
protected static final Unsafe unsafe = Bits.unsafe();
}
- java.nio.Bits#unsafe
class Bits {
private static final Unsafe unsafe = Unsafe.getUnsafe();
static Unsafe unsafe() {
return unsafe;
}
}
- sun.misc.Unsafe#getUnsafe
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
- 總結: ByteBuffer.allocateDirect(int capacity) 的底層是 unsafe.allocateMemory()
3.4.1 Unsafe類
Java提供了Unsafe類用來進行直接內存的分配與釋放
public native long allocateMemory(long var1);
public native void freeMemory(long var1);
3.5 unreserveMemory 方法
- java.nio.Bits#unreserveMemory
static void unreserveMemory(long size, int cap) {
long cnt = count.decrementAndGet();
long reservedMem = reservedMemory.addAndGet(-size);
long totalCap = totalCapacity.addAndGet(-cap);
assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
}
3.6 構造 Cleaner對象
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
這個create靜態方法提供給我們來實例化Cleaner對象,需要兩個參數:
- 被引用的對象
- 實現了Runnable接口的對象,這個用來回調的時候執行內部的 run 方法
新創建的Cleaner對象被加入到了 dummyQueue 隊列裏。
Cleaner#create方法「【JAVA Reference】Cleaner 源碼剖析(三)」
3.6.1 Deallocator 對象
- 內部類 java.nio.DirectByteBuffer.Deallocator
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;
}
// 3.6.2 調用C++代碼釋放堆外內存
unsafe.freeMemory(address);
address = 0; // 設置爲0,表示已經釋放了
// 剛剛的 3.5 釋放後,標記資源
Bits.unreserveMemory(size, capacity);
}
}
3.6.2 freeMemory 方法
/**
* Disposes of a block of native memory, as obtained from {@link
* #allocateMemory} or {@link #reallocateMemory}. The address passed to
* this method may be null, in which case no action is taken.
*
* @see #allocateMemory
*/
public native void freeMemory(long address);
四、總結
4.1 tryHandlePendingReference 的調用場景
- 幽靈線程死循環調用 看下:【JAVA Reference】ReferenceQueue 與 Reference 源碼剖析(二)# Reference的static代碼塊
- 申請內存的時候,會調用(本文)
4.2 堆外緩存的特點
- 對垃圾回收停頓的改善可以明顯感覺到
- 對於大內存有良好的伸縮性
- 在進程間可以共享,減少虛擬機間的複製
- netty 就是使用堆外緩存,可以減少數據的複製操作,提高性能
4.3 使用堆外內存的原因
- 可以一定程度改善垃圾回收停頓的影響。full gc 意味着徹底回收,過大的堆會影響Java應用的性能。如果使用堆外內存的話,堆外內存是直接受操作系統管理( 而不是JVM )。
- 在某些場景下可以提升程序I/O操縱的性能。減少了將數據從堆內內存拷貝到堆外內存的步驟。
4.4 對外內存的使用場景
- 直接的文件拷貝操作,或者I/O操作。直接使用堆外內存就能少去內存從用戶內存拷貝到系統內存的操作,因爲I/O操作是系統內核內存和設備間的通信,而不是通過程序直接和外設通信的。
- 堆外內存適用於生命週期中等或較長的對象。( 如果是生命週期較短的對象,在YGC的時候就被回收了,就不存在大內存且生命週期較長的對象在FGC對應用造成的性能影響 )。
同時,還可以使用池+堆外內存 的組合方式,來對生命週期較短,但涉及到I/O操作的對象進行堆外內存的再使用。( Netty中就使用了該方式 )
五、番外篇
下一章節:【JAVA Reference】Finalizer 剖析 (六)
上一章節:【JAVA Reference】Cleaner 對比 finalize 對比 AutoCloseable(四)