自己學習記錄(偷學享學King老師),方便隨時查看,不喜勿噴,如有錯誤,歡迎指出。
先來個思維導圖看一下大概內容:
一、jvm基礎知識
1.jvm與操作系統(tǒng)的關系
JVM 全稱 Java Virtual Machine,也就是我們耳熟能詳?shù)?Java 虛擬機。它能識別 .class后綴的文件,并且能夠解析它的指令,最終調用操作系統(tǒng)上的函數(shù),完成我們想要的操作。
jvm就像是一個翻譯,Java 程序不一樣,使用 javac 編譯成 .class 文件之后,還需要使用 Java 命令去主動執(zhí)行它,操作系統(tǒng)并不認識這些 .class 文件,所以JVM就是一個翻譯,例如下圖:從圖中可以看到,有了 JVM 這個抽象層之后,Java 就可以實現(xiàn)跨平臺了。JVM 只需要保證能夠正確執(zhí)行 .class 文件,就可以運行在諸如 Linux、Windows、MacOS 等平臺上了。
2.從跨平臺到跨語言
跨平臺:我們寫的這個類Person這個類,在不同的操作系統(tǒng)上(Linux、Windows、MacOS 等平臺)執(zhí)行,效果是一樣,這個就是JVM的跨平臺性。為了實現(xiàn)跨平臺型,不同操作系統(tǒng)有不同的JDK的版本。
跨語言:JVM只識別字節(jié)碼,所以JVM其實跟語言是解耦的,也就是沒有直接關聯(lián),并不是它翻譯Java文件,而是識別class文件,這個一般稱之為字節(jié)碼。還有像Groovy 、Kotlin、Jruby等等語言,它們其實也是編譯成字節(jié)碼,所以也可以在JVM上面跑,這個就是JVM的跨語言特征。
3.JVM、JRE、JDK的關系
JVM只是一個翻譯,把Class翻譯成機器識別的代碼,但是需要注意,JVM 不會自己生成代碼,需要大家編寫代碼,同時需要很多依賴類庫,這個時候就需要用到JRE。
JRE是什么,它除了包含JVM之外,提供了很多的類庫(就是我們說的jar包,它可以提供一些即插即用的功能,比如讀取或者操作文件,連接網(wǎng)絡,使用I/O等等之類的)這些東西就是JRE提供的基礎類庫。JVM 標準加上實現(xiàn)的一大堆基礎類庫,就組成了 Java 的運行時環(huán)境,也就是我們常說的 JRE(Java Runtime Environment)。
但對于程序員來說,JRE還不夠。我寫完要編譯代碼,還需要調試代碼,還需要打包代碼、有時候還需要反編譯代碼。所以我們會使用JDK,因為JDK還提供了一些非常好用的小工具,比如 javac(編譯代碼)、java、jar (打包代碼)、javap(反編譯<反匯編>)等。這個就是JDK。
二、JVM整體結構
一個 Java 程序,首先經(jīng)過 javac 編譯成 .class 文件,然后 JVM 將其加載到方法區(qū),執(zhí)行引擎將會執(zhí)行這些字節(jié)碼。執(zhí)行時,會翻譯成操作系統(tǒng)相關的函數(shù)。JVM 作為 .class 文件的翻譯存在,輸入字節(jié)碼,調用操作系統(tǒng)函數(shù)。
過程如下:Java 文件->編譯器>字節(jié)碼->JVM->機器碼。
我們所說的 JVM,狹義上指的就 HotSpot(因為JVM有很多版本,但是使用最多的是HotSpot)。如非特殊說明,我們都以 HotSpot 為準。Java 之所以成為跨平臺,就是由于 JVM 的存在。Java 的字節(jié)碼,是溝通 Java 語言與 JVM 的橋梁,同時也是溝通 JVM 與操作系統(tǒng)的橋梁。
1.解釋執(zhí)行與JIT(just in time)
字節(jié)碼執(zhí)行原理:
編譯后的字節(jié)碼在沒有經(jīng)過JIT編譯前,是通過字節(jié)碼解釋器進行解釋執(zhí)行。其執(zhí)行原理為:字節(jié)碼解釋器讀取內存中的字節(jié)碼,按照順序讀取字節(jié)碼指令,讀取一個指令就將其翻譯成固定的操作,根據(jù)這些操作進行分支,循環(huán),跳轉等動作。
字節(jié)碼無法直接交給硬件執(zhí)行需要虛擬機翻譯成機器碼才能執(zhí)行,“翻譯”的策略有兩種:解釋執(zhí)行和編譯執(zhí)行又稱即時編譯(JIT)。解釋執(zhí)行是沒執(zhí)行一句字節(jié)碼的時候把字節(jié)碼翻譯成機器碼并執(zhí)行,優(yōu)點是啟動效率快,缺點是整體的執(zhí)行速度較慢。編譯執(zhí)行預先把所有機器碼編譯成字節(jié)碼并一起執(zhí)行,其特點與解釋執(zhí)行相反,啟動較慢執(zhí)行較快。
在jvm虛擬機中是兩者混合出現(xiàn),既有解釋執(zhí)行也有編譯執(zhí)行。首先是解釋執(zhí)行,一條條執(zhí)行所有字節(jié)碼,如果JVM發(fā)現(xiàn)某個方法被頻繁的調用會把該方法用編譯執(zhí)行的策略編譯好,下次執(zhí)行的時候直接調用機器碼,這種方法被稱為熱點方法,由此可見編譯執(zhí)行是以方法為單位。
2.運行時數(shù)據(jù)區(qū)
Java 引以為豪的就是它的自動內存管理機制。java虛擬機在執(zhí)行java程序過程中會把他管理的內存劃分為多個不同的數(shù)據(jù)區(qū)域:程序計數(shù)器、虛擬機棧、本 地方法棧、Java堆、方法區(qū) (運行時常量池)、直接內存(堆外內存),其中也分為線程共享和線程私有的數(shù)據(jù)區(qū),如圖:
運行時數(shù)據(jù)區(qū).png
3.程序計數(shù)器
(1).定義
程序計數(shù)器是當前線程正在執(zhí)行的字節(jié)碼的地址。
(2).作用
程序計數(shù)器是一塊很小的內存空間,主要用來記錄各個線程執(zhí)行的字節(jié)碼的地址,例如,分支、循環(huán)、跳轉、異常、線程恢復等都依賴于計數(shù)器,l類似記錄代碼的行號。
由于 Java 是多線程語言,當執(zhí)行的線程數(shù)量超過 CPU 核數(shù)時,線程之間會根據(jù)時間片輪詢爭奪 CPU 資源。如果一個線程的時間片用完了,或者是其它原因導致這個線程的 CPU 資源被提前搶奪,那么這個退出的線程就需要單獨的一個程序計數(shù)器,來記錄下一條運行的指令,以便在時間片回來時能接著執(zhí)行后續(xù)代碼。
(3).特點(參考如下(https://blog.csdn.net/sunhuiliang85/article/details/90718251))
1.程序計數(shù)器具有線程隔離性,每一個線程在工作的時候都有一個獨立的計數(shù)器
2.程序計數(shù)器占用的內存空間非常小,可以忽略不計
3.程序計數(shù)器是java虛擬機規(guī)范中唯一一個沒有規(guī)定任何OutofMemeryError的區(qū)域
4.程序執(zhí)行的時候,程序計數(shù)器是有值的,其記錄的是程序正在執(zhí)行的字節(jié)碼的地址
5.執(zhí)行native本地方法時,程序計數(shù)器的值為空。原因是native方法是java通過jni調用本地C/C++庫來實現(xiàn),非java字節(jié)碼實現(xiàn),所以無法統(tǒng)計.
4.虛擬機棧
先來個圖看看虛擬機棧結構:
棧是先進后出FILO(First In Last Out)的數(shù)據(jù)結構;
虛擬機棧在JVM運行過程中存儲當前線程運行方法所需的數(shù)據(jù),指令、返回地址。
Java 虛擬機棧是基于線程的。哪怕你只有一個 main() 方法,也是以線程的方式運行的。在線程的生命周期中,參與計算的數(shù)據(jù)會頻繁地入棧和出棧,棧的生命周期是和線程一樣的。
棧里的每條數(shù)據(jù),就是棧幀。在每個 Java 方法被調用的時候,都會創(chuàng)建一個棧幀,并入棧。一旦完成相應的調用,則出棧。所有的棧幀都出棧后,線程也就結束了。
每個棧幀,都包含四個區(qū)域:(局部變量表、操作數(shù)棧、動態(tài)連接、返回地址)
棧的大小缺省為1M,可用參數(shù) –Xss調整大小,例如-Xss256k。
(1)局部變量表
顧名思義就是局部變量的表,用于存放我們的局部變量的。首先它是一個32位的長度,主要存放我們的Java的八大基礎數(shù)據(jù)類型,一般32位就可以存放下,如果是64位的就使用高低位占用兩個也可以存放下,如果是局部的一些對象,比如我們的Object對象,我們只需要存放它的一個引用地址即可。
(2)操作數(shù)棧
在JVM中,基于解釋執(zhí)行的這種方式是基于棧的引擎,這個說的棧,就是操作數(shù)棧。
存放我們方法執(zhí)行的操作數(shù)的,它就是一個棧,先進后出的棧結構,操作數(shù)棧,就是用來操作的,操作的的元素可以是任意的java數(shù)據(jù)類型,所以我們知道一個方法剛剛開始的時候,這個方法的操作數(shù)棧就是空的,操作數(shù)棧運行方法就是JVM一直運行入棧/出棧的操作。
(3)動態(tài)連接
Java語言特性多態(tài)(需要類運行時才能確定具體的方法)??蓞⒖?br> https://blog.csdn.net/FloatDreamed/article/details/96147409
(4)完成出口(返回地址)
正常返回(調用程序計數(shù)器中的地址作為返回)、異常的話(通過異常處理器表<非棧幀中的>來確定;在方法退出后到該方法被調用的位置,方法正常退出時,調用者的PC計數(shù)器的值作為返回地址,即調用該方法的指令的下一條指令地址,而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
public class Person {
public int work()throws Exception{//運行過程中 打包一個棧幀
int x =1;//int型1入操作數(shù)棧(iconst_1); 然后存入局部變量表下標為1的位置(istore_1)
int y =2;//iconst_2 istore_2
//x+y:iload_1和iload_2將局部變量表下標為1和2的數(shù)據(jù)入棧
//iadd:3步走:(1)將棧頂兩個int數(shù)據(jù)出棧 (2)相加 (3)將結果壓入操作數(shù)棧
//*10:bipush 10 將10擴展成int值入棧
//imul:3步走:(1)將棧頂?shù)?和10出棧 (2)相乘 (3)將結果壓入操作數(shù)棧
//int z:istore_3:將棧頂?shù)?0存入局部變量表下標Wie3的位置
int z =(x+y)*10;
//返回:iload_3:將局部變量表下標為3的數(shù)據(jù)入棧
return z;
}
public static void main(String[] args) throws Exception{
Person person = new Person();//person 一個引用, new Person()對象
person.work();//執(zhí)行完了,出棧
}
}
這段代碼對應的字節(jié)碼:
流程簡圖對照代碼注釋:
5.本地方法棧
本地方法棧跟 Java 虛擬機棧的功能類似,Java 虛擬機棧用于管理 Java 函數(shù)的調用,而本地方法棧則用于管理本地方法的調用。但本地方法并不是用 Java 實現(xiàn)的,而是由 C 語言實現(xiàn)的。
本地方法棧是和虛擬機棧非常相似的一個區(qū)域,它服務的對象是 native 方法。你甚至可以認為虛擬機棧和本地方法棧是同一個區(qū)域。
虛擬機規(guī)范無強制規(guī)定,各版本虛擬機自由實現(xiàn) ,HotSpot直接把本地方法棧和虛擬機棧合二為一 。
6.方法區(qū)
方法區(qū)主要是用來存放已被虛擬機加載的類相關信息,包括類信息、靜態(tài)變量、常量、運行時常量池、字符串常量池,即時編譯期編譯后的代碼。
很多開發(fā)者都習慣將方法區(qū)稱為“永久代”,其實這兩者并不是等價的。
HotSpot 虛擬機使用永久代來實現(xiàn)方法區(qū),但在其它虛擬機中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一說。因此,方法區(qū)只是 JVM 中規(guī)范的一部分,可以說,在 HotSpot 虛擬機中,設計人員使用了永久代來實現(xiàn)了 JVM 規(guī)范的方法區(qū)。
JVM 在執(zhí)行某個類的時候,必須先加載。在加載類(加載、驗證、準備、解析、初始化)的時候,JVM 會先加載 class 文件,而在 class 文件中除了有類的版本、字段、方法和接口等描述信息外,還有一項信息是常量池 (Constant Pool Table),用于存放編譯期間生成的各種字面量和符號引用。
字面量包括字符串(String a=“b”)、基本類型的常量(final 修飾的變量),符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、字段的名稱和描述符以及方法的名稱和描述符。
而當類加載到內存中后,JVM 就會將 class 文件常量池中的內容存放到運行時的常量池中;在解析階段,JVM 會把符號引用替換為直接引用(對象的索引值)。
例如,類中的一個字符串常量在 class 文件中時,存放在 class 文件常量池中的;在 JVM 加載完類之后,JVM 會將這個字符串常量放到運行時常量池中,并在解析階段,指定該字符串對象的索引值。運行時常量池是全局共享的,多個類共用一個運行時常量池,class 文件中常量池多個相同的字符串在運行時常量池只會存在一份。
方法區(qū)與堆空間類似,也是一個共享內存區(qū),所以方法區(qū)是線程共享的。假如兩個線程都試圖訪問方法區(qū)中的同一個類信息,而這個類還沒有裝入 JVM,那么此時就只允許一個線程去加載它,另一個線程必須等待。在 HotSpot 虛擬機、Java7 版本中已經(jīng)將永久代的靜態(tài)變量和運行時常量池轉移到了堆中,其余部分則存儲在 JVM 的非堆內存中,而 Java8 版本已經(jīng)將方法區(qū)中實現(xiàn)的永久代去掉了,并用元空間(class metadata)代替了之前的永久代,并且元空間的存儲位置是本地。
元空間大小參數(shù):
jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
jdk1.8以后大小就只受本機總內存的限制(如果不設置參數(shù)的話)
Java8 為什么使用元空間替代永久代,這樣做有什么好處和壞處呢?
官方給出的解釋是:移除永久代是為了融合 HotSpot JVM 與 JRockit VM 而做出的努力,因為 JRockit 沒有永久代,所以不需要配置永久代。
永久代內存經(jīng)常不夠用或發(fā)生內存溢出,拋出異常java.lang.OutOfMemoryError: PermGen。這是因為在 JDK1.7 版本中,指定的 PermGen 區(qū)大小為 8M,由于 PermGen 中類的元數(shù)據(jù)信息在每次 FullGC 的時候都可能被收集,回收率都偏低,成績很難令人滿意;還有,為 PermGen 分配多大的空間很難確定,PermSize 的大小依賴于很多因素,比如,JVM 加載的 class 總數(shù)、常量池的大小和方法的大小等。
好處:元空間只受機器內存限制,方便拓展;
壞處:擠壓堆空間;因為堆空間受-Xms(堆的最小值)和-Xmx(堆的最大值)參數(shù)限制,如果我機器內存有20G,堆空間最大值為10G,然而元空間只受機器限制,此時元空間如果使用了15G,那么堆空間最大只能擴展5G,則你的參數(shù)10G不生效了,所以就擠壓了堆空間。
7.java堆
堆是 JVM 上最大的內存區(qū)域,我們申請的幾乎所有的對象,都是在這里存儲的。我們常說的垃圾回收,操作的對象就是堆。
堆空間一般是程序啟動時,就申請了,但是并不一定會全部使用。
隨著對象的頻繁創(chuàng)建,堆空間占用的越來越多,就需要不定期的對不再使用的對象進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。
那一個對象創(chuàng)建的時候,到底是在堆上分配,還是在棧上分配呢?這和兩個方面有關:對象的類型和在 Java 類中存在的位置。
Java 的對象可以分為基本數(shù)據(jù)類型和普通對象。
對于普通對象來說,JVM 會首先在堆上創(chuàng)建對象,然后在其他地方使用的其實是它的引用。比如,把這個引用保存在虛擬機棧的局部變量表中。
對于基本數(shù)據(jù)類型來說(byte、short、int、long、float、double、char),有兩種情況。當你在方法體內聲明了基本數(shù)據(jù)類型的對象,它就會在棧上直接分配。其他情況,都是在堆上分配。
堆大小參數(shù):
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大??;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
方法區(qū)和堆都是線程共享的,為什么要放在兩個區(qū)?
動靜分離(變和不變)的思想,提高垃圾回收的效率。堆中存放的是經(jīng)常手動創(chuàng)建的對象或數(shù)組,會被GC頻繁回收;而方法區(qū)存放的是靜態(tài)變量,類信息,常量等,不會變的信息,很難被GC回收。
8.直接內存
不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內存區(qū)域;如果使用了NIO,這塊區(qū)域會被頻繁使用,在java堆內可以用directByteBuffer對象直接引用并操作;
這塊內存不受java堆大小限制,但受本機總內存的限制,可以通過-XX:MaxDirectMemorySize來設置(默認與堆內存最大值一樣),所以也會出現(xiàn)OOM異常。
三、從底層深入理解運行時數(shù)據(jù)區(qū)
public class JVMObject {
public final static String MAN_TYPE = "man"; // 常量
public static String WOMAN_TYPE = "woman"; // 靜態(tài)變量
public static void main(String[] args)throws Exception {//棧幀
Teacher T1 = new Teacher();//堆中 T1 是局部變量
T1.setName("Mark");
T1.setSexType(MAN_TYPE);
T1.setAge(36);
for (int i=0;i<15;i++){//進行15次垃圾回收
System.gc();//垃圾回收
}
Teacher T2 = new Teacher();
T2.setName("King");
T2.setSexType(MAN_TYPE);
T2.setAge(18);
Thread.sleep(Integer.MAX_VALUE);//線程休眠很久很久
}
}
class Teacher{
String name;
String sexType;
int age;//堆
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
以上代碼運行后,運行時數(shù)據(jù)區(qū)大體機構:
HSDB使用
運行以上代碼,在jdk目錄C:\Program Files\Java\jdk1.8.0_60\lib下執(zhí)行cmd,輸入
java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB,啟動HSDB工具,然后使用jps命令查看JVMObject程序的進程id,
然后綁定id,
1.點擊第二個圖標,查看main方法的棧幀信息。
1列:內存地址:指的是虛擬內存意義上的地址,不是物理內存的地址
2列:改地址上存的數(shù)據(jù)
2.查看Teacher的信息
T1:0x0000000012cd1c40
T2:0x0000000012200000
3.查看兩個對象在堆上的分布
有代碼可知,T1經(jīng)過了15次的垃圾回收,年齡超過15,所以落在老年代,而T2是新生對象,所以落在新生代的eden區(qū)。
四.深入辨析堆和棧
功能
1.以棧幀的方式存儲方法調用的過程,并存儲方法調用過程中基本數(shù)據(jù)類型的變量(int、short、long、byte、float、double、boolean、char等)以及對象的引用變量,其內存分配在棧上,變量出了作用域就會自動釋放;
2.而堆內存用來存儲Java中的對象。無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆內存中;
線程獨享還是共享
1.棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。
2.堆內存中的對象對所有線程可見。堆內存中的對象可以被所有線程訪問。
空間大小
棧的內存要遠遠小于堆內存,棧的深度是有限的,可能會發(fā)生StackOverFlowError問題。
五.內存溢出
棧溢出:
(1)StackOverflowError :
HotSpot版本中棧的大小是固定的,是不支持拓展的。
java.lang.StackOverflowError 一般的方法調用是很難出現(xiàn)的,如果出現(xiàn)了可能會是無限遞歸。
虛擬機棧帶給我們的啟示:方法的執(zhí)行因為要打包成棧楨,所以天生要比實現(xiàn)同樣功能的循環(huán)慢,所以樹的遍歷算法中:遞歸和非遞歸(循環(huán)來實現(xiàn))都有存在的意義。遞歸代碼簡潔,非遞歸代碼復雜但是速度較快。
(2)OutOfMemoryError:不斷建立線程,JVM申請棧內存,機器沒有足夠的內存。(一般演示不出,演示出來機器也死了)
public void king(){//一個棧幀--虛擬機棧運行
king();//無窮的遞歸
}
public static void main(String[] args)throws Throwable {
StackOverFlow javaStack = new StackOverFlow(); //new一個對象
javaStack.king();
}
}
堆溢出:
(1)內存溢出:申請內存空間,超出最大堆內存空間。
如果是內存溢出,則通過 調大 -Xms,-Xmx參數(shù)。
如果不是內存泄漏,就是說內存中的對象都是必須存活的,那么就應該檢查JVM的堆參數(shù)設置,與機器的內存對比,看是否還有可以調整的空間,再從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長、存儲結構設計不合理等情況,盡量減少程序運行時的內存消耗。
/**
* VM Args:-Xms30m -Xmx30m 給堆分配30M大小 -XX:+PrintGCDetails 打印堆詳情
* 堆內存溢出(直接溢出)
*/
public class HeapOom {
public static void main(String[] args)
{
//35M的數(shù)組(堆)
String[] strings = new String[35*1000*1000];
}
}
(2)GC回收時間長時會拋出OutOfMemoryError。過長的定義是,超過98%的時間用來做GC并且回收了不到2%的堆內存,連續(xù)多次GC都只回收了不到2%的極端情況下才會拋出。假設不拋出GC overhead limit錯誤會發(fā)生什么情況呢?那就是GC清理的這么點內存很快會再次填滿,迫使GC再次執(zhí)行,這樣就形成惡性循環(huán),CPU使用率一直是100%,而GC缺沒有任何成果。
/**
* VM Args:-Xms30m -Xmx30m -XX:+PrintGC 堆的大小30M
* GC調優(yōu)---生產(chǎn)服務器推薦開啟(默認是關閉的)
* -XX:+HeapDumpOnOutOfMemoryErro
*/
public class HeapOom2 {
public static void main(String[] args)
{
//GC ROOTS
List<Object> list = new LinkedList<>();
int i =0;
while(true){
i++;
if(i%10000==0) System.out.println("i="+i);
list.add(new Object());
}
}
}
方法區(qū)溢出:
- 運行時常量池溢出
- 方法區(qū)中保存的Class對象沒有被及時回收掉或者Class信息占用的內存超過了我們配置。
注意Class要被回收,條件比較苛刻(僅僅是可以,不代表必然,因為還有一些參數(shù)可以進行控制):
1、該類所有的實例都已經(jīng)被回收,也就是堆中不存在該類的任何實例。
2、加載該類的ClassLoader已經(jīng)被回收。
3、該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
/**
* cglib動態(tài)生成
* Enhancer中 setSuperClass和setCallback, 設置好了SuperClass后, 可以使用create制作代理對象了
* 限制方法區(qū)的大小導致的內存溢出
* VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
* */
public class MethodAreaOutOfMemory {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MethodAreaOutOfMemory.TestObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
public static class TestObject {
private double a = 34.53;
private Integer b = 9999999;
}
}
直接內存(堆外)溢出:
直接內存的容量可以通過MaxDirectMemorySize來設置(默認與堆內存最大值一樣),所以也會出現(xiàn)OOM異常;
由直接內存導致的內存溢出,一個比較明顯的特征是在HeapDump文件中不會看見有什么明顯的異常情況,如果發(fā)生了OOM,同時Dump文件很小,可以考慮重點排查下直接內存方面的原因。
(JVM Heap Dump(堆轉儲文件),Heap Dump記錄了JVM中堆內存運行的情況)
import java.nio.ByteBuffer;//使用了nio
/**
* VM Args:-XX:MaxDirectMemorySize=100m 限制100M
* 堆外內存(直接內存溢出)
*/
public class DirectOom {
public static void main(String[] args) {
//直接分配128M的直接內存(100M)
ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204);
}
}
六.虛擬機的優(yōu)化技術
1.編譯優(yōu)化技術——方法內聯(lián)
方法內聯(lián)的優(yōu)化行為,就是把目標方法的代碼原封不動的“復制”到調用的方法中,避免真實的方法調用而已。
public class MethodDeal {
public static void main(String[] args) {
// max(1,2);//調用max方法: 虛擬機棧 --入棧(max 棧幀)
//方法內聯(lián)
boolean i1 = 1>2;// 減少一次 棧幀入棧
}
public static boolean max(int a,int b){//方法的執(zhí)行入棧幀。
return a>b;
}
}
2.棧的優(yōu)化技術——棧幀之間數(shù)據(jù)的共享
在一般的模型中,兩個不同的棧幀的內存區(qū)域是獨立的,但是大部分的JVM在實現(xiàn)中會進行一些優(yōu)化,使得兩個棧幀出現(xiàn)一部分重疊。(主要體現(xiàn)在方法中有參數(shù)傳遞的情況),讓下面棧幀的操作數(shù)棧和上面棧幀的部分局部變量重疊在一起,這樣做不但節(jié)約了一部分空間,更加重要的是在進行方法調用時就可以直接公用一部分數(shù)據(jù),無需進行額外的參數(shù)復制傳遞了。
public class JVMStack {
public int work(int x) throws Exception{
int z =(x+5)*10;//局部變量表有
Thread.sleep(Integer.MAX_VALUE);
return z;
}
public static void main(String[] args)throws Exception {
JVMStack jvmStack = new JVMStack();
jvmStack.work(10);//10 放入main棧幀操作數(shù)棧
}
}
看一下這段代碼的棧幀信息:可以看出main方法的操作數(shù)棧和work方法的局部變量表有數(shù)據(jù)共享。