android複習路之java虛擬機

JVM所管理的內存分爲以下幾個運行時數據區:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區。

程序計數器(Program Counter Register)
一塊較小的內存空間,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。
當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是Native方法(調用本地操作系統方法)時,該計數器的值爲空。另外,該內存區域是唯一一個在Java虛擬機規範中麼有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。
Java虛擬機棧(Java Virtual Machine Stacks)
該區域也是線程私有的,它的生命週期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用於支持續虛擬機進行方法調用和方法執行的數據結構。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
本地方法棧(Native Method Stacks)
該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲使用到的本地操作系統(Native)方法服務。
Java堆(Java Heap)
Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域。幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱爲“GC堆”。
根據Java虛擬機規範的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,並且堆也無法擴展時,將會拋出OutOfMemoryError異常。
方法區(Method Area)
方法區也是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區域又被稱爲“永久代”,但這僅僅對於Sun HotSpot來講,JRockit和IBM J9虛擬機中並不存在永久代的概念。Java虛擬機規範把方法區描述爲Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的內存,可以選擇固定大小或可擴展,另外,虛擬機規範允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域比較少出現。該區域的內存回收目標主要針是對廢棄常量的和無用類的回收。運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Class文件常量池),用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。運行時常量池相對於Class文件常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class文件中的常量池的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。
根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
內存泄漏和內存溢出的差別
內存泄露是指分配出去的內存沒有被回收回來,由於失去了對該內存區域的控制,因而造成了資源的浪費。Java中一般不會產生內存泄露,因爲有垃圾回收器自動回收垃圾,但這也不絕對,當我們new了對象,並保存了其引用,但是後面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成內存泄露,
內存溢出是指程序所需要的內存超出了系統所能分配的內存(包括動態擴展)的上限。



 GC回收器有哪些?

1 Serial收集器

1.1 Serial收集器運行過程
從名字可以看出,這個收集器是一個單線程的收集器。但是,它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是,在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
"Stop The World"是在用戶不可見的情況下,把用戶正常工作的線程全部停掉,對於"Stop The World"給用戶帶來的不良體驗,但從JDK1.3開始,HotSpot虛擬機開發團隊爲消除或減少工作線程因內存回收而導致停頓的努力一直進行着。從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成功Garbage First(G1)收集器,用戶線程停頓時間不短縮短,但是仍然無法完全消除!

1.2 Serial收集器應用場景
雖然Serial收集器看起來“老而無用、食之無味棄之可惜”,但實際上到目前爲止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它有着優於其他收集器的地方:簡單高效(與其他收集器的單線程比)。
對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得更高的單線程收集效率。
在用戶的桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆新生代(僅僅是新生代使用的內存,桌面應用基本上不會再大了),停頓時間完全可以控制在幾十毫秒最多一百毫秒以內,只要不是頻繁發生,這點停頓還是可以接受的,所以Serial收集器對應運行Client模式下的虛擬機來說是一個很好的選擇。

2 ParNew 收集器
2.1 ParNew 收集器運行過程
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲包括Serial收集器可用的所有控制參數(例如:-XX:SruvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在現實上,這兩種收集器也共用了相當多代碼。

1.2 ParNew收集器應用場景
ParNew收集器除了多線程收集之外,其他與Serial收集器相比並沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器。其中一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果。甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現兩個CPU環境中都不能百分百地保證可以超越Serial收集器。當然,隨着CPU數量增加,它對於GC時,系統資源的有效利用還是很有好處。它默認開啓的收集線程數與CPU數量相同,在CPU非常多(例如32個,現在CPU動輒就4核加超線程,服務器超過32個邏輯CPU的情況越來越多了)環境下,可以使用-XX:ParalleGCThreads參數來限制垃圾收集的線程數。
3 Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器....看上去和ParNew都一樣,那它有啥特別的地方呢?
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能第縮短垃圾收集時用戶線程停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂的吞吐量就是:
CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即:吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
虛擬機總共運行100分鐘,其中垃圾收集化掉1分鐘,那吞吐量就是99%。
4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,它主要還有兩大用途:
在JDK1.5以及之前版本中與Parallel Scavenge收集器搭配使用
作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器在JDK1.6中才開始提供。

6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目的的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統服務器上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶最好的體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對前面幾種收集器來說更復雜一些,整個過程分爲4個步驟:
初始標記
併發標記
重新標記
併發清除
其中,初始標記、重新標記着兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。併發標記階段就是進行GC Roots Tracing的過程。而重新標記階段則是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間會比初始標記稍長一些,但遠比並發標記時間短。

CMS是一款優秀的收集器,它的主要優點從名字上體現出來:併發收集、低停頓。但是CMS還遠達不到完美程度,它有以下3個明顯的缺點:
CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。
CMS收集器無法處理浮動垃圾,可能出現Concurrent Mode Failure失敗而導致另一次Full GC產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法再當次收集中處理掉它們,治好留待下一次GC時再清理掉。這部分垃圾就稱爲“浮動垃圾”。因此,CMS不能像其他收集器那樣等到老年代幾乎完全被填滿再進行手機,CMS需要預留一部分空間。
由於CMS基於“標記-清除”算法,意味着收集結束時會有大量空間碎片產生。
7 G1 收集器
G1(Garbage First)收集器是當今收集器技術發展的最前沿成果之一。G1是面向服務端應用的垃圾收集器,與其他GC收集器相比,G1具備如下特點:
並行與併發:充分利用多CPU、多核環境下的硬件優勢,使用多個CPU(CPU或CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器需要停頓Java線程執行的GC動作,G1仍然能通過併發方式讓Java程序繼續執行。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保存。
空間整合:與CMS的“標記-清理”算法不同,G1從整體上看是基於“標記-整理”算法實現的收集器,從局部上看是基於“複製”算法實現的,這兩種算法意味着G1運作期間不會產生內存空間碎片。
可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得從超出N毫秒,這幾乎已經是實時Java的垃圾收集器的特徵了。
G1運作大致可劃分爲以下幾個步驟:
初始標記
併發標記
最終標記
篩選回收


類與類加載器

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名空間。簡單說:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義
否則,即使兩個類來源於同一個Class文件,被同一個虛擬機加載,只要他們的類加載器不同,那這兩個類就必定不等。這裏指的“相等”,包括代表Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,而包括使用instanceof關鍵字做對象所屬關係判定等情況。


雙親委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
啓動類加載器,負責將存放在\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即時放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被java程序直接引用。
擴展類加載器:負責加載\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用該類加載器。
應用程序類加載器:負責加載用戶路徑上所指定的類庫,開發者可以直接使用這個類加載器,也是默認的類加載器。 三種加載器的關係:啓動類加載器->擴展類加載器->應用程序類加載器->自定義類加載器。
這種關係即爲類加載器的雙親委派模型。其要求除啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不以繼承關係實現,而是用組合的方式來複用父類的代碼。
雙親委派模型的工作過程:如果一個類加載器接收到了類加載的請求,它首先把這個請求委託給他的父類加載器去完成,每個層次的類加載器都是如此,因此所有的加載請求都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它在搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。
好處:java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名爲java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行爲也無法保證,應用程序也會變得一片混亂。
實現:在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父加載失敗,則拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章