多多色-多人伦交性欧美在线观看-多人伦精品一区二区三区视频-多色视频-免费黄色视屏网站-免费黄色在线

國內最全IT社區平臺 聯系我們 | 收藏本站
阿里云優惠2
您當前位置:首頁 > php開源 > php教程 > JVM理解其實并不難!

JVM理解其實并不難!

來源:程序員人生   發布時間:2016-06-04 15:22:24 閱讀次數:2886次

我的簡書同步發布:JVM理解其實其實不難!

在瀏覽本文之前,先向大家強烈推薦1下周志明的《深入理解Java虛擬機》這本書。

前些天面試了阿里的實習生,問到關于Dalvik虛擬性能不能履行class文件,我當時的回答是不能,但是它履行的是class轉換的dex文件。當面試官繼續問,為何不能履行class文件時,我卻只能回答Dalvik虛擬機內部的優化緣由,卻不能正確回答具體的緣由。其實周志明的這本書就有回答:Dakvik其實不是1個Java虛擬機,它沒有遵守Java虛擬機規范,不能履行Java的class文件,使用的是寄存器架構而不是JVM中常見的棧架構,但是它與Java又有著千絲萬縷的關系,它履行的dex文件可以通過class文件轉化而來

其實在本科期間,就有接觸過《深入理解Java虛擬機》,但是1直以來都沒去仔細研讀,現在回頭想一想實在是覺得惋惜!研1期間花了很多時間研讀,現在準備找工作了,發現好多內容看了又忘。索性寫1篇文章,把這本書的知識點做1個總結。固然了,如果你想看比較詳細的內容,可以翻看《深入理解Java虛擬機》。

JVM內存區域

我們在編寫程序時,常常會遇到OOM(out of Memory)和內存泄漏等問題。為了不出現這些問題,我們首先必須對JVM的內存劃分有個具體的認識。JVM將內存主要劃分為:方法區、虛擬機棧、本地方法棧、堆、程序計數器。JVM運行時數據區以下:
JVM運行時數據區

程序計數器

程序計數器是線程私有的區域,很好理解嘛~,每一個線程固然得有個計數器記錄當前履行到那個指令。占用的內存空間小,可以把它看成是當前線程所履行的字節碼的行號唆使器。如果線程在履行Java方法,這個計數器記錄的是正在履行的虛擬機字節碼指令地址;如果履行的是Native方法,這個計數器的值為空(Undefined)。此內存區域是唯逐一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域

Java虛擬機棧

與程序計數器1樣,Java虛擬機棧也是線程私有的。其生命周期與線程相同。如何理解虛擬機棧呢?本質上來說,就是個棧。里面寄存的元素叫棧幀,棧幀好像很復雜的模樣,其實它很簡單!它里面寄存的是1個函數的上下文,具體寄存的是履行的函數的1些數據。履行的函數需要的數據不過就是局部變量表(保存函數內部的變量)、操作數棧(履行引擎計算時需要),方法出口等等。

履行引擎每調用1個函數時,就為這個函數創建1個棧幀,并加入虛擬機棧。換個角度理解,每一個函數從調用到履行結束,實際上是對應1個棧幀的入棧和出棧。

注意這個區域可能出現的兩種異常:1種是StackOverflowError,當前線程要求的棧深度大于虛擬機所允許的深度時,會拋出這個異常。制造這類異常很簡單:將1個函數反復遞歸自己,終究會出現棧溢出毛病(StackOverflowError)。另外一種異常是OutOfMemoryError異常,當虛擬機棧可以動態擴大時(當前大部份虛擬機都可以),如果沒法申請足夠多的內存就會拋出OutOfMemoryError,如何制作虛擬機棧OOM呢,參考1下代碼:

public void stackLeakByThread(){ while(true){ new Thread(){ public void run(){ while(true){ } } }.start() } }

這段代碼有風險,可能會致使操作系統假死,請謹慎使用~~~

本地方法棧

本地方法棧與虛擬機棧所發揮的作用很類似,他們的區分在于虛擬機棧為履行Java代碼方法服務,而本地方法棧是為Native方法服務。與虛擬機棧1樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

