## 什么是堆外內存
### 堆內內存(on-heap memory)回顧
堆外內存和堆內內存是相對的兩個概念,其中堆內內存是我們平常工作中接觸比較多的,我們在jvm參數中只要使用-Xms,-Xmx等參數就可以設置堆的大小和最大值,理解jvm的堆還需要知道下面這個公式:
```
堆內內存 = 新生代+老年代+持久代
```
如下面的圖所示:

在使用堆內內存(on-heap memory)的時候,完全遵守JVM虛擬機的內存管理機制,采用垃圾回收器(GC)統一進行內存管理,GC會在某些特定的時間點進行一次徹底回收,也就是Full GC,GC會對所有分配的堆內內存進行掃描,在這個過程中會對JAVA應用程序的性能造成一定影響,還可能會產生Stop The World。
常見的垃圾回收算法主要有:
* 引用計數器法(Reference Counting)
* 標記清除法(Mark-Sweep)
* 復制算法(Coping)
* 標記壓縮法(Mark-Compact)
* 分代算法(Generational Collecting)
* 分區算法(Region)
### 堆外內存(off-heap memory)介紹
又叫做直接內存。
和堆內內存相對應,堆外內存就是把內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。
作為JAVA開發者我們經常用java.nio.DirectByteBuffer對象進行堆外內存的管理和使用,它會在對象創建的時候就分配堆外內存。
DirectByteBuffer類是在Java Heap外分配內存,對堆外內存的申請主要是通過成員變量unsafe來操作,下面介紹構造方法。
```
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
//內存是否按頁分配對齊
boolean pa = VM.isDirectMemoryPageAligned();
//獲取每頁內存大小
int ps = Bits.pageSize();
//分配內存的大小,如果是按頁對齊方式,需要再加一頁內存的容量
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//用Bits類保存總分配內存(按頁分配)的大小和實際內存的大小
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;
}
```
注:在Cleaner 內部中通過一個列表,維護了一個針對每一個 directBuffer 的一個回收堆外內存的 線程對象(Runnable),回收操作是發生在 Cleaner 的 clean() 方法中。
```
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);
}
}
```
## 概念和特征
* 直接內存并非 JVMS 定義的標準 Java 運行時內存。
* JDK1.4 加入了新的 NIO 機制,目的是防止 Java 堆 和 Native 堆之間往復的數據復制帶來的性能損耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配內存。
* 直接內存區域是全局共享的內存區域。
* 直接內存區域可以進行自動內存管理(GC),但機制并不完善。
* 本機的 Native 堆(直接內存) 不受 JVM 堆內存大小限制。
* 可能出現 OutOfMemoryError 異常。
*
## 使用堆外內存的優點
* 減少了垃圾回收
因為垃圾回收會暫停其他的工作。
* 加快了復制的速度
堆內在flush到遠程時,會先復制到直接內存(非堆內存),然后在發送;而堆外內存相當于省略掉了這個工作。
## 使用DirectByteBuffer的注意事項
同樣任何一個事物使用起來有優點就會有缺點,堆外內存的缺點就是內存難以控制,使用了堆外內存就間接失去了JVM管理內存的可行性,改由自己來管理,當發生內存溢出時排查起來非常困難。
java.nio.DirectByteBuffer對象在創建過程中會先通過Unsafe接口直接通過os::malloc來分配內存,然后將內存的起始地址和大小存到java.nio.DirectByteBuffer對象里,這樣就可以直接操作這些內存。這些內存只有在DirectByteBuffer回收掉之后才有機會被回收,因此如果這些對象大部分都移到了old,但是一直沒有觸發CMS GC或者Full GC,那么悲劇將會發生,因為你的物理內存被他們耗盡了,因此為了避免這種悲劇的發生,通過-XX:MaxDirectMemorySize來指定最大的堆外內存大小,當使用達到了閾值的時候將調用System.gc來做一次full gc,以此來回收掉沒有被使用的堆外內存。
## DirectByteBuffer使用測試
我們在寫NIO程序經常使用ByteBuffer來讀取或者寫入數據,那么使用ByteBuffer.allocate(capability)還是使用ByteBuffer.allocteDirect(capability)來分配緩存了?第一種方式是分配JVM堆內存,屬于GC管轄范圍,由于需要拷貝所以速度相對較慢;第二種方式是分配OS本地內存,不屬于GC管轄范圍,由于不需要內存拷貝所以速度相對較快。
代碼如下:
```
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
public class DirectByteBufferTest {
public static void main(String[] args) throws InterruptedException{
//分配128MB直接內存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}
}
```
### 測試用例1
設置JVM參數-Xmx100m,運行異常,因為如果沒設置-XX:MaxDirectMemorySize,則默認與-Xmx參數值相同,分配128M直接內存超出限制范圍。
```
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)
at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)
```
### 測試用例2
設置JVM參數-Xmx256m,運行正常,因為128M小于256M,屬于范圍內分配。
### 測試用例3
設置JVM參數-Xmx256m -XX:MaxDirectMemorySize=100M,運行異常,分配的直接內存128M超過限定的100M。
```
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)
at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)
```
### 測試用例4
設置JVM參數-Xmx768m,運行程序觀察內存使用變化,會發現clean()后內存馬上下降,說明使用clean()方法能有效及時回收直接緩存。
代碼如下:
```
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import sun.nio.ch.DirectBuffer;
public class DirectByteBufferTest {
public static void main(String[] args) throws InterruptedException{
//分配512MB直接緩存
ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*512);
TimeUnit.SECONDS.sleep(10);
//清除直接緩存
((DirectBuffer)bb).cleaner().clean();
TimeUnit.SECONDS.sleep(10);
System.out.println("ok");
}
}
```
### 異常演示
測試代碼:
```
public class TestNativeHeap {
/**
* VM Args: -XX:MaxDirectMemorySize=10M
*/
// 每次內存分配大小
private static int _1M =1024*1024;
public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
Field unsafeFiled = Unsafe.class.getFields()[0];
unsafeFiled.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeFiled.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
```
以上代碼運行會出現 OutOfMemoryError 異常,原因是不斷申請內存將超出 Native 堆限制。
直接內存導致的 OutOfMemoryError 異常,在異常信息中不會有明顯的堆棧區錯誤提示;同時另一大特點就是內存轉儲文件(dump)出來會非常小,如果項目中出現這種情況,并且直接或者間接地使用了 NIO 技術,那么應該考慮是否為直接內存導致的內存溢出。
## 細說System.gc方法
### JDK里的System.gc的實現
```
/**
* Runs the garbage collector.
* <p>
* Calling the <code>gc</code> method suggests that the Java Virtual
* Machine expend effort toward recycling unused objects in order to
* make the memory they currently occupy available for quick reuse.
* When control returns from the method call, the Java Virtual
* Machine has made a best effort to reclaim space from all discarded
* objects.
* <p>
* The call <code>System.gc()</code> is effectively equivalent to the
* call:
* <blockquote><pre>
* Runtime.getRuntime().gc()
* </pre></blockquote>
*
* @see java.lang.Runtime#gc()
*/
public static void gc() {
Runtime.getRuntime().gc();
}
```
其實發現System.gc方法其實是調用的Runtime.getRuntime.gc(),我們再接著看。
```
/*
運行垃圾收集器。
調用此方法表明,java虛擬機擴展
努力回收未使用的對象,以便內存可以快速復用,
當控制從方法調用返回的時候,虛擬機盡力回收被丟棄的對象
*/
public native void gc();
```
這里看到gc方法是native的,在java層面只能到此結束了,代碼只有這么多,要了解更多,可以看方法上面的注釋,不過我們需要更深層次地來了解其實現,那還是準備好進入到jvm里去看看。
### System.gc的作用有哪些
說起堆外內存免不了要提及System.gc方法,下面就是使用了System.gc的作用是什么?
* 做一次full gc
* 執行后會暫停整個進程。
* System.gc我們可以禁掉,使用-XX:+DisableExplicitGC,
* 其實一般在cms gc下我們通過-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一點的gc,也就是并行gc。
* 最常見的場景是RMI/NIO下的堆外內存分配等
注:
如果我們使用了堆外內存,并且用了DisableExplicitGC設置為true,那么就是禁止使用System.gc,這樣堆外內存將無從觸發極有可能造成內存溢出錯誤,在這種情況下可以考慮使用ExplicitGCInvokesConcurrent參數。
說起Full gc我們最先想到的就是stop thd world,這里要先提到VMThread,在jvm里有這么一個線程不斷輪詢它的隊列,這個隊列里主要是存一些VM_operation的動作,比如最常見的就是內存分配失敗要求做GC操作的請求等,在對gc這些操作執行的時候會先將其他業務線程都進入到安全點,也就是這些線程從此不再執行任何字節碼指令,只有當出了安全點的時候才讓他們繼續執行原來的指令,因此這其實就是我們說的stop the world(STW),整個進程相當于靜止了。
## 開源堆外緩存框架
關于堆外緩存的開源實現。查詢了一些資料后了解到的主要有:
* Ehcache 3.0:3.0基于其商業公司一個非開源的堆外組件的實現。
* Chronical Map:OpenHFT包括很多類庫,使用這些類庫很少產生垃圾,并且應用程序使用這些類庫后也很少發生Minor GC。類庫主要包括:Chronicle Map,Chronicle Queue等等。
* OHC:來源于Cassandra 3.0, Apache v2。
* Ignite: 一個規模宏大的內存計算框架,屬于Apache項目。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux