// DirectMemory.java
package com.infuq.memory;
import org.jctools.util.UnsafeAccess;
import sun.misc.Unsafe;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import java.util.Scanner;
public class DirectMemory {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
long _30M = 30 * 1024 * 1024;
long direct = 0;
Unsafe unsafe = UnsafeAccess.UNSAFE;
while (scanner.hasNext()) {
String input = scanner.next();
if (input.equals("1")) {
System.out.println("malloc...");
// 向操作系統(tǒng)申請(qǐng)內(nèi)存,底層調(diào)用glibc庫(kù)的malloc庫(kù)函數(shù)
direct = unsafe.allocateMemory(_30M);
}
else if (input.equals("2")) {
System.out.println("init...");
byte b = 6;
// 使用上一步向操作系統(tǒng)申請(qǐng)的內(nèi)存
unsafe.setMemory(direct, _30M, b);
}
}
}
}
上面這個(gè)程序的功能, 執(zhí)行之后, 等待用戶輸入, 如果輸入1,那么程序會(huì)向操作系統(tǒng)申請(qǐng)30M的內(nèi)存, 如果輸入2, 那么程序會(huì)初始化申請(qǐng)的30M內(nèi)存.
這里說(shuō)的初始化的言外之意是模擬程序使用向操作系統(tǒng)申請(qǐng)的內(nèi)存
程序運(yùn)行之后, 我們會(huì)通過(guò)使用JDK自帶的jconsole(或jvisualvm)工具查看進(jìn)程內(nèi)存情況, 使用top,ps等命令查看進(jìn)程內(nèi)存情況, 使用JDK自帶的jcmd命令查看進(jìn)程內(nèi)存情況, 使用pmap命令查看進(jìn)程內(nèi)存情況, 使用阿里云的arms查看進(jìn)程的內(nèi)存情況, 使用smem工具查看進(jìn)程的內(nèi)存情況. 從多維度查看內(nèi)存情況 .
本次實(shí)驗(yàn)的環(huán)境: JDK1.8 Win10下的WSL2的Ubuntu20
還會(huì)使用2個(gè)三方包: jctools-core-2.1.2.jar jol-core-0.9.jar
文件結(jié)構(gòu)如下圖
run.py中的內(nèi)容如下
其實(shí)就是調(diào)用了 javac 和 java 命令而已,
設(shè)置堆空間50M, -XX:MaxMetaspaceSize=16M
訪問(wèn) https://www.selenic.com/smem/download/ 下載一個(gè)smem工具, 可以用于查看進(jìn)程的內(nèi)存
解壓下載的 smem-1.4.tar.gz
最后我們的目錄結(jié)構(gòu)如下
運(yùn)行程序
運(yùn)行之后, 程序阻塞, 等待用戶的輸入
使用 jps 查看進(jìn)程的PID = 15933
我們先使用 smem 工具查看下內(nèi)存, 如下圖
./smem -t -k
以上輸出當(dāng)前系統(tǒng)所有進(jìn)程的內(nèi)存情況, 由于我實(shí)驗(yàn)使用的是Win10的WSL系統(tǒng), 所以系統(tǒng)里的進(jìn)程很少. 能夠看出進(jìn)程15933使用的內(nèi)存, USS=27.8M, PSS=28M, RSS=30.3M
USS,PSS,RSS都是表示進(jìn)程實(shí)際使用的內(nèi)存. 更多關(guān)于USS,PSS,RSS關(guān)系和區(qū)別, 讀者自行了解.
我們經(jīng)常聽(tīng)到RSS/RES, 在使用top和ps命令的時(shí)候會(huì)看到, 如下圖
如上圖, 使用 top 和 ps 查看進(jìn)程15933的RSS/RES = 54552KB, 即53.27M, 約等于使用 smem 工具查看的RSS=54.2M內(nèi)存.
RSS是常駐于內(nèi)存的內(nèi)存, RSS中還會(huì)包含與其他進(jìn)程一起共享的內(nèi)存.
我們使用如下shell命令可以每隔2秒打印進(jìn)程15933的實(shí)際使用的內(nèi)存情況
i=0;while true; do echo $((i++)) $(./smem -t -k | tail -3 | head -1); sleep 2; done
我們還會(huì)使用如下shell命令每隔2秒打印進(jìn)程15933的committed內(nèi)存
i=0;while true; do echo $((i++)) $(pmap -d 15933 | tail -1); sleep 2; done
關(guān)于reserved(預(yù)留內(nèi)存), committed(提交內(nèi)存), used(已使用內(nèi)存)的關(guān)系如下圖, 更詳細(xì)內(nèi)容讀者自行了解
比如我們向操作系統(tǒng)申請(qǐng)30M的內(nèi)存, 則committed=30M. 但是操作系統(tǒng)并不會(huì)馬上將真實(shí)的30M內(nèi)存全部分配給進(jìn)程, 只會(huì)先分配一小部分真實(shí)內(nèi)存給進(jìn)程使用, 當(dāng)再次需要真實(shí)內(nèi)存的時(shí)候再次分配. 因此一個(gè)進(jìn)程的committed內(nèi)存一定大于等于used的內(nèi)存.
好了, 我們把上面兩個(gè)shell命令運(yùn)行起來(lái)
然后我們捕捉某一時(shí)刻的內(nèi)存情況如下圖
進(jìn)程15933當(dāng)前時(shí)刻實(shí)際使用的內(nèi)存54.2M, 虛擬內(nèi)存1641572K=1603M, committed內(nèi)存114552K=111.86M
接下來(lái)輸入1,那么我們的程序會(huì)向操作系統(tǒng)申請(qǐng)30M的內(nèi)存
如上圖, 我們向操作系統(tǒng)申請(qǐng)了30M的內(nèi)存, 而進(jìn)程的已使用內(nèi)存并沒(méi)有變化, 但是進(jìn)程commited內(nèi)存從114552K->145276K, 相差30724K=30M.
我們繼續(xù)再輸入1, 結(jié)果如下圖
如上圖, 繼續(xù)向操作系統(tǒng)申請(qǐng)30M內(nèi)存, 進(jìn)程已使用的內(nèi)存也沒(méi)有變化, 而進(jìn)程committed內(nèi)存又從145276增長(zhǎng)到176000K, 又相差了30M.
我們不做任何操作, 時(shí)間過(guò)去了一會(huì)...
進(jìn)程的內(nèi)存如下圖所示
在這一段時(shí)間我們并沒(méi)有任何操作, 內(nèi)存有了一些小變化, 這很正常, 畢竟JVM進(jìn)程里面還有一些JVM自身的線程也要隨著程序的運(yùn)行需要申請(qǐng)一些內(nèi)存, 后面我們使用 jconsole 連接到進(jìn)程, 內(nèi)存也會(huì)發(fā)生一些增長(zhǎng), 這都是正常情況.
我們使用JDK自帶的 jcmd 命令查看內(nèi)存
committed=173226KB 與上圖使用pmap顯示的176000KB有一些差. 畢竟它們是兩個(gè)不同的命令, 統(tǒng)計(jì)的角度不一樣.
pmap 命令統(tǒng)計(jì)的會(huì)比 jcmd統(tǒng)計(jì)的更準(zhǔn)確. 查看man手冊(cè), pmap統(tǒng)計(jì)的是進(jìn)程自身的smaps文件
接下來(lái)
如上圖, 重點(diǎn)需要關(guān)注Heap和Internal內(nèi)存的情況
我們使用JDK自帶的 jconsole 工具查看內(nèi)存
上圖查看的是堆空間的內(nèi)存情況, committed=49152KB, 與使用 jcmd 命令查看的51200KB有一些差, 可以忽略, 畢竟是2個(gè)不同的工具統(tǒng)計(jì)的. 上圖同時(shí)也說(shuō)明了, 雖然向操作系統(tǒng)申請(qǐng)了50M的堆空間, 但是目前實(shí)際使用了Used=10578KB, 此時(shí)操作系統(tǒng)也只是把部分真實(shí)內(nèi)存分配給進(jìn)程, 只有隨著進(jìn)程的運(yùn)行需要的內(nèi)存越多, 操作系統(tǒng)才會(huì)分配更多的真實(shí)內(nèi)存給進(jìn)程, 當(dāng)分配的真實(shí)內(nèi)存一旦超過(guò)committed時(shí), 也就會(huì)報(bào)OOM了.
我們?cè)俅尾蹲侥骋粫r(shí)刻的內(nèi)存情況
接下來(lái)我們輸入2, 我們寫(xiě)的程序就會(huì)使用申請(qǐng)到的內(nèi)存
如上圖, 當(dāng)我們真正使用內(nèi)存的時(shí)候, committed(374664KB)內(nèi)存沒(méi)有發(fā)生變化, 而使用內(nèi)存發(fā)生了變化, 增大了30M, 和我們之前申請(qǐng)的30M是一致的.
這個(gè)時(shí)候我們看一下通過(guò) jconsole 統(tǒng)計(jì)的非堆內(nèi)存的情況
我們繼續(xù)輸入1, 再申請(qǐng)30M內(nèi)存, 再輸入2, 使用申請(qǐng)的內(nèi)存,
看一下內(nèi)存的變化
和之前的實(shí)驗(yàn)一樣, 當(dāng)輸入1申請(qǐng)內(nèi)存時(shí), committed內(nèi)存發(fā)生了變化, 已經(jīng)使用內(nèi)存沒(méi)有發(fā)生變化
輸入2之后
committed內(nèi)存沒(méi)有發(fā)生變化, 已使用內(nèi)存增長(zhǎng)了30M
而且我們?cè)俅慰匆幌路嵌褍?nèi)存, 與之前的統(tǒng)計(jì)幾乎一樣, 沒(méi)變化.
我們所說(shuō)的非堆內(nèi)存包括Metaspace, CodeCache, CCS, 使用ByteBuffer.allocateDirect(),使用unsafe.allocateMemory(), 使用FileChannel.map(), 使用FileChannel.transferTo()等申請(qǐng)的內(nèi)存.其中重點(diǎn)要說(shuō)的是ByteBuffer.allocateDirect()和unsafe.allocateMemory().雖然都是申請(qǐng)的直接內(nèi)存, 也就是操作系統(tǒng)本地內(nèi)存, 但是 jconsole 只能統(tǒng)計(jì)到使用ByteBuffer.allocateDirect()申請(qǐng)的直接內(nèi)存, 它是無(wú)法統(tǒng)計(jì)到使用unsafe.allocateMemory()申請(qǐng)的直接內(nèi)存. 我們使用的-XX:MaxMetaspaceSize也是控制ByteBuffer.allocateDirect()申請(qǐng)的直接內(nèi)存大小, 無(wú)法控制unsafe.allocateMemory()申請(qǐng)的直接內(nèi)存大小.比如我們使用的Dubbo, RocketMQ等底層網(wǎng)絡(luò)通信都是使用Netty, Netty就是通過(guò)unsafe.allocateMemory()向操作系統(tǒng)申請(qǐng)內(nèi)存并自己管理這塊內(nèi)存, Netty也會(huì)自己管理向操作系統(tǒng)申請(qǐng)內(nèi)存的空間大小, 畢竟不能無(wú)限制向操作系統(tǒng)申請(qǐng)內(nèi)存.
FileChannel.map() 和 FileChannel.transferTo() 涉及到零拷貝知識(shí), 讀者朋友可以去了解下, 在我的 https://www.yuque.com/infuq/others/miqbcc 文章也有記錄
如果讀者朋友所在公司的服務(wù)器部署在阿里云上, 通過(guò)阿里云的arms監(jiān)控平臺(tái)查看服務(wù)器的內(nèi)存情況
上圖右下角的直接緩沖區(qū)與 jconsole 統(tǒng)計(jì)的直接內(nèi)存一樣, 它們都無(wú)法統(tǒng)計(jì)到使用unsafe.allocateMemory()申請(qǐng)的內(nèi)存.
如果要查看堆內(nèi)存的使用情況, 可以使用 jconsole 或者 arms 查看堆內(nèi)存的情況, 它們的統(tǒng)計(jì)沒(méi)問(wèn)題.
如果要查看直接內(nèi)存的情況, 或者查看進(jìn)程的內(nèi)存情況, 僅僅使用 jconsole 或者 arms 是不完全的, 看到的內(nèi)存是比實(shí)際要少的.
上圖并非此次實(shí)驗(yàn)程序的內(nèi)存統(tǒng)計(jì), 我是從線上找的一個(gè)服務(wù)器
接下來(lái)
當(dāng)我一直輸入1, 也就是一直向操作系統(tǒng)申請(qǐng)內(nèi)存, 只能表明進(jìn)程的committed內(nèi)存一直在增長(zhǎng)
而且我的宿主機(jī)Win10的內(nèi)存也不會(huì)隨著committed內(nèi)存增長(zhǎng)而增長(zhǎng)
接下來(lái)我們輸入2, 讓進(jìn)程使用申請(qǐng)到的內(nèi)存
進(jìn)程已使用的內(nèi)存到了1.5G
宿主機(jī)的內(nèi)存也從之前的6.6增長(zhǎng)到了7.1G, 進(jìn)程已使用內(nèi)存也從828M增長(zhǎng)到了1.5G, 兩者增長(zhǎng)量基本吻合的.
【總結(jié)1】
通過(guò)實(shí)驗(yàn), 零零散散介紹了如何查看進(jìn)程的內(nèi)存, 包括committed內(nèi)存, 已使用內(nèi)存等. 進(jìn)程使用unsafe.allocateMemory()申請(qǐng)內(nèi)存只是屬于committed內(nèi)存, 只有在進(jìn)程真正使用這塊內(nèi)存的時(shí)候, 操作系統(tǒng)才會(huì)一部分一部分的將真實(shí)的內(nèi)存分配給進(jìn)程使用. 通過(guò)實(shí)驗(yàn)也能知道, 使用unsafe.allocateMemory()方式申請(qǐng)內(nèi)存是不受-XX:MaxMetaspaceSize參數(shù)控制的, 實(shí)驗(yàn)中設(shè)置-XX:MaxMetaspaceSize=16M , 但是我們程序已經(jīng)申請(qǐng)使用了好幾百M(fèi)的內(nèi)存.
【總結(jié)2】
1.如果要查看進(jìn)程的committed內(nèi)存, 使用pmap -d <進(jìn)程ID>查看
2.如果要查看進(jìn)程已使用的內(nèi)存(USS,PSS,RSS), 使用smem工具查看, 使用top和ps命令也可以查看到RSS值, 但這個(gè)RSS值包含共享的內(nèi)存, 因此我們也要關(guān)注PSS,USS
3.如果要查看JVM的直接內(nèi)存, 可以使用 jcmd <進(jìn)程ID> VM.native_memory scale=KB
4.當(dāng)使用unsafe.allocateMemory(30M)申請(qǐng)內(nèi)存的時(shí)候, committed內(nèi)存會(huì)增長(zhǎng)30M, 但是已使用內(nèi)存不會(huì)增長(zhǎng)
5.當(dāng)程序使用unsafe.allocateMemory(30M)申請(qǐng)到的內(nèi)存時(shí), 已使用內(nèi)存會(huì)增長(zhǎng)
6.jconsole , jvisualvm, 阿里云arms 是監(jiān)控不到unsafe.allocateMemory()方式申請(qǐng)的內(nèi)存
關(guān)于如何監(jiān)控遠(yuǎn)程Java進(jìn)程可以查看我的這篇語(yǔ)雀文章
https://www.yuque.com/infuq/default/wwmdfk#rJSoP
關(guān)于JVM內(nèi)存的布局圖可以在下面這篇語(yǔ)雀文章中查找到
https://www.yuque.com/infuq/default/bzu9ef
再貼一張圖