Java堆

Java堆可以說是虛擬機中最大1塊內存了。它是所有線程所同享的內存區域,幾近所有的實例對象都是在這塊區域中寄存。固然,睡著JIT編譯器的發展,所有對象在堆上分配漸漸變得不那末“絕對”了。

Java堆是垃圾搜集器管理的主要區域。由于現在的搜集器基本上采取的都是分代搜集算法,所有Java堆可以細分為:新生代和老年代。在細致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。當堆沒法再擴大時,會拋出OutOfMemoryError異常。

方法區

方法區寄存的是類信息、常量、靜態變量等。方法區是各個線程同享區域,很容易理解,我們在寫Java代碼時,每一個線程度可以訪問同1個類的靜態變量對象。由于使用反射機制的緣由,虛擬機很難推測那個類信息不再使用,因此這塊區域的回收很難。另外,對這塊區域主要是針對常量池回收,值得注意的是JDK1.7已把常量池轉移到堆里面了。一樣,當方法區沒法滿足內存分配需求時,會拋出OutOfMemoryError。
制造方法區內存溢出,注意,必須在JDK1.6及之前版本才會致使方法區溢出,緣由后面解釋,履行之前,可以把虛擬機的參數-XXpermSize和-XX:MaxPermSize限制方法區大小。

List<String> list =new ArrayList<String>(); int i =0; while(true){ list.add(String.valueOf(i).intern()); }

運行后會拋出java.lang.OutOfMemoryError:PermGen space異常。
解釋1下,Stringintern()函數作用是如果當前的字符串在常量池中不存在,則放入到常量池中。上面的代碼不斷將字符串添加到常量池,終究肯定會致使內存不足,拋出方法區的OOM。

下面解釋1下,為何必須將上面的代碼在JDK1.6之前運行。我們前面提到,JDK1.7后,把常量池放入到堆空間中,這致使intern()函數的功能不同,具體怎樣個不同法,且看看下面代碼:

String str1 =new StringBuilder("hua").append("chao").toString(); System.out.println(str1.intern()==str1); String str2=new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern()==str2);

這段代碼在JDK1.6和JDK1.7運行的結果不同。JDK1.6結果是:false,false ,JDK1.7結果是true, false。緣由是:JDK1.6中,intern()方法會吧首次遇到的字符串實例復制到常量池中,返回的也是常量池中的字符串的援用,而StringBuilder創建的字符串實例是在堆上面,所以必定不是同1個援用,返回false。在JDK1.7中,intern不再復制實例,常量池中只保存首次出現的實例的援用,因此intern()返回的援用和由StringBuilder創建的字符串實例是同1個。為何對str2比較返回的是false呢?這是由于,JVM中內部在加載類的時候,就已有"java"這個字符串,不符合“首次出現”的原則,因此返回false

垃圾回收(GC)

JVM的垃圾回收機制中,判斷1個對象是不是死亡,其實不是根據是不是還有對象對其有援用,而是通過可達性分析。對象之間的援用可以抽象成樹形結構,通過樹根(GC Roots)作為出發點,從這些樹根往下搜索,搜索走過的鏈稱為援用鏈,當1個對象到GC Roots沒有任何援用鏈相連時,則證明這個對象是不可用的,該對象會被判定為可回收的對象。

那末那些對象可作為GC Roots呢?主要有以下幾種:

1.虛擬機棧(棧幀中的本地變量表)中援用的對象。
2.方法區中類靜態屬性援用的對象。
3.方法區中常量援用的對象
4.本地方法棧中JNI(即1般說的Native方法)援用的對象。

另外,Java還提供了軟援用和弱援用,這兩個援用是可以隨時被虛擬機回收的對象,我們將1些比較占內存但是又可能后面用的對象,比如Bitmap對象,可以聲明為軟援用貨弱援用。但是注意1點,每次使用這個對象時候,需要顯示判斷1下是不是為null,以避免出錯。

3種常見的垃圾搜集算法

1.標記-清除算法

首先,通過可達性分析將可回收的對象進行標記,標記后再統1回收所有被標記的對象,標記進程其實就是可達性分析的進程。這類方法有2個不足點:效力問題,標記和清除兩個進程的效力都不高;另外一個是空間問題,標記清除以后會產生大量的不連續的內存碎片。

2.復制算法

為了解決效力問題,復制算法是將內存分為大小相同的兩塊,每次只使用其中1塊。當這塊內存用完了,就將還存活的對象復制到另外一塊內存上面。然后再把已使用過的內存1次清算掉。這使得每次只對半個區域進行垃圾回收,內存分配時也不用斟酌內存碎片情況。

但是,這代價實在是讓人沒法接受,需要犧牲1般的內存空間。研究發現,大部份對象都是“朝生夕死”,所以不需要安裝1:1比例劃份內存空間,而是將內存分為1塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和1塊Survivor空間,默許比例為Eden:Survivor=8:1.新生代區域就是這么劃分,每次實例在Eden和1塊Survivor中分配,回收時,將存活的對象復制到剩下的另外一塊Survivor。這樣只有10%的內存會被浪費,但是帶來的效力卻很高。當剩下的Survivor內存不足時,可以去老年代內存進行分配擔保。如何理解分配擔保呢,其實就是,內存不足時,去老年代內存空間分配,然后等新生代內存緩過來了以后,把內存歸還給老年代,保持新生代中的Eden:Survivor=8:1.另外,兩個Survivor分別有自己的名稱:From Survivor、To Survivor。2者身份常常調換,即有時這塊內存與Eden1起參與分配,有時是另外一塊。由于他們之間常常相互復制。

3.標記-整理算法

標記整理算法很簡單,就是先標記需要回收的對象,然后把所有存活的對象移動到內存的1端。這樣的好處是避免了內存碎片。

類加載機制

類從被加載到虛擬機內存開始,到卸載出內存為止,全部生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。

其中加載、驗證、準備、初始化、和卸載這5個階段的順序是肯定的。而解析階段不1定:它在某些情況下可以在初始化階段以后再開始,這是為了支持Java的運行時綁定。

關于初始化:JVM規范明確規定,有且只有5中情況必須履行對類的初始化(加載、驗證、準備自然再此之前要產生):
1.遇到new、getstatic、putstatic、invokestatic,如果類沒有初始化,則必須初始化,這幾條指令分別是指:new新對象、讀取靜態變量、設置靜態變量,調用靜態函數。
2.使用java.lang.reflect包的方法對類進行反射調用時,如果類沒初始化,則需要初始化
3.當初始化1個類時,如果發現父類沒有初始化,則需要先觸發父類初始化。
4.當虛擬機啟動時,用戶需要制定1個履行的主類(包括main函數的類),虛擬機會先初始化這個類。
5.但是用JDK1.7啟的動態語言支持時,如果1個MethodHandle實例最后解析的結果是REF_getStaticREF_putStaticRef_invokeStatic的方法句柄時,并且這個方法句柄所對應的類沒有進行初始化,則要先觸發其初始化。

另外要注意的是:通過子類來援用父類的靜態字段,不會致使子類初始化

public class SuperClass{ public static int value=123; static{ System.out.printLn("SuperClass init!"); } } public class SubClass extends SuperClass{ static{ System.out.println("SubClass init!"); } } public class Test{ public static void main(String[] args){ System.out.println(SubClass.value); } }

最后只會打印:SuperClass init!
對應靜態變量,只有直接定義這個字段的類才會被初始化,因此通過子類類援用父類中定義的靜態變量只會觸發父類初始化而不會觸發子類初始化。

通過數組定義來援用類,不會觸發此類的初始化

public class Test{ public static void main(String[] args){ SuperClass[] sca=new SuperClass[10]; } }

常量會在編譯階段存入調用者的常量池,本質上并沒有直接援用到定義常量的類,因此不會觸發定義常量的類初始化,示例代碼以下:

