Java虛擬機器
java虛擬機器
Java支援平台無關性、安全性和網路移動性。而Java平台由Java虛擬機器和Java核心類所構成,它為純Java程式提供了統一的編程介面,而不管下層作業系統是什麼。正是得益於Java虛擬機器,它號稱的“一次編譯,到處運行”才能有所保障。
java虛擬機器主要完成了幾大任務:
將java檔案編譯為.class檔案、解析並執行.class,藉助他,使得java能夠實現平台無關的特性。 實現了自動記憶體管理,記憶體回收的存在使得編程時無需關注記憶體的分配與釋放。一、記憶體地區
JVM對記憶體進行了清晰的劃分。
從程式執行講起
為了更清楚地瞭解記憶體地區的各個地區之間的關聯性,我們從一個程式的執行開始講起。
1、要執行一個程式,首先虛擬機器要對其進行解析,將程式轉化為虛擬機器可識別的格式,將一些類、編譯後的代碼、常亮等放在方法區當中。
2、一個程式是從main()方法中開始的,我們都知道,main()方法其實對應了一個線程,所以,要執行一個方法首先要建立起一個線程,所以將有了虛擬機器棧,在方法調用時通過出棧、入棧的方式進行控制。同時,為了確保在多個線程切換的時候,程式能夠回到正確的位置繼續執行,每個線程都分配了對應的程式計數器。有虛擬機器棧,對於java的本地方法,也有對應的本地方法棧,用於管理如wait、signal等一些本地方法。
3、當程式執行時,建立一個對象時,則會在堆中為其分配記憶體,而在棧中儲存對應的引用指向堆中的對象執行個體。
另外,還有常量池,用於存放程式中的常量。直接記憶體,作為獨立於虛擬機器運行時資料區的一塊記憶體,使得一些本地方法可以在堆外直接使用記憶體。
運行時的資料區
以上提及的方法區、java虛擬機器棧、程式計數器、本地方法棧、堆、常量池,都屬於jvm的運行時的資料區。
展示了抽象的虛擬機器的內部體繫結構。
二、記憶體回收
Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的“高牆” , 牆外面的人想進去,牆裡面的人卻想出來。這就是記憶體回收,記憶體回收並非java創造,最早的記憶體回收是在神一樣的Lisp語言中產生的。
java中,所有的對象都是分配在堆中進行統一管理的。相較於各個線程獨自開闢空間儲存物件,採取這種統一管理的方式的好處,我的理解是:統一的管理有利於充分利用記憶體,也便於管理,當進行記憶體回收時,由於空閑地區是共用的,所以每個線程都可以使用,做到了空間使用的最大化。
要對記憶體空間進行動態管理,自動進行記憶體回收。要解決以下問題:
1、哪些對象需要回收?
2、如何高效的進行記憶體回收?
3、什麼時候進行回收?
下面,就從對這幾個問題展開。
1、哪些對象需要回收?
要回收,當然要保證當對象不會再被程式任何地方使用,這些對象便能夠進行回收了。即這個對象不再“存活”。
要判斷一個對象是否還存活,主要有
1)引用計數法,這種方法通過為每一個對象添加一個引用計數器,每當有一個地方引用它時,就加一,當引用時效時,就減一。這種方式簡單方便,但無法解決循環參考的問題。
2)可達性分析法。這是目前各個語言主流的實現方式。他通過一系列的GCRoots對象作為起點向下搜尋,若沒有任何路徑能夠到達某個對象,則認為這個對象不可達,即不再“存活”的對象。能夠作為GCRoots的對象有:虛擬機器棧中的對象、方法區中的靜態屬性的對象、方法區的常量對象、本地方法棧中引用的對象。其他存活的對象都能夠通過這些對象的引用鏈到達。
2、如何高效的進行記憶體回收?
找到了程式中不再存活的對象,下一步便是清理對象以釋放記憶體空間。主要有兩類的方案來處理:將需要回收的對象清除,或者類似Windows磁碟整理的方式,將有用的對象整理後釋放掉其他的空間。所以,目前有以下幾種記憶體回收的機制:
1)標記清除。
標記需要回收的對象,釋放記憶體。這種方案方便快速但會產生大量的記憶體片段。
2)標記-整理演算法。
標記需要回收的對象,然後相Windows進行磁碟整理一樣,將存活的對象向一端移動。這種方式不會產生記憶體片段,但效率就低了。如果存活對象數量很多的情況下,就悲劇了。
3)複製演算法。
將記憶體劃分為兩塊等大小的地區,每次使用一塊,進行GC時,將存活的資料複製到另一塊地區中。這種方式方便且不會產生記憶體片段,就是利用率太低了。考慮到在實際情況中,大量的對象都是“朝生夕死”的,所以,升級版的複製演算法將記憶體劃分為Eden和Survivor(8:1),Eden用於正常的對象分配使用,當進行回收時,將存活的對象複製到Survivor區中,這樣,記憶體的使用率達到了80%,頓時好多了。但如果在一次GC時,存貨的對象很多,Survivor區不夠用時怎麼辦?則需要進行分配擔保了,請度娘。
4)分代收集
將java的堆分為老年代和新生代。對於新生代,採用複製演算法。而對於老年代,則使用標記-清除或者標記-整理的方法。
3、什麼時候進行回收?
java中,有System.gc()的方法,但該方法只是建議JVM進行記憶體回收,最終的決定權由JVM控制。
當新生代Eden的空間無法進行新對象的分配時,VM會進行一次Minor GC。
新生代GC( Minor GC) : 指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性, 所以Minor GC非常頻繁, 一般回收速度也比較快。
老年代GC( Major GC/Full GC) : 指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC( 但非絕對的, 在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程) 。 Major GC的速度一般會比Minor GC慢10倍以上。
三、類載入機制
一個java類在編寫完成後,首先要被編譯成.class檔案。然後,需要通過類載入器將.class檔案裝載到虛擬機器中,才能夠運行。將.class檔案的位元組流轉換為虛擬機器能夠識別的格式(轉化為java.lang.Class類的一個執行個體)的過程,即解析二進位流並載入記憶體的行為,就是類載入器的行為。
為什麼要有類載入器?
java為了實現平台無關性,將所有的類都編譯成.class檔案,為瞭解析.class類檔案,於是,類載入器的出現就變得自然而然了。只是這個名字聽起來比較進階了點,在我看來就是檔案解析器。
而類載入器重要的一點是,他是在程式執行的時候對類進行載入的,這就使得對類的操作變得更加靈活。比如,在程式執行時可以通過網路傳輸一個.class檔案,載入到類中執行;可以在程式執行時為類動態添加一些行為(ioc)…
類載入器做了哪些事?
如:
1、 載入:尋找並載入類的位元據。
2、 連結:
驗證:確保被載入類的正確性。
準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
解析:把類中的符號引用轉換為直接引用。
3、 初始化:為類的靜態變數賦予正確的初始值。
類載入器的雙親委派模型
這個我認為是類載入器很重要的一個概念。雖然實現上很簡單,只是幾個繼承的關係。
來看個圖:
最上層的BootStrap類載入器是個本地方法,用於載入<JAVA_HOME>\lib中運行時必須的特定類庫,比如rt.jar。
ExtClassLoader,用於載入<JAVA_HOME>\lib\ext中的擴充類。
APPClassLoader,應用程式類載入器,負責載入使用者指定類路徑上的jar檔案,開發人員可以通過getSystemClassLoader()方法擷取。
類載入器之間的這種層次關係, 稱為類載入器的雙親委派模型(Parents DelegationModel) 。 這種方式,下級的類載入器要載入一個類時,會先委託上級的類進行載入,若上級的類載入器無法載入時,才由下級的類進行處理。(當然, 你也可以完全拋開這個規範,將所有的類都由自訂的類載入器進行載入。)遵循雙親委派模型的好處是,最頂層的類是jre必須的類,如Object類,這樣,即便使用者自訂了一個Object類,也不會被載入(因為最頂層仍會載入系統最基礎的類),即可以保證基礎類庫不會被覆蓋。
自訂類載入器的應用情境
首先,我們需要清楚,自訂類載入器的目的是為了在讀取.class檔案之後,進行一些自訂的操作,再載入成對應的類。
需要加密傳輸.class檔案時,對.class檔案加密後,系統預設的類載入器便無法正確識別檔案的內容,所以,需要自訂類載入器對其解密之後,再交由應用程式類加實現載器進行載入。 cglib、asm包中,對類進行添加方法等方法,是需要對.class的位元組流進行修改後,再交由類載入器進行載入。 OSGI,為了實現各個模組之間的可見度,通過為每個模組建立對應的類載入器得以實現。四、對並發的支援
JVM為什麼會講到並發呢?因為多線程。多線程如何??多線程間如何解決資料競爭的問題?這些都是虛擬機器需要考慮的事項。
1、虛擬機器對多線程的支援
Java虛擬機器規範中,定義了一套Java記憶體模型,使其在各個平台下的訪問都能夠達到一致的記憶體訪問效果,在此之前,主流程式語言( 如C/C++等) 直接使用物理硬體和作業系統的記憶體模型,因此, 會由於不同平台上記憶體模型的差異,有可能導致程式在一套平台上並發完全正常, 而在另外一套平台上並發訪問卻經常出錯,因此在某些情境就必須針對不同的平台來編寫程式。
java的記憶體模型
各個線程中,都有屬於自己的工作記憶體,同時,還有各個線程間共用的主記憶體。所有的變數都儲存在主記憶體中,線程需要通過特定的操作(read、load)將變數的副本拷貝到工作記憶體中,才能夠使用,換句話說,線程只能直接存取自己的工作記憶體。<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPtTaamF2YdDpxOK7+rnmt7bW0KOstqjS5cHLOLj2stnX96OsttSx5MG/vfjQ0LLZ1/ejujwvcD4NCjxwPmxvY2ujqCDL+Laoo6kgo7rX99PD09rW98TatOa1xLHkwb+jrMv8sNHSu7j2seTBv7HqyrbOqtK7zPXP37PMtsDVvLXE17TMrKGjPC9wPg0KPHA+dW5sb2Nro6ggveLL+KOpIKO61/fTw9Pa1vfE2rTmtcSx5MG/o6zL/LDR0ru49rSm09rL+Lao17TMrLXEseTBv8rNt8Wz9sC0o6wgys23xbrztcSx5MG/ssW/ydLUsbvG5Mv7z9+zzMv4tqihozwvcD4NCjxwPnJlYWSjqCC2wciho6kgo7og1/fTw9Pa1vfE2rTmtcSx5MG/o6zL/LDR0ru49rHkwb+1xNa1tNPW98TatOa0q8rktb3P37PMtcS5pNf3xNq05tbQo6wg0tSx48vmuvO1xGxvYWS2r9f3yrnTw6GjPC9wPg0KPHA+bG9hZKOoINTYyOujqSCjuiDX99PD09q5pNf3xNq05rXEseTBv6Osy/yw0XJlYWSy2df3tNPW98TatObW0LXDtb21xLHkwb/WtbfFyOu5pNf3xNq05rXEseTBv7ixsb7W0KGjPC9wPg0KPHA+dXNlo6ggyrnTw6OpIKO6INf308PT2rmk1/fE2rTmtcSx5MG/o6zL/LDRuaTX98TatObW0NK7uPax5MG/tcTWtbSrtd24+Na00NDS/cfmo6wgw7+1sdDpxOK7+tP2tb3Su7j20OjSqsq508O1vbHkwb+1xNa1tcTX1r3awuvWuMHuyrG9q7vh1rTQ0NXiuPay2df3oaM8L3A+DQo8cD5hc3NpZ26jqCC4s9a1o6kgo7og1/fTw9PauaTX98TatOa1xLHkwb+jrMv8sNHSu7j2tNPWtNDQ0v3H5r3TytW1vbXE1rW4s7j4uaTX98TatOa1xLHkwb+jrCDDv7Wx0OnE4rv60/a1vdK7uPa4+LHkwb+4s9a1tcTX1r3awuvWuMHuyrHWtNDQ1eK49rLZ1/ehozwvcD4NCjxwPnN0b3Jlo6ggtOa0oqOpIKO6INf308PT2rmk1/fE2rTmtcSx5MG/o6zL/LDRuaTX98TatObW0NK7uPax5MG/tcTWtbSry821vdb3xNq05tbQo6wg0tSx48vmuvO1xHdyaXRlstk8YnIgLz4NCtf3yrnTw6GjPC9wPg0Kd3JpdGWjqCDQtMjro6kgo7og1/fTw9Pa1vfE2rTmtcSx5MG/o6zL/LDRc3RvcmWy2df3tNO5pNf3xNq05tbQtcO1vbXEseTBv7XE1rW3xcjr1vfE2rTmtcSx5MG/1tChow0KPHA+we3N4qOsuea2qMHLvfjQ0NXi0Kmy2df3tcS55re2o6zS1LGj1qTK/b7dstnX97XE1f3It9DUoaM8L3A+DQo8aDQgaWQ9"多線程的實現">多線程的實現
多線程的實現,與其說是由虛擬機器負責,不如說與作業系統更相關。我們都知道,實現線程有三種方式:
使用核心線程實現。
這種實現方式直接由作業系統核心控制的線程來實現。使用者往往使用核心線程的一種進階介面——輕量級進程(LWP),即我們通常意義上的線程,來實現,一個程式中的線程對應核心線程。
這種方式實現上簡單,但各種調用都都需要切換到核心態進行操作,所以系統調用產生的,切換的代價比較高。
使用使用者線程實現
完全建立在使用者空間上的線程,可以理解為使用一個核心態的線程類比出多線程的效果。這種方式靈活性高,且不需要切換到核心態進行操作,大大減少了開銷。只是,由於使用者線程的底層仍是一個線程,若底層的核心線程掛起,會導致基於該核心線程的所有使用者線程都被無條件掛起。並且,實現難度很大。
使用使用者線程+輕量級線程實現
這種方式中,有別於純粹的使用者線程的將所有建立的線程對應到一個核心線程的方式,他將N個使用者線程映射到M個核心線程中,這樣,既保留了使用者線程高度靈活、操作代價低的特質,又避免了由於核心線程阻塞而導致所有使用者線程都被阻塞的風險。
2、安全執行緒
Java中,安全執行緒由強到弱分為5類。不可變、絕對安全執行緒、 相對安全執行緒、 線程相容和線程對立。
不可變。這是最簡單的控制方法,final對象由於狀態不會變化,所有不存在不安全的因素。如java中的String類型 絕對安全執行緒。不管運行時環境如何,調用者都不需要任何額外的同步措施,將能夠保證安全執行緒。實際上,大部分類都無法保證到這一點。 相對安全執行緒。它需要保證對這個對象單獨的操作是安全執行緒的, 我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。 我們目前使用的大多數安全執行緒的類都是這樣的,比如vector的get、add、remove這些單獨的操作都是安全執行緒的,但若需要連續操作,比如getSize之後,然後get所有的元素輸出,就需要外部單獨的同步保證資料的正確性(否則,可能在半途資料被remove而導致擷取的資料量不正確)。 線程相容。如我們熟悉的ArrayList和HashMap本身是線程不安全的,但我們可以通過外部同步手段對其安全地使用 線程對立。線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中並發使用的代碼。這種情況比較少見。一個線程對立的例子是Thread類的suspend()和resume()方法,如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢複線程,如果並發進行的話,無論調用時是否進行了同步,目標線程都是存在死結風險的,如果suspend()中斷的線程就是即將要執行resume()的那個線程,那就肯定要產生死結了。安全執行緒實現方法
1、互斥同步
這種方式,主要通過synchronized關鍵字對某些操作進行同步,若某個線程在synchronized的臨界區內,其他任何進入要進入臨界區的線程都會阻塞、進入等待隊列。
2、非阻塞同步
與上一中線程會阻塞進入等待隊列的方式相比,考慮到線程的狀態切換是有較大代價,而很多情況下線程只需等待很短的時間將能夠獲得鎖,引入了樂觀的並發機制——非阻塞同步。在進入同步塊時,若無法獲得鎖,便不斷while迴圈嘗試獲得鎖,直到成功。自旋鎖便能夠符合這種情形,並且,你可以設定進行自旋的次數,超過該次數,則線程掛起等待。而JDK1.6引入的更進階的自適應自旋鎖能夠自動根據前一個鎖自旋及擁有鎖的時間來設定自旋的次數。so smart!