程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java虛擬機

Java虛擬機

編輯:JAVA綜合教程

Java虛擬機


java虛擬機

Java支持平台無關性、安全性和網絡移動性。而Java平台由Java虛擬機和Java核心類所構成,它為純Java程序提供了統一的編程接口,而不管下層操作系統是什麼。正是得益於Java虛擬機,它號稱的“一次編譯,到處運行”才能有所保障。

java虛擬機主要完成了幾大任務:

將java文件編譯為.class文件、解析並執行.class,借助他,使得java能夠實現平台無關的特性。 實現了自動內存管理,垃圾回收的存在使得編程時無需關注內存的分配與釋放。

一、內存區域

JVM對內存進行了清晰的劃分。

從程序執行講起

為了更清楚地了解內存區域的各個區域之間的關聯性,我們從一個程序的執行開始講起。

1、要執行一個程序,首先虛擬機要對其進行解析,將程序轉化為虛擬機可識別的格式,將一些類、編譯後的代碼、常亮等放在方法區當中。

2、一個程序是從main()方法中開始的,我們都知道,main()方法其實對應了一個線程,所以,要執行一個方法首先要建立起一個線程,所以將有了虛擬機棧,在方法調用時通過出棧、入棧的方式進行控制。同時,為了確保在多個線程切換的時候,程序能夠回到正確的位置繼續執行,每個線程都分配了對應的程序計數器。有虛擬機棧,對於java的本地方法,也有對應的本地方法棧,用於管理如wait、signal等一些本地方法。

3、當程序執行時,新建一個對象時,則會在中為其分配內存,而在棧中存儲對應的引用指向堆中的對象實例。

另外,還有常量池,用於存放程序中的常量。直接內存,作為獨立於虛擬機運行時數據區的一塊內存,使得一些本地方法可以在堆外直接使用內存。

運行時的數據區

以上提及的方法區、java虛擬機棧、程序計數器、本地方法棧、堆、常量池,都屬於jvm的運行時的數據區。

下圖展示了抽象的虛擬機的內部體系結構。

image

二、垃圾回收

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)…

類加載器做了哪些事?

如下圖:
image

1、 加載:查找並加載類的二進制數據。

2、 鏈接:
驗證:確保被加載類的正確性。
准備:為類的靜態變量分配內存,並將其初始化為默認值;
解析:把類中的符號引用轉換為直接引用。

3、 初始化:為類的靜態變量賦予正確的初始值。

類加載器的雙親委派模型

這個我認為是類加載器很重要的一個概念。雖然實現上很簡單,只是幾個繼承的關系。

來看個圖:

image

最上層的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的內存模型

image

各個線程中,都有屬於自己的工作內存,同時,還有各個線程間共享的主內存。所有的變量都存儲在主內存中,線程需要通過特定的操作(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"多線程的實現">多線程的實現

多線程的實現,與其說是由虛擬機負責,不如說與操作系統更相關。我們都知道,實現線程有三種方式:

使用內核線程實現。

image

這種實現方式直接由操作系統內核控制的線程來實現。用戶往往使用內核線程的一種高級接口——輕量級進程(LWP),即我們通常意義上的線程,來實現,一個程序中的線程對應內核線程。

這種方式實現上簡單,但各種調用都都需要切換到內核態進行操作,所以系統調用產生的,切換的代價比較高。

使用用戶線程實現

image

完全建立在用戶空間上的線程,可以理解為使用一個內核態的線程模擬出多線程的效果。這種方式靈活性高,且不需要切換到內核態進行操作,大大減少了開銷。只是,由於用戶線程的底層仍是一個線程,若底層的內核線程掛起,會導致基於該內核線程的所有用戶線程都被無條件掛起。並且,實現難度很大。

使用用戶線程+輕量級線程實現

image

這種方式中,有別於純粹的用戶線程的將所有創建的線程對應到一個內核線程的方式,他將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!

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved