官术网_书友最值得收藏!

第一章 Java虛擬機(jī)棧

你遇到過(guò)StackOverflowError嗎

StackOverflowError這個(gè)錯(cuò)誤常出現(xiàn)在較深的方法調(diào)用以及遞歸方法中,平時(shí)很少會(huì)遇到。我們以一道經(jīng)典的遞歸算法題為例,求1到n的和。為了查看在發(fā)生棧溢出時(shí)方法一共遞歸了多少次,我們?cè)诜椒ㄖ写蛴‘?dāng)前n的值。如代碼清單1-1所示。

代碼清單1-1 使用遞歸算法求1到n的和

  
public class RecursionAlgorithmMain { public static int sigma(int n) { System.out.println("current 'n' value is " + n); return n + sigma(n + 1); } public static void main(String[] args) throws IOException { new Thread(() -> sigma(1)).start(); System.in.read(); } }

在默認(rèn)棧大小情況下,程序拋出棧溢出錯(cuò)誤并終止線程時(shí),方法遞歸調(diào)用了5961次,如圖1.1所示。

圖1.1 使用默認(rèn)棧大小時(shí)的棧溢出

在默認(rèn)棧大小的情況下,多次運(yùn)行代碼清單1-1的代碼,得出的結(jié)果是相差不大的。在發(fā)生StackOverflowError時(shí),進(jìn)程并沒(méi)有結(jié)束,因?yàn)橐粋€(gè)線程的StackOverflowError并不影響整個(gè)進(jìn)程。

現(xiàn)在我們將配置JVM的啟動(dòng)參數(shù)-Xss,以調(diào)整虛擬機(jī)棧的大小為256k。如果你是使用idea運(yùn)行本例代碼,可直接在VM options配置加上-Xss256K。如果你是使用java命令運(yùn)行,可在java命令后面加上-Xss256k,啟動(dòng)命令如下。

java -Xss256k -jar RecursionAlgorithmMain.jar

在調(diào)整棧的大小后,運(yùn)行代碼清單1-1的代碼,sigma方法一共遞歸調(diào)用了1601次,如圖1.2所示。

圖1.2 棧大小為256K時(shí)的棧溢出

這與調(diào)整棧大小之前似乎存在著某種關(guān)系,用棧大小調(diào)整之前程序發(fā)生棧溢出時(shí)方法的調(diào)用次數(shù)除以棧大小調(diào)整后的,結(jié)果約是4。這是不是說(shuō)明棧的大小默認(rèn)為1024K呢。當(dāng)然,以這個(gè)測(cè)試結(jié)果來(lái)說(shuō)明其實(shí)并不嚴(yán)謹(jǐn)。

我們可以通過(guò)打印虛擬機(jī)參數(shù)查看默認(rèn)的棧大小。使用jinfo[1]命令行工具可查看某個(gè)Java進(jìn)程當(dāng)前虛擬機(jī)棧的大小,這是jdk提供的工具,不需要額外下載安裝。使用jinfo查看Java進(jìn)程線程棧大小如下:

wjy$ jinfo -flag ThreadStackSize 29643

其中29643為進(jìn)程ID。在不修改任何配置的情況下,在64位Linux系統(tǒng)上執(zhí)行jinfo命令,查詢出來(lái)的默認(rèn)棧大小為1M,如圖1.3所示。

圖1.3 jinfo查看默認(rèn)線程棧大小

棧大小除了可以使用jinfo命令行工具查看之外,我們還可以通過(guò)NAT工具[2]查看。使用NAT還能查看方法區(qū)的大小。以使用Java命令啟動(dòng)Java進(jìn)程為例,在Java命令后面加上開啟NAT的配置參數(shù)NativeMemoryTracking,如下:

java -XX:NativeMemoryTracking=summary -jar xxx.jar

進(jìn)程啟動(dòng)后,可通過(guò)jcmd[3]命令行工具查看該進(jìn)程的內(nèi)存占用信息,如圖1.4所示。

圖1.4 NAT打印內(nèi)存占用信息

從圖1.4中,我們能看到當(dāng)前進(jìn)程Java堆分配的大小、用于存儲(chǔ)類元數(shù)據(jù)信息使用的內(nèi)存、線程棧總共占用的內(nèi)存等。圖中有每個(gè)參數(shù)的詳細(xì)說(shuō)明,這里不再詳細(xì)說(shuō)明。從線程棧信息來(lái)看,被查看的進(jìn)程當(dāng)前線程數(shù)為63,使用內(nèi)存為63696K,也就是每個(gè)線程棧占用1M內(nèi)存。

