Java內(nèi)存結(jié)構(gòu)
- 1.JVM概述
- 2.程序計(jì)數(shù)器
-
- 2.1.定義
- 2.2.作用及特點(diǎn)解釋
- 3.虛擬機(jī)棧
-
- 3.1.棧的特點(diǎn)
- 3.2.棧的演示
- 3.3.棧的問(wèn)題辨析
- 3.4.棧的線(xiàn)程安全問(wèn)題
- 3.5.棧內(nèi)存溢出(StackOverflowError)
- 3.6.線(xiàn)程運(yùn)行診斷
-
- 3.6.1.案例1:cpu占用過(guò)多(linux系統(tǒng)為例)
- 3.6.2.案例2:線(xiàn)程診斷_遲遲得不到結(jié)果
- 4.本地方法棧
- 5.堆
-
- 5.1.定義
- 5.2.堆內(nèi)存溢出(OutOfMemoryError:Java heap space)
- 5.3.堆內(nèi)存診斷
- 6.方法區(qū)
-
- 6.1.定義
- 6.2.定義
- 6.3.方法區(qū)內(nèi)存溢出(OutOfMemoryError: Metaspace)
- 6.4.常量池
1.JVM概述
定義:
JVM全稱(chēng)是Java Virtual Machine-java程序的運(yùn)行環(huán)境(java二進(jìn)制字節(jié)碼的運(yùn)行環(huán)境)
好處:
- 一次編寫(xiě),到處運(yùn)行(跨平臺(tái))
- 自動(dòng)內(nèi)存管理,垃圾回收功能
- 數(shù)組下標(biāo)越界檢查
- 多態(tài)
比較JVM,JRE,JDK之間的聯(lián)系和區(qū)別,我們可以用一張圖來(lái)解釋
JVM體系結(jié)構(gòu)如圖所示
一個(gè)類(lèi)從Java源代碼(.java文件)編譯成了Java二進(jìn)制字節(jié)碼以后,必須經(jīng)過(guò)類(lèi)加載器才能被加載到JVM里面才能運(yùn)行。
我們一般把類(lèi)放在方法區(qū)里。類(lèi)將來(lái)創(chuàng)建的對(duì)象放在堆的部分,而堆里面的對(duì)象在調(diào)用方法時(shí)會(huì)用到虛擬機(jī)棧和程序計(jì)數(shù)器以及本地方發(fā)展。
方法執(zhí)行時(shí)每行代碼是由執(zhí)行引擎中的解釋器逐行進(jìn)行執(zhí)行的。方法里面的熱點(diǎn)代碼也就是頻繁調(diào)用的代碼,由即時(shí)編譯器來(lái)編譯執(zhí)行。GC會(huì)對(duì)垃圾進(jìn)行回收。
我們可以通過(guò)本地方法接口來(lái)調(diào)用操作系統(tǒng)提供的功能。
JVM的內(nèi)存結(jié)構(gòu)包括:
1.方法區(qū)
2.程序計(jì)數(shù)器
3.虛擬機(jī)棧
4.本地方法棧
5.堆
2.程序計(jì)數(shù)器
2.1.定義
Program Counter Register程序計(jì)數(shù)器(寄存器)
作用:
??是記住下一條jvm指令的執(zhí)行地址
特點(diǎn)
??是線(xiàn)程私有的
??不會(huì)存在內(nèi)存溢出(內(nèi)存結(jié)構(gòu)中唯一一個(gè)不會(huì)內(nèi)存溢出的結(jié)構(gòu))
在2.2中我們將會(huì)解釋程序計(jì)數(shù)器的作用及特點(diǎn)。
2.2.作用及特點(diǎn)解釋
二進(jìn)制字節(jié)碼 JVM指令 Java源代碼 0: getstatic #20 // PrintStream out = System.out; 3: astore_1 // - 4: aload_1 // out.println(1); 5: iconst_1 // - 6: invokevirtual #26 // - 9: aload_1 // out.println(2); 10: iconst_2 // - 11: invokevirtual #26 // - 14: aload_1 // out.println(3); 15: iconst_3 // - 16: invokevirtual #26 // - 19: aload_1 // out.println(4); 20: iconst_4 // - 21: invokevirtual #26 // - 24: aload_1 // out.println(5); 25: iconst_5 // - 26: invokevirtual #26 // - 29: return
我們可以看到這些代碼,第一行System.out賦值給了一個(gè)變量,在4:中去調(diào)用println()方法。然后依次打印1,2,3,4,5。這些指令不能直接交給CPU來(lái)執(zhí)行,必須經(jīng)過(guò)解釋器的作用。它負(fù)責(zé)把一條一條的字節(jié)碼指令解釋成機(jī)器碼,然后機(jī)器碼就可以交給CPU來(lái)執(zhí)行。
也就是
二進(jìn)制字節(jié)碼->解釋器->機(jī)器碼->CPU
實(shí)際上程序計(jì)數(shù)器的作用就是在指令的執(zhí)行過(guò)程中,記住下一條JVM指令的執(zhí)行地址。
上面我們二進(jìn)制字節(jié)碼前面的數(shù)字0,3,4…我們可以把其理解為地址。根據(jù)這些地址信息,我們就可以找到命令來(lái)執(zhí)行。
在每次拿到指令交給CPU執(zhí)行之后,程序計(jì)數(shù)器就會(huì)把下一條指令的地址放入到程序計(jì)數(shù)器中,等一條指令執(zhí)行完成之后,解釋器就會(huì)到程序計(jì)數(shù)器中取到下一條指令的地址。再把其經(jīng)過(guò)解釋器解釋成機(jī)器碼然后交給CPU執(zhí)行。然后一直重復(fù)這樣的過(guò)程。
在物理上,實(shí)現(xiàn)程序計(jì)數(shù)器是通過(guò)寄存器來(lái)實(shí)現(xiàn)的。寄存器是CPU組件里讀取最快的存儲(chǔ)單元。
程序計(jì)數(shù)器是線(xiàn)程私有的
假如說(shuō)上述代碼都在線(xiàn)程1中運(yùn)行,同時(shí)運(yùn)行的還有線(xiàn)程2和線(xiàn)程3,多個(gè)線(xiàn)程運(yùn)行的時(shí)候,CPU會(huì)給每個(gè)線(xiàn)程分配時(shí)間片,給線(xiàn)程1分配時(shí)間片,如果線(xiàn)程1在指定的時(shí)間沒(méi)有運(yùn)行完,它就會(huì)把狀態(tài)暫存,切換到線(xiàn)程2,線(xiàn)程2執(zhí)行自己的代碼。線(xiàn)程2執(zhí)行完了,再繼續(xù)執(zhí)行線(xiàn)程1的代碼,在線(xiàn)程切換的過(guò)程中,我們要記住下一條指令的執(zhí)行地址。就需要用到程序計(jì)數(shù)器。假如說(shuō)線(xiàn)程1剛開(kāi)始執(zhí)行到第9行代碼,恰好這個(gè)時(shí)候時(shí)間片用完,CPU切換到線(xiàn)程2去執(zhí)行,這時(shí)它就會(huì)把下一條指令的地址10記錄到程序計(jì)數(shù)器里面,而且程序計(jì)數(shù)器是線(xiàn)程私有的,它是屬于線(xiàn)程1的,等線(xiàn)程2代碼執(zhí)行完了,線(xiàn)程1搶到了時(shí)間片,它就會(huì)從自己的程序計(jì)數(shù)器里面取出下一行代碼。每個(gè)線(xiàn)程都有自己的程序計(jì)數(shù)器
3.虛擬機(jī)棧
3.1.棧的特點(diǎn)
棧類(lèi)似現(xiàn)實(shí)生活中的子彈夾。棧最重要的特點(diǎn)是后進(jìn)先出。
如圖,1是最先進(jìn)入棧中的,3是最后進(jìn)入棧中的,但是在出棧的時(shí)候,3最先出棧,1最后出棧。即他們按照1,2,3的順序入棧,按照3,2,1的順序出棧
虛擬機(jī)棧就是我們線(xiàn)程運(yùn)行時(shí)需要的內(nèi)存空間,一個(gè)線(xiàn)程運(yùn)行時(shí)需要一個(gè)棧。如果將來(lái)有多個(gè)線(xiàn)程的話(huà),它就會(huì)有多個(gè)虛擬機(jī)棧。
每個(gè)??梢钥闯墒怯啥鄠€(gè)棧幀組成,例如上圖中每個(gè)元素1,2,3都可以看成是棧幀。
一個(gè)棧幀就對(duì)應(yīng)著Java中一個(gè)方法的調(diào)用,即棧幀就是每個(gè)方法運(yùn)行時(shí)需要的內(nèi)存。每個(gè)方法運(yùn)行時(shí)需要的內(nèi)存一般有參數(shù),局部變量,返回地址,這些都需要占用內(nèi)存,所以每個(gè)方法執(zhí)行時(shí),都要預(yù)先把這些內(nèi)存分配好。
當(dāng)我們調(diào)用第一個(gè)方法棧幀時(shí),它就會(huì)給第一個(gè)方法分配棧幀空間,并且壓入棧內(nèi),當(dāng)這個(gè)方法執(zhí)行完了,就會(huì)把這個(gè)方法棧幀出棧,釋放這個(gè)方法所占用的內(nèi)存。
一個(gè)棧內(nèi)可能有多個(gè)棧幀存在。
總結(jié)
Java Virtual Machine Stacks(Java虛擬機(jī)棧)
- 每個(gè)線(xiàn)程運(yùn)行時(shí)所需要的內(nèi)存,稱(chēng)為虛擬機(jī)棧
- 每個(gè)棧由多個(gè)棧幀(Frame)組成,對(duì)應(yīng)著每次方法調(diào)用時(shí)所占用的內(nèi)存
- 每個(gè)線(xiàn)程只能有一個(gè)活動(dòng)棧幀,對(duì)應(yīng)著當(dāng)前正在執(zhí)行的那個(gè)方法(位于棧頂)
活動(dòng)棧幀表示線(xiàn)程正在執(zhí)行的方法。
3.2.棧的演示
public class teststacks { public static void main(String[] args) throws InterruptedException{ method1(); } public static void method1(){ method2(1,2); } public static int method2(int a,int b){ int c=a+b; return c; }}
可以自行調(diào)試以上代碼來(lái)觀察棧中的變化情況。
入棧順序:main->method1->method2
出棧順序:method2->method1->main
3.3.棧的問(wèn)題辨析
- 垃圾回收是否涉及棧內(nèi)存?
不涉及,垃圾回收只是回收堆內(nèi)存中的無(wú)用對(duì)象,棧內(nèi)存不需要對(duì)它執(zhí)行垃圾回收,隨著方法的調(diào)用結(jié)束,棧內(nèi)存就釋放了。 - 棧內(nèi)存分配越大越好嗎?
首先棧內(nèi)存可以指定:-Xss size(如果不指定棧內(nèi)存大小,不同系統(tǒng)會(huì)有一個(gè)不同的默認(rèn)值)
其次由于電腦內(nèi)存一定,假如有100Mb,如果給棧內(nèi)存指定為2Mb,則最多只能存在50個(gè)線(xiàn)程,所以并不是越大越好,棧內(nèi)存較大一般是可以進(jìn)行較多次的方法遞歸調(diào)用,而不會(huì)增強(qiáng)線(xiàn)程效率,反而會(huì)使線(xiàn)程數(shù)量減少,一般使用默認(rèn)大小。
3.4.棧的線(xiàn)程安全問(wèn)題
看一個(gè)變量是否線(xiàn)程安全,首先就是看這個(gè)變量對(duì)多個(gè)線(xiàn)程是共享的還是私有的,共享的變量需要考慮線(xiàn)程安全。
其次局部變量也不能保證是線(xiàn)程安全的,需要看此變量是否逃離了方法的作用范圍(作為參數(shù)和返回值逃出方法作用范圍時(shí)需要考慮線(xiàn)程安全問(wèn)題)
例如:
以下代碼中局部變量是私有的,是線(xiàn)程安全的
//多個(gè)線(xiàn)程同時(shí)執(zhí)行該方法,會(huì)不會(huì)造成x值混亂呢? //不會(huì),因?yàn)閤是方法內(nèi)的局部變量,是線(xiàn)程私有的,互不干擾 static void m1(){ int x=0; for(int i=0;i<5000;i++){ x++; } System.out.println(x); }
但是如果我們把變量的類(lèi)型改為static,此時(shí)就大不一樣了,x是靜態(tài)變量,線(xiàn)程1和線(xiàn)程2同時(shí)擁有同一個(gè)x,static變量針對(duì)多個(gè)線(xiàn)程是一個(gè)共享的,不加安全保護(hù)的話(huà),就會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題。
static void m1(){ static int x=0; for(int i=0;i<5000;i++){ x++; } System.out.println(x); }
我們?cè)倏磶讉€(gè)方法
public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(()->{ m2(sb); }).start(); } public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
m1是線(xiàn)程安全的:m1中的sb是線(xiàn)程中的局部變量,它是屬于線(xiàn)程私有的
m2線(xiàn)程不安全:sb它是方法的參數(shù),有可能有其它的線(xiàn)程訪問(wèn)到它,它就不再是線(xiàn)程私有的了,它對(duì)多個(gè)線(xiàn)程是共享的。
m3不是線(xiàn)程安全的:它被當(dāng)成返回結(jié)果返回了,返回了有可能其它的線(xiàn)程拿到這個(gè)對(duì)象,從而并發(fā)的修改。
3.5.棧內(nèi)存溢出(StackOverflowError)
什么情況下會(huì)導(dǎo)致棧內(nèi)存溢出吶?
1.棧幀過(guò)多導(dǎo)致棧內(nèi)存溢出(一般遞歸調(diào)用次數(shù)太多,進(jìn)棧太多導(dǎo)致溢出)
這里最容易出現(xiàn)的場(chǎng)景是函數(shù)的遞歸調(diào)用。
2.棧幀過(guò)大導(dǎo)致棧內(nèi)存溢出(不太容易出現(xiàn))
棧內(nèi)存溢出代碼演示1(自己開(kāi)發(fā)):
測(cè)試以下的程序,其中遞歸函數(shù)沒(méi)有遞歸邊界
public class Demo1_2 { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); }}
運(yùn)行結(jié)果如下
…
這里報(bào)了錯(cuò)誤StackOverflowError。
總共進(jìn)行了22846次遞歸調(diào)用
idea中設(shè)置棧內(nèi)存大小:
將棧內(nèi)存設(shè)置的小一點(diǎn),發(fā)現(xiàn)5000多次遞歸調(diào)用就溢出了。
棧內(nèi)存溢出代碼演示2(第三方依賴(lài)庫(kù)出現(xiàn)):
本案例可以使用JsonIgnore注解解決循環(huán)依賴(lài),數(shù)據(jù)轉(zhuǎn)換時(shí),只讓部門(mén)類(lèi)去關(guān)聯(lián)員工類(lèi),員工類(lèi)不再關(guān)聯(lián)部門(mén)類(lèi),在員工類(lèi)的部門(mén)屬性(dept)上加@JsonIgnore注解。具體使用詳情可以點(diǎn)擊此處查看
3.6.線(xiàn)程運(yùn)行診斷
3.6.1.案例1:cpu占用過(guò)多(linux系統(tǒng)為例)
排查步驟:
1.在linux中使用top命令,去查看后臺(tái)進(jìn)程對(duì)cpu的占用情況
注意,在這之前我們運(yùn)行了一道Java程序
Java代碼占用了CPU的99.3%.top命令只能定位到進(jìn)程,而無(wú)法定位到線(xiàn)程。
2.查看線(xiàn)程對(duì)cpu的占用情況:ps H -eo pid,tid,%cpu
如果顯示過(guò)多,可使用ps H -eo pid,tid,%cpu | grep 進(jìn)程id,過(guò)濾掉不想看的部分進(jìn)程
注意:ps不僅可以查看進(jìn)程,也可以查看線(xiàn)程對(duì)CPU的占用情況。H把進(jìn)程中的線(xiàn)程所有信息都展示出來(lái)。-eo規(guī)定輸出感興趣的內(nèi)容,這里我們想看看pid,tid和CPU的占用情況%cpu
當(dāng)線(xiàn)程數(shù)太多,排查不方便的話(huà),我們可以用grep pid來(lái)進(jìn)行篩選,過(guò)濾掉不感興趣的進(jìn)程
ps H -eo pid,tid,%cpu |grep 32655
3.定位到是哪個(gè)線(xiàn)程占用內(nèi)存過(guò)高后,再使用Jdk提供的命令(jstack+進(jìn)程id)去查看進(jìn)程中各線(xiàn)程的運(yùn)行信息,需要把第二步中查到的線(xiàn)程id(十進(jìn)制)轉(zhuǎn)為十六進(jìn)制,然后進(jìn)行比較查詢(xún)到位置后判斷異常信息。
thread1,thread2,thread3是我們自己定義的線(xiàn)程。
可以根據(jù)線(xiàn)程id,找到有問(wèn)題的線(xiàn)程,進(jìn)一步定位到問(wèn)題代碼的源碼行號(hào)
3.6.2.案例2:線(xiàn)程診斷_遲遲得不到結(jié)果
仍然通過(guò)jdk提供的 jstack+進(jìn)程id的方式,去查看進(jìn)程中各個(gè)線(xiàn)程的運(yùn)行信息
4.本地方法棧
含義:Java虛擬機(jī)調(diào)用本地方法時(shí),需要給本地方法提供的一些內(nèi)存空間
本地方法不是由Java編寫(xiě)的代碼,由于Java有時(shí)不能直接和操作系統(tǒng)打交道,所以需要用C/C++語(yǔ)言來(lái)與操作系統(tǒng)打交道,那么Java就可以通過(guò)調(diào)用本地方法來(lái)獲得這些功能。本地方法非常的多,如Object類(lèi)的clone(),hashCode方法,wait方法,notify方法等
public native int hashCode();
5.堆
5.1.定義
1.虛擬機(jī)棧,程序計(jì)數(shù)器,本地方法棧,這些都是線(xiàn)程私有的,而堆和方法區(qū),是線(xiàn)程公用的一塊內(nèi)存區(qū)域。
2.通過(guò)new關(guān)鍵字創(chuàng)建的對(duì)象都會(huì)使用堆內(nèi)存
3.由于堆是線(xiàn)程共享的,堆內(nèi)的對(duì)象都要考慮線(xiàn)程安全問(wèn)題(也有一些例外)
4.堆有垃圾回收機(jī)制,不再被引用的對(duì)象會(huì)被回收
5.2.堆內(nèi)存溢出(OutOfMemoryError:Java heap space)
對(duì)象一直存在于堆中未被回收,且占用內(nèi)存越來(lái)越大,最終導(dǎo)致堆內(nèi)存溢出(雖然堆中有垃圾回收機(jī)制,但垃圾回收機(jī)制不是回收所有的對(duì)象)
我們可以看看下面的代碼
public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>(); String a = "hello"; while (true) { list.add(a); // hello, hellohello, hellohellohellohello ... a = a + a; // hellohellohellohello i++; } } catch (Throwable e) { e.printStackTrace(); System.out.println(i); }}
報(bào)了錯(cuò)誤java.lang.OutOfMemoryError
代碼中每次都拼接一個(gè)hello,由于定義的list集合創(chuàng)建在try語(yǔ)句里面,所以在for循環(huán)不斷執(zhí)行過(guò)程中,list集合是不會(huì)被回收的,只要程序還沒(méi)到catch之前,它就一直有效。而字符串對(duì)象都被追加到了集合內(nèi)部,字符串對(duì)象由于一直被使用,所以不會(huì)被回收。
我們可以通過(guò)-Xmx來(lái)設(shè)置堆空間大小。
我們把堆內(nèi)存改成8M(之前內(nèi)存是4G),此時(shí)只運(yùn)行了17次。
5.3.堆內(nèi)存診斷
1.jps工具:jps,查看當(dāng)前進(jìn)程中有哪些Java進(jìn)程,并將進(jìn)程id顯示出來(lái)(idea中通過(guò)terminal命令行輸入命令)
2.jmap工具:jmap -heap 進(jìn)程id 查詢(xún)某一個(gè)時(shí)刻堆內(nèi)存的占用情況
3.jconsole工具:圖形界面的,多功能監(jiān)測(cè)工具,可連續(xù)監(jiān)測(cè),使用流程圖如下(1-2-3):
6.方法區(qū)
6.1.定義
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域,他用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)常量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。(與類(lèi)有關(guān)的信息)。雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是他卻有一個(gè)別名叫做Non-Heap(非堆),目的應(yīng)該是與Java堆區(qū)分開(kāi)來(lái)。方法區(qū)在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
對(duì)于習(xí)慣在HotSpot虛擬機(jī)上開(kāi)發(fā)、部署程序的開(kāi)發(fā)者來(lái)說(shuō),很多都更愿意把方法取稱(chēng)為“永久代”(Permanent Generation),本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說(shuō)使用永久代來(lái)實(shí)現(xiàn)方法區(qū)而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存,能夠省去專(zhuān)門(mén)為方法區(qū)編寫(xiě)內(nèi)存管理代碼的工作。對(duì)于其他虛擬機(jī)(如BEA JRockit、IBM J9等)來(lái)說(shuō)是不存在永久代的概念的。原則上,如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受虛擬機(jī)規(guī)范約束,但使用永久代來(lái)實(shí)現(xiàn)方法區(qū),現(xiàn)在看來(lái)并不是一個(gè)好主意,因?yàn)檫@樣更容易遇到內(nèi)存溢出問(wèn)題(永久代有-XX:MaxPermSize的上限,J9和JRockit只要沒(méi)有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB,就不會(huì)出現(xiàn)問(wèn)題),而且有極少數(shù)方法(例如String.intern())會(huì)因這個(gè)原因?qū)е虏煌摂M機(jī)下有不同的表現(xiàn)。因此,對(duì)于HotSpot虛擬機(jī),根據(jù)官方發(fā)布的路線(xiàn)圖信息,現(xiàn)在也已放棄永久代并逐步改為采用Navtive Memory來(lái)實(shí)現(xiàn)方法區(qū)的規(guī)劃,在JDK1.7的HostSpot中,已經(jīng)把原本放在永久代的字符串常量池移出,jdk1.8中后稱(chēng)作元空間,用的操作系統(tǒng)內(nèi)存。
Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以喧囂而固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類(lèi)型的卸載,一般來(lái)說(shuō),這個(gè)區(qū)域的回收“成績(jī)”比較難以令人滿(mǎn)意,尤其是類(lèi)型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收確實(shí)是必要的。在Sun公司的BUG列表中,曾出現(xiàn)過(guò)的若干個(gè)嚴(yán)重的BUG就是由于低版本的HotSpot虛擬機(jī)對(duì)此區(qū)域未完全回收而導(dǎo)致內(nèi)存泄漏。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無(wú)法滿(mǎn)足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。
文原文關(guān)于虛擬機(jī)的定義:
6.2.定義
jdk1.8之前,方法區(qū)是用的堆內(nèi)存,1.8之后,方法區(qū)用的操作系統(tǒng)內(nèi)存。
這塊不是太清晰,可以參考下此篇博客點(diǎn)擊查看
常量池分為靜態(tài)常量池和動(dòng)態(tài)常量池,下圖中的常量池指的是動(dòng)態(tài)常量池,因?yàn)樗鼈円呀?jīng)被讀入內(nèi)存中去,而靜態(tài)常量池存在于class文件中
6.3.方法區(qū)內(nèi)存溢出(OutOfMemoryError: Metaspace)
1.8以前會(huì)導(dǎo)致永久代內(nèi)存溢出
1.8以后會(huì)導(dǎo)致元空間內(nèi)存溢出
/** * 演示元空間內(nèi)存溢出 java.lang.OutOfMemoryError: Metaspace * -XX:MaxMetaspaceSize=8m */public class Demo1_8 extends ClassLoader { // 可以用來(lái)加載類(lèi)的二進(jìn)制字節(jié)碼 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); for (int i = 0; i < 10000; i++, j++) { // ClassWriter 作用是生成類(lèi)的二進(jìn)制字節(jié)碼 ClassWriter cw = new ClassWriter(0); //參數(shù):版本號(hào), public, 類(lèi)名, 包名, 父類(lèi), 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 生成類(lèi),二進(jìn)制字節(jié)碼用byte來(lái)表示,返回 byte[] byte[] code = cw.toByteArray(); // 執(zhí)行了類(lèi)的加載 test.defineClass("Class" + i, code, 0, code.length); // Class 對(duì)象 } } finally { System.out.println(j); } }}
jdk1.8以后, 默認(rèn)情況下,方法區(qū)用的是系統(tǒng)內(nèi)存,所以加大還是不會(huì)導(dǎo)致內(nèi)存溢出,循環(huán)很多次都運(yùn)行成功。
當(dāng)設(shè)置了-XX:MaxMetaspaceSize=8m,到了5411次就溢出了。報(bào)的是java.lang.OutOfMemoryError: Metaspace錯(cuò)誤
而1.8以前永久代溢出報(bào)的錯(cuò)誤是java.lang.OutOfMemoryError:PermGen space
6.4.常量池
常量池,就是一張表,虛擬機(jī)指令根據(jù)這站常量表找到要執(zhí)行的類(lèi)名、方法名、參數(shù)類(lèi)型、字面量信息(如字符串常量、true和false)。
運(yùn)行時(shí)常量池,常量池是.class文件中的,當(dāng)該類(lèi)被加載,它的常量池信息就會(huì)放入運(yùn)行時(shí)常量池,并把里面的符號(hào)地址變?yōu)檎鎸?shí)地址*。
public class HelloWorld { public static void main(String[] args) { System.out.println("hello,world"); }}
以上是一個(gè)helloworld程序,helloworld要運(yùn)行,肯定要先編譯成一個(gè)二進(jìn)制字節(jié)碼。
二進(jìn)制字節(jié)碼由類(lèi)的基本信息、常量池、類(lèi)方法定義(包含了虛擬機(jī)指令)。
反編譯HelloWorld(之前需要運(yùn)行將.java文件編譯成.class文件)
使用idea工具
F:IDEAprojectsjvm>javap -v F:IDEAprojectsjvmoutproductionuntitledHelloWorld.class
F:IDEAprojectsjvmoutproductionuntitled是HelloWorld.class所在的路徑
顯示類(lèi)的詳細(xì)信息
Classfile /F:/IDEA/projects/jvm/out/production/untitled/HelloWorld.class Last modified 2021-1-30; size 533 bytes MD5 checksum 82d075eb7217b4d23706f6cfbd44f8f1 Compiled from "HelloWorld.java"public class HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
可以看到類(lèi)的文件,最后修改時(shí)間,簽名。以及版本等等。有的還有訪問(wèn)修飾符、父類(lèi)和接口等詳細(xì)信息。
顯示常量池
Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // hello,world #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // HelloWorld #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 LHelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 hello,world #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V
顯示方法定義
{ public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LHelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello,world 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;}
第一個(gè)方法是public HelloWorld();它是編譯器自動(dòng)為我們構(gòu)造的無(wú)參構(gòu)造方法。
第二個(gè)是public static void main(java.lang.String[]);即main方法
方噶里面就包括了虛擬機(jī)的指令了。
getstatic獲取一個(gè)靜態(tài)變量,即獲取System.out靜態(tài)變量
ldc是加載一個(gè)參數(shù),參數(shù)是字符串hello,world
invokevirtual虛方法調(diào)用,println方法
return執(zhí)行結(jié)束。
我們getstatic、ldc、invokevirtual后面都有一個(gè)#2,#3,#4。在解釋器翻譯這些虛擬機(jī)指令的時(shí)候,它會(huì)把這些#2,#3,#4進(jìn)行一個(gè)查表翻譯。比如getstatic #2,就去查常量池的表。在常量池中
#2 = Fieldref #21.#22 引用的是成員變量#21,#22.
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
然后再去找#28.29,30
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
所以現(xiàn)在我就知道了,我是要找到j(luò)ava.lang.system類(lèi)下叫out的成員變量,類(lèi)型是java/io。
同理,ldc是找#3 = String #23 Utf8 hello,world,它是虛擬機(jī)常量池的一個(gè)字符串。把helloworld常量變成字符串對(duì)象加載進(jìn)來(lái)。
invokevirtual #4 Methodref #24.#25 等等
所以常量池的作用就是給我們指令提供一些常量符號(hào),根據(jù)這些常量符號(hào),我們就可以根據(jù)查表的方式去找到它,這樣虛擬機(jī)才能成功的執(zhí)行它。
相關(guān)免費(fèi)學(xué)習(xí)推薦:java基礎(chǔ)教程