JVM說--直接記憶體的使用
作者:京東物流 劉作龍
前言:
學習底層原理有的時候不一定你是要用到他,而是學習他的設計思想和思路。再或者,當你在日常工作中遇到棘手的問題時候,可以多一條解決問題的方式
分享大綱:
本次分享主要由io與nio讀取檔案速度差異的情況,去了解nio為什麼讀取大檔案的時候效率較高,檢視nio是如何使用直接記憶體的,再深入到如何使用直接記憶體
1 nio與io讀寫檔案的效率比對
首先上程式碼,有興趣的同學可以將程式碼拿下來進行除錯檢視
package com.lzl.netty.study.jvm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* java對於直接記憶體使用的測試類
*
* @author liuzuolong
* @date 2022/6/29
**/
@Slf4j
public class DirectBufferTest {
private static final int SIZE_10MB = 10 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
//讀取和寫入不同的檔案,保證互不影響
String filePath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioInputFile.zip";
String filePath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectInputFile.zip";
String filePath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapInputFile.zip";
String toPath1 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/ioOutputFile.zip";
String toPath2 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioDirectOutputFile.zip";
String toPath3 = "/Users/liuzuolong/CODE/OWN/netty-study/src/main/resources/nioHeapOutputFile.zip";
Integer fileByteLength = SIZE_10MB;
//新建io讀取檔案的執行緒
Thread commonIo = new Thread(() -> {
commonIo(filePath1, fileByteLength, toPath1);
});
//新建nio使用直接記憶體讀取檔案的執行緒
Thread nioWithDirectBuffer = new Thread(() -> {
nioWithDirectBuffer(filePath2, fileByteLength, toPath2);
});
//新建nio使用堆記憶體讀取檔案的執行緒
Thread nioWithHeapBuffer = new Thread(() -> {
nioWithHeapBuffer(filePath3, fileByteLength, toPath3);
});
nioWithDirectBuffer.start();
commonIo.start();
nioWithHeapBuffer.start();
}
public static void commonIo(String filePath, Integer byteLength, String toPath) {
//進行時間監控
StopWatch ioTimeWatch = new StopWatch();
ioTimeWatch.start("ioTimeWatch");
try (FileInputStream fis = new FileInputStream(filePath);
FileOutputStream fos = new FileOutputStream(toPath);
) {
byte[] readByte = new byte[byteLength];
int readCount = 0;
while ((readCount = fis.read(readByte)) != -1) {
// 讀取了多少個位元組,轉換多少個。
fos.write(readByte, 0, readCount);
}
} catch (Exception e) {
e.printStackTrace();
}
ioTimeWatch.stop();
log.info(ioTimeWatch.prettyPrint());
}
public static void nioWithDirectBuffer(String filePath, Integer byteLength, String toPath) {
StopWatch nioTimeWatch = new StopWatch();
nioTimeWatch.start("nioDirectTimeWatch");
try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel();
) {
// 讀寫的緩衝區(分配一塊兒直接記憶體)
//要與allocate進行區分
//進入到函式中
ByteBuffer bb = ByteBuffer.allocateDirect(byteLength);
while (true) {
int len = fci.read(bb);
if (len == -1) {
break;
}
bb.flip();
fco.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
nioTimeWatch.stop();
log.info(nioTimeWatch.prettyPrint());
}
public static void nioWithHeapBuffer(String filePath, Integer byteLength, String toPath) {
StopWatch nioTimeWatch = new StopWatch();
nioTimeWatch.start("nioHeapTimeWatch");
try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel();
) {
// 讀寫的緩衝區(分配一塊兒直接記憶體)
//要與allocate進行區分
ByteBuffer bb = ByteBuffer.allocate(byteLength);
while (true) {
int len = fci.read(bb);
if (len == -1) {
break;
}
bb.flip();
fco.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
nioTimeWatch.stop();
log.info(nioTimeWatch.prettyPrint());
}
}
1.主函式呼叫
為排除當前環境不同導致的檔案讀寫效率不同問題,使用多執行緒分別呼叫io方法和nio方法
2.分別進行IO呼叫和NIO呼叫
通過nio和io的讀取寫入檔案方式進行操作
3.結果
經過多次測試後,發現nio讀取檔案的效率是高於io的,尤其是讀取大檔案的時候
11:12:26.606 [Thread-1] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 1157-----------------------------------------ms % Task name-----------------------------------------01157 100% nioDirectTimeWatch11:12:27.146 [Thread-0] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 1704-----------------------------------------ms % Task name-----------------------------------------01704 100% ioTimeWatch
4 提出疑問
那到底為什麼nio的速度要快於普通的io呢,結合原始碼檢視以及網上的資料,核心原因是:
nio讀取檔案的時候,使用直接記憶體進行讀取,那麼,如果在nio中也不使用直接記憶體的話,會是什麼情況呢?
5.再次驗證
新增使用堆記憶體讀取檔案
執行時間驗證如下:
11:30:35.050 [Thread-1] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 2653-----------------------------------------ms % Task name-----------------------------------------02653 100% nioDirectTimeWatch11:30:35.399 [Thread-2] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 3038-----------------------------------------ms % Task name-----------------------------------------03038 100% nioHeapTimeWatch11:30:35.457 [Thread-0] INFO com.lzl.netty.study.jvm.DirectBufferTest - StopWatch '': running time (millis) = 3096-----------------------------------------ms % Task name-----------------------------------------03096 100% ioTimeWatch
根據上述的實際驗證,nio讀寫檔案比較快的主要原因還是在於使用了直接記憶體,那麼為什麼會出現這種情況呢?
2 直接記憶體的讀寫效能強的原理
直接上圖說明
1.堆記憶體讀寫檔案
堆記憶體讀寫檔案的步驟:
當JVM想要去和磁碟進行互動的時候,因為JVM和作業系統之間存在讀寫屏障,所以在進行資料互動的時候需要進行頻繁的複製
- 先由作業系統進行磁碟的讀取,將讀取資料放入系統記憶體緩衝區中
- JVM與系統記憶體緩衝區進行資料拷貝
- 應用程式再到JVM的堆記憶體空間中進行資料的獲取
2.直接記憶體讀寫檔案
直接記憶體讀寫檔案的步驟
如果使用直接記憶體進行檔案讀取的時候,步驟如下
- 會直接呼叫native方法allocateMemory進行直接記憶體的分配
- 作業系統將檔案讀取到這部分的直接記憶體中
- 應用程式可以通過JVM堆空間的DirectByteBuffer進行讀取
與使用對堆記憶體讀寫檔案的步驟相比減少了資料拷貝的過程,避免了不必要的效能開銷,因此NIO中使用了直接記憶體,對於效能提升很多
那麼,直接記憶體的使用方式是什麼樣的呢?
3 nio使用直接記憶體的原始碼解讀
在閱讀原始碼之前呢,我們首先對於兩個知識進行補充
1.虛引用Cleaner sun.misc.Cleaner
什麼是虛引用
虛引用所引用的物件,永遠不會被回收,除非指向這個物件的所有虛引用都呼叫了clean函式,或者所有這些虛引用都不可達
-
必須關聯一個引用佇列
-
Cleaner繼承自虛引用PhantomReference,關聯引用佇列ReferenceQueue
概述的說一下,他的作用就是,JVM會將其對應的Cleaner加入到pending-Reference連結串列中,同時通知ReferenceHandler執行緒處理,ReferenceHandler收到通知後,會呼叫Cleaner#clean方法
2.Unsafesun misc.Unsafe
位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統記憶體資源、自主管理記憶體資源等,這些方法在提升Java執行效率、增強Java語言底層資源操作能力方面起到了很大的作用。3.直接記憶體是如何進行申請的 java.nio.DirectByteBuffer
進入到DirectBuffer中進行檢視
原始碼解讀
PS:只需要讀核心的劃紅框的位置的原始碼,其他內容按個人興趣閱讀- 直接呼叫ByteBuffer.allocateDirect方法
- 宣告一個一個DirectByteBuffer物件
- 在DirectByteBuffer的構造方法中主要進行三個步驟
步驟1:呼叫Unsafe的native方法allocateMemory進行快取空間的申請,獲取到的base為記憶體的地址
步驟2:設定記憶體空間需要和步驟1聯合進行使用
步驟3:使用虛引用Cleaner型別,建立一個快取的釋放的虛引用
直接快取是如何釋放的
我們前面說的了Cleaner的使用方式,那麼cleaner在直接記憶體的釋放中的流程是什麼樣的呢?3.1 新建虛引用
java.nio.DirectByteBuffer
步驟如下
- 呼叫Cleaner.create()方法
- 將當前新建的Cleaner加入到連結串列中
3.2 宣告清理快取任務
檢視java.nio.DirectByteBuffer.Deallocator的方法
- 實現了Runnable介面
- run方法中呼叫了unsafe的native方法freeMemory()進行記憶體的釋放
3.3 ReferenceHandler進行呼叫
首先進入:java.lang.ref.Reference.ReferenceHandler
當前執行緒優先順序最高,呼叫方法tryHandlePending
進入方法中,會呼叫c.clean c—>(Cleaner)
clean方法為Cleaner中宣告的Runnable,呼叫其run()方法
Cleaner中的宣告:private final Runnable thunk;回到《宣告清理快取任務》這一節,檢視Deallocator,使用unsafe的native方法freeMemory進行快取的釋放
4 直接記憶體的使用方式
直接記憶體特性
- nio中比較經常使用,用於資料緩衝區ByteBuffer
- 因為其不受JVM的垃圾回收管理,故分配和回收的成本較高
- 使用直接記憶體的讀寫效能非常高
直接記憶體是否會記憶體溢位
直接記憶體是跟系統記憶體相關的,如果不做控制的話,走的是當前系統的記憶體,當然JVM中也可以對其使用的大小進行控制,設定JVM引數-XX:MaxDirectMemorySize=5M,再執行的時候就會出現記憶體溢位直接記憶體是否會被JVM的GC影響
如果在直接記憶體宣告的下面呼叫System.gc();因為會觸發一次FullGC,則物件會被回收,則ReferenceHandler中的會被呼叫,直接記憶體會被釋放。我想使用直接記憶體,怎麼辦
如果你很想使用直接記憶體,又想讓直接記憶體儘快的釋放,是不是我直接呼叫System.gc();就行?
答案是不行的- 首先呼叫System.gc();會觸發FullGC,造成stop the world,影響系統性能
- 系統怕有初級研發顯式呼叫System.gc();會配置JVM引數:-XX:+DisableExplicitGC,禁止顯式呼叫
如果還想呼叫的話,自己使用Unsafe進行操作,以下為示例程式碼
PS:僅為建議,如果沒有對於Unsafe有很高的理解,請勿嘗試package com.lzl.netty.study.jvm;import sun.misc.Unsafe;import java.lang.reflect.Field;/** * 使用Unsafe物件操作直接記憶體 * * @author liuzuolong * @date 2022/7/1 **/public class UnsafeOperateDirectMemory { private static final int SIZE_100MB = 100 * 1024 * 1024; public static void main(String[] args) { Unsafe unsafe = getUnsafePersonal(); long base = unsafe.allocateMemory(SIZE_100MB); unsafe.setMemory(base, SIZE_100MB, (byte) 0); unsafe.freeMemory(base); } /** * 因為Unsafe為底層物件,所以正式是無法獲取的,但是反射是萬能的,可以通過反射進行獲取 * Unsafe自帶的方法getUnsafe 是不能使用的,會拋異常SecurityException * 獲取 Unsafe物件 * * @return unsafe物件 * @see sun.misc.Unsafe#getUnsafe() */ public static Unsafe getUnsafePersonal() { Field f; Unsafe unsafe; try { f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null); } catch (Exception e) { throw new RuntimeException("initial the unsafe failure..."); } return unsafe; }}
5 總結
JVM相關知識是中高階研發人員必備的知識,學習他的一些執行原理,對我們的日常工作會有很大的幫助
- 應用健康度隱患刨析解決系列之資料庫時區設定
- 對於Vue3和Ts的心得和思考
- 一文詳解擴散模型:DDPM
- zookeeper的Leader選舉原始碼解析
- 一文帶你搞懂如何優化慢SQL
- 京東金融Android瘦身探索與實踐
- 微前端框架single-spa子應用載入解析
- cookie時效無限延長方案
- 聊聊前端效能指標那些事兒
- Spring竟然可以建立“重複”名稱的bean?—一次專案中存在多個bean名稱重複問題的排查
- 京東金融Android瘦身探索與實踐
- Spring原始碼核心剖析
- 深入淺出RPC服務 | 不同層的網路協議
- 安全測試之探索windows遊戲掃雷
- 關於資料庫分庫分表的一點想法
- 對於Vue3和Ts的心得和思考
- Bitmap、RoaringBitmap原理分析
- 京東小程式CI工具實踐
- 測試用例設計指南
- 當你對 redis 說你中意的女孩是 Mia