NAT工具也用于排查內(nèi)存泄露問(wèn)題,當(dāng)項(xiàng)目中依賴了一些使用直接內(nèi)存的第三方j(luò)ar包時(shí),可能會(huì)因?yàn)槭褂貌划?dāng)而造成內(nèi)存泄露。如堆內(nèi)存沒(méi)有用滿,但top命令[4]查看內(nèi)存使用率卻接近百分百,這種情況就很有可能是程序使用堆外直接內(nèi)存造成的。

-Xss參數(shù)在多線程項(xiàng)目中常用于JVM調(diào)優(yōu)。假設(shè)項(xiàng)目中開啟1024個(gè)線程,那么使用默認(rèn)棧大小的情況下,虛擬機(jī)棧將會(huì)占用1G的內(nèi)存,而如果將棧大小調(diào)整為256K,虛擬機(jī)將只花費(fèi)256M內(nèi)存用于1024個(gè)棧的分配。

最后,我們也可以在HotSpot源碼中找到關(guān)于棧大小的設(shè)置[5]。以64位Linux操作系統(tǒng)為例,默認(rèn)棧大小為1M,編譯線程的棧大小為4M,如代碼清單1-2所示。

代碼清單1-2 獲取默認(rèn)棧大小的方法

 // return default stack size for thr_type
 size_t os::Linux::default_stack_size(os::ThreadType thr_type) {
      // default stack size (compiler thread needs larger stack)
     #ifdef AMD64
           size_t s = (thr_type == os::compiler_thread ? 4 * M : 1 * M);
     #else
           size_t s = (thr_type == os::compiler_thread ? 2 * M : 512 * K);
     #endif // AMD64
      return s;
}

棧也有最小值,在不同的操作系統(tǒng)及CPU環(huán)境下,棧的最小值也不一樣。如在64位的Linxu系統(tǒng)下,使用java命令啟動(dòng)一個(gè)jar包并將-Xss配置為128K,進(jìn)程將會(huì)異常終止,并提示創(chuàng)建Java虛擬機(jī)失敗,要求棧最小值為228K。如圖1.5所示。

圖1.5 設(shè)置棧大小為128k進(jìn)程啟動(dòng)失敗

虛擬機(jī)棧的最小值在虛擬機(jī)啟動(dòng)時(shí)解析完全局參數(shù)之后調(diào)用os::init_2方法設(shè)置。虛擬機(jī)棧的最小值受當(dāng)前系統(tǒng)是32位還是64位的影響,也受系統(tǒng)頁(yè)大小影響。在64位Linxu操作系統(tǒng)下,HotSopt所允許設(shè)置的棧的最小值為228K[6],如代碼清單1-3所示。

代碼清單1-3 設(shè)置棧的最小值

 // os::init_2(void)
 os::Linux::min_stack_allowed = MAX2(os::Linux::min_stack_allowed,
            (size_t)(StackYellowPages+StackRedPages+StackShadowPages) * Linux::page_size()   + (2*BytesPerWord COMPILER2_PRESENT(+1)) * Linux::vm_default_page_size()); 

注釋:

[1] jinfo是JDK提供的命令行工具,可以用來(lái)查看正在運(yùn)行的java應(yīng)用程序的擴(kuò)展參數(shù)。

[2] Native Memory Tracking是一個(gè)Java Hotspot VM新特性,用于跟蹤熱點(diǎn)VM的內(nèi)部?jī)?nèi)存使用情況。

[3] jcmd是JDK提供的命令行工具,使用jcmd可訪問(wèn)NMT數(shù)據(jù)。

[4] Linux操作系統(tǒng)提供的top命令可用于查看系統(tǒng)當(dāng)前每個(gè)進(jìn)程對(duì)內(nèi)存的占用率以及CPU的使用率等信息。

[5] 源碼在hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp文件中,方法名為default_stack_size。

[6] 源碼在hotspot/src/os/linux/vm/os_linux.cpp文件中

主站蜘蛛池模板: 浦城县| 宜城市| 兴山县| 黄浦区| 岳普湖县| 分宜县| 报价| 都昌县| 清丰县| 田阳县| 合水县| 会宁县| 文水县| 葫芦岛市| 台前县| 金山区| 女性| 祁连县| 山阳县| 长治市| 正宁县| 蓬莱市| 富锦市| 潜山县| 北京市| 屯昌县| 武汉市| 什邡市| 林西县| 永仁县| 青州市| 林芝县| 肥西县| 平利县| 永清县| 泰兴市| 疏附县| 东台市| 十堰市| 霍城县| 石嘴山市|