詳細總結Java堆棧內(nèi)存、堆外內(nèi)存、零拷貝淺析與代碼實現(xiàn)
一、堆棧內(nèi)存
堆棧內(nèi)存,顧名思義,指的是堆內(nèi)存以及棧內(nèi)存,其中,堆內(nèi)存是由Java GC進行管理的內(nèi)存區(qū)域,而棧內(nèi)存則是線程內(nèi)存。關于棧內(nèi)存,這里不去細說。以Hotspot為例,堆內(nèi)存的簡要結構如下圖所示:

而堆棧的關系,我們可以通過一行簡單的代碼來理解:
public static void main(String[] args) {
Object o = new Object();
}
上述代碼主要完成了兩件事,new Object( ) 在堆上開辟了一塊內(nèi)存,也就是說,new Object( )是分配在堆上的;而變量o,則是在線程main的棧上面的,它指向了new Object( ) 開辟的堆內(nèi)存地址。簡單來說,程序中創(chuàng)建的對象,都存儲在堆內(nèi)存中,棧內(nèi)存包含對它的引用。
二、堆外內(nèi)存
簡單來說,除了堆棧內(nèi)存,剩下的就都是堆外內(nèi)存了(當然,這是從Java運行時內(nèi)存的角度來看),堆外內(nèi)存直接受操作系統(tǒng)管理,而不是虛擬機。而使用堆外內(nèi)存的原因,主要有幾點:
- 一定程度上減少了GC,堆外內(nèi)存是直接受操作系統(tǒng)管理的,而不是JVM,因此使用堆外內(nèi)存的話,就可以保持一個比較小的堆內(nèi)內(nèi)存,減少垃圾回收對程序性能的影響。這一塊,在Kafka中就應用得很好,感興趣的同學可以去了解一下;
- 還有一個更大的優(yōu)點,就是提高IO操作的效率!這里就涉及用戶態(tài)與內(nèi)核態(tài),以及內(nèi)核緩沖區(qū)的概念,具體可以看筆者之前的一篇文章Java隨筆記 - 內(nèi)核緩沖區(qū)與進程緩沖區(qū)。其中,堆內(nèi)內(nèi)存其實就是用戶進程的進程緩沖區(qū),屬于用戶態(tài),而堆外內(nèi)存由操作系統(tǒng)管理,屬于內(nèi)核態(tài)。如果從堆內(nèi)向磁盤寫數(shù)據(jù),數(shù)據(jù)會被先復制到堆外內(nèi)存,即內(nèi)核緩沖區(qū),然后再由OS寫入磁盤,但使用堆外內(nèi)存的話則可以避免這個復制操作。