public class ConstClass{ public static final String HELLO_WORLD="hello world"; static { System.out.println("ConstClass init!"); } } public class Test{ public static void main(String[] args){ System.out.print(ConstClass.HELLO_WORLD); } }

上面代碼不會出現ConstClass init!

加載

加載進程主要做以下3件事
1.通過1個類的全限定名稱來獲得此類的2進制流
2.強這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
3.在內存中生成1個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據訪問入口。

驗證

這個階段主要是為了確保Class文件字節流中包括信息符合當前虛擬機的要求,并且不會出現危害虛擬機本身的安全。

準備

準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都在方法區中分配。首先,這個時候分配內存僅僅包括類變量(被static修飾的變量),而不包括實例變量。實例變量會在對象實例化時隨著對象1起分配在java堆中。其次這里所說的初始值“通常情況下”是數據類型的零值,假定1個類變量定義為

public static int value=123;

那變量value在準備階段后的初始值是0,而不是123,由于還沒有履行任何Java方法,而把value賦值為123是在程序編譯后,寄存在類構造函數<clinit>()方法中。

解析

解析階段是把虛擬機中常量池的符號援用替換為直接援用的進程。

初始化

類初始化時類加載的最后1步,前面類加載進程中,除加載階段用戶可以通過自定義類加載器參與之外,其余動作都是虛擬機主導和控制。到了初始化階段,才是真正履行類中定義Java程序代碼。

準備階段中,變量已賦過1次系統要求的初始值,而在初始化階段,根據程序員通進程序制定的主觀計劃初始化類變量。初始化進程實際上是履行類構造器<clinit>()方法的進程。

<clinit>()方法是由編譯器自動搜集類中所有類變量的賦值動作和靜態語句塊中的語句合并產生的。搜集的順序是依照語句在源文件中出現的順序。靜態語句塊中只能訪問定義在靜態語句塊之前的變量,定義在它以后的變量可以賦值,但不能訪問。以下所示:

public class Test{ static{ i=0;//給變量賦值,可以通過編譯 System.out.print(i);//這句編譯器會提示:“非法向前援用” } static int i=1; }

<clinit>()方法與類構造函數(或說實例構造器<init>())不同,他不需要顯式地調用父類構造器,虛擬機會保證子類的<clinit>()方法履行之前,父類的<clinit>()已履行終了。

類加載器

關于自定義類加載器,和雙親委派模型,這里不再提,寫了幾個小時了,該洗洗睡了~

生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
程序員人生
------分隔線----------------------------
分享到:
------分隔線----------------------------
關閉
程序員人生
主站蜘蛛池模板: 日本做爰免费大片视频 | 天天视频官网天天视频在线 | 双性h啪啪樱桃动漫直接观看 | 最近中文字幕免费高清版7 最近中文字幕免费国语 | 亚洲欧美日韩精品一区 | 中文字幕天天躁夜夜狠狠综合 | 国内精品一级毛片免费看 | 免费的禁片 | 亚洲福利一区二区 | 91久久色 | 99爱视频在线观看 | freesex呦交孩 | 性色网站 | 欧美极品videosvideo激情 欧美极品video粗暴 | 欧美激情伦妇在线观看 | 亚洲图片综合网 | 欧美不卡在线视频 | 亚洲欧美一区二区三区 | 国产男女爽爽爽爽爽免费视频 | 国产精品日韩欧美 | 日韩城人网站 | 国产福利视频一区二区三区 | 日韩精品亚洲人成在线播放 | 亚洲精品第1页 | 欧美一区二区丝袜高跟鞋 | 亚洲视频在线a视频 | 欧美精品国产精品 | 亚洲国产片在线观看 | 免费h视频在线观看 | 一本大道香蕉大无线视频 | 亚洲激情中文字幕 | 国产欧美成人不卡视频 | 欧美一区亚洲二区 | 一区在线看 | 中文国产| 欧美日本一区二区 | 亚洲黄色大全 | 亚洲成a人片在线观看中文!!! | 亚洲精品国产三级在线观看 | 欧美人与性动交α欧美精品 | 久久精品精品 |