三、零拷貝
總結上述內(nèi)容中對堆棧內(nèi)存與堆外內(nèi)存的說明,主要解決了兩個疑問:“零拷貝”是從哪拷貝到哪?“零拷貝”是怎么優(yōu)化掉這一拷貝操作的?
- 用戶進程需要像磁盤寫數(shù)據(jù)時,需要將用戶緩沖區(qū)(堆內(nèi)內(nèi)存)中的內(nèi)容拷貝到內(nèi)核緩沖區(qū)(堆外內(nèi)存)中,操作系統(tǒng)再將內(nèi)核緩沖區(qū)中的內(nèi)容寫進磁盤中;
- 通過在用戶進程中,直接申請堆外內(nèi)存,存儲其需要寫進磁盤的數(shù)據(jù),就能夠省掉上述拷貝操作。
在Java中,提供了一些使用堆外內(nèi)存以及DMA的方法,能夠在很大程度上優(yōu)化用戶進程的IO效率。這里,給出一份拷貝文件的代碼,分別使用BIO、NIO和使用堆外內(nèi)存的NIO進行文件復制,簡單對比其耗時。
這里我使用一個200MB左右的pdf文件進行拷貝操作,你可以另外指定更大的文件,文件越大對比越明顯。這里我運行出來的延時,BIO的平均耗時1500ms上下,NIO耗時120ms左右, 使用堆外內(nèi)存的NIO耗時100ms上下。
package top.jiangnanmax.nio;
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author jiangnanmax
* @email jiangnanmax@gmail.com
* @description CopyCompare
* @date 2021/5/7
**/
public class CopyCompare {
public static void main(String[] args) throws Exception {
String inputFile = "/tmp/nio/input/HyperLedger.pdf";
String outputFile = "/tmp/nio/output/HyperLedger.pdf";
long start = System.currentTimeMillis();
nioCopyByDirectMem(inputFile, outputFile);
long end = System.currentTimeMillis();
System.out.println("cost time: " + (end - start) + " ms");
deleteFile(outputFile);
}
/**
* 使用傳統(tǒng)IO進行文件復制
*
* 平均耗時 15** ms
*
* @param sourcePath
* @param destPath
*/
private static void bioCopy(String sourcePath, String destPath) throws Exception {
File sourceFile = new File(sourcePath);
File destFile = new File(destPath);
if (!destFile.exists()) {
destFile.createNewFile();
}
FileInputStream inputStream = new FileInputStream(sourceFile);
FileOutputStream outputStream = new FileOutputStream(destFile);
byte[] buffer = new byte[512];
int lenRead;
while ((lenRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, lenRead);
}
inputStream.close();
outputStream.close();
}
/**
* 使用NIO進行文件復制,但不使用堆外內(nèi)存
*
* 平均耗時 1** ms, 比BIO直接快了一個數(shù)量級???
*
* @param sourcePath
* @param destPath
*/
private static void nioCopy(String sourcePath, String destPath) throws Exception {
File sourceFile = new File(sourcePath);
File destFile = new File(destPath);
if (!destFile.exists()) {
destFile.createNewFile();
}
FileInputStream inputStream = new FileInputStream(sourceFile);
FileOutputStream outputStream = new FileOutputStream(destFile);
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
// transferFrom底層調(diào)用的應該是sendfile
// 直接在兩個文件描述符之間進行了數(shù)據(jù)傳輸
// DMA
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
inputChannel.close();
outputChannel.close();
inputStream.close();
outputStream.close();
}
/**
* 使用NIO進行文件復制,并使用堆外內(nèi)存
*
* 平均耗時100ms上下,比沒使用堆外內(nèi)存的NIO快一點
*
* @param sourcePath
* @param destPath
*/
private static void nioCopyByDirectMem(String sourcePath, String destPath) throws Exception {
File sourceFile = new File(sourcePath);
File destFile = new File(destPath);
if (!destFile.exists()) {
destFile.createNewFile();
}
FileInputStream inputStream = new FileInputStream(sourceFile);
FileOutputStream outputStream = new FileOutputStream(destFile);
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
MappedByteBuffer buffer = inputChannel.map(FileChannel.MapMode.READ_ONLY, 0, inputChannel.size());
outputChannel.write(buffer);
inputChannel.close();
outputChannel.close();
inputStream.close();
outputStream.close();
}
/**
* 刪除目標文件
*
* @param target
*/
private static void deleteFile(String target) {
File file = new File(target);
file.delete();
}
}
到此這篇關于詳細總結Java堆棧內(nèi)存、堆外內(nèi)存、零拷貝淺析與代碼實現(xiàn)的文章就介紹到這了,更多相關Java堆棧內(nèi)存 堆外內(nèi)存 零拷貝淺析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring Boot中整合PageHelper實現(xiàn)分頁功能詳細步驟
在Spring Boot項目中整合PageHelper并實現(xiàn)分頁查詢功能的全部步驟,通過以上配置和代碼,我們可以輕松地實現(xiàn)數(shù)據(jù)庫分頁查詢,提高了開發(fā)效率并改善了用戶體驗,感興趣的朋友跟隨小編一起看看吧2024-05-05
Java中IO流的BufferedOutputStream和FileOutputStream對比
這篇文章主要介紹了Java中IO流的BufferedOutputStream和FileOutputStream對比,不帶緩沖的操作,每讀一個字節(jié)就要寫入一個字節(jié),由于涉及磁盤的IO操作相比內(nèi)存的操作要慢很多,所以在讀寫的字節(jié)比較少的情況下,效率比較低,需要的朋友可以參考下2023-07-07
解析Spring Boot內(nèi)嵌tomcat關于getServletContext().getRealPath獲取得到臨時
大家都很糾結這個問題在使用getServletContext().getRealPath()得到的是臨時文件的路徑,每次重啟服務,這個臨時文件的路徑還好變更,下面小編通過本文給大家分享Spring Boot內(nèi)嵌tomcat關于getServletContext().getRealPath獲取得到臨時路徑的問題,一起看看吧2021-05-05
java 中如何獲取字節(jié)碼文件的相關內(nèi)容
這篇文章主要介紹了java 中如何獲取字節(jié)碼文件的相關內(nèi)容的相關資料,需要的朋友可以參考下2017-04-04
java客戶端Etcd官方倉庫jetcd中KeepAlive接口實現(xiàn)
這篇文章主要為大家介紹了java客戶端Etcd官方倉庫jetcd中KeepAlive接口實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,多多加薪2022-02-02
SpringBoot?spring.factories加載時機分析
這篇文章主要為大家介紹了SpringBoot?spring.factories加載時機分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03

