淺談Java的虛擬機結(jié)構(gòu)以及虛擬機內(nèi)存的優(yōu)化
工作以來,代碼越寫越多,程序也越來越臃腫,效率越來越低,對于我這樣一個追求完美的程序員來說,這是絕對不被允許的,于是除了不斷優(yōu)化程序結(jié)構(gòu)外,內(nèi)存優(yōu)化和性能調(diào)優(yōu)就成了我慣用的“伎倆”。
要對Java程序進行內(nèi)存優(yōu)化和性能調(diào)優(yōu),不了解虛擬機的內(nèi)部原理(或者叫規(guī)范更嚴謹一點)是肯定不行的,這里推薦一本好書《深入Java虛擬機(第二版)》(Bill Venners著,曹曉剛 蔣靖 譯,實際上本文正是作者閱讀本書之后,對Java虛擬機的個人理解闡述)。當然了,了解Java虛擬機的好處并不僅限于上述兩點好處。從更深一點的技術(shù)層面上看,了解Java虛擬機的規(guī)范和實現(xiàn),將更加有助于我們編寫高效、穩(wěn)定的Java代碼。比如,假如了解Java虛擬機的內(nèi)存模型,了解虛擬機的內(nèi)存回收機制,那么我們就不會過分依賴它,而會在需要的時候顯式的”釋放內(nèi)存”(Java代碼不能顯式釋放內(nèi)存,但是可以通過釋放對象引用告知垃圾回收器回收該對象需要被回收),以降低不必要的內(nèi)存消耗;假如我們了解Java棧的工作原理,那么我們就可以通過減少遞歸層數(shù),減少循環(huán)次數(shù)來降低堆棧溢出的風險??赡軐τ趹?yīng)用開發(fā)人員來說,可能不會直接去涉及這些Java虛擬機底層實現(xiàn)的工作,但是了解這些背景知識,或多或少,都會對我們寫的程序產(chǎn)生潛移默化的好的影響。
本篇文章,將簡明扼要的說明Java虛擬機的體系結(jié)構(gòu)和內(nèi)存模型,如有用詞不妥或解釋不準確之處,請不吝指正,深感榮幸!
Java 虛擬機體系結(jié)構(gòu)

類裝載子系統(tǒng)
Java虛擬機有兩種類裝載器,分別是啟動類裝載器和用戶自定義裝載器。
通類裝載子系統(tǒng)通過類的全限定名(包名和類名,網(wǎng)絡(luò)裝載還包括 URL)將 Class 裝載進運行時數(shù)據(jù)區(qū)。對于每一個被裝載的類型,Java虛擬機都會創(chuàng)建一個java.lang.Class類的實例來代表該類型,該實例被放在內(nèi)存中的堆區(qū),而裝載的類型信息則位于方法區(qū),這一點和所有其他對象都是一樣的。
類裝載子系統(tǒng)在裝載一個類型前,除了要定位和導入對應(yīng)的二進制class文件外,還要驗證導入類的正確性,為類變量分配并初始化內(nèi)存,以及解析符號引用為直接引用,這些動作嚴格按照以下順序進行:
1)裝載——查找并裝載類型的二進制數(shù)據(jù);
2)連接——執(zhí)行驗證,準備以及解析(可選)
3)驗證 確保被導入類型的正確性
4)準備 為類變量分配內(nèi)存,并將其初始化為默認值
5)解析 把類型中的符號引用轉(zhuǎn)換為直接應(yīng)用
方法區(qū)
對于每一個被類裝載子系統(tǒng)裝載的類型,虛擬機都會保存下列數(shù)據(jù)到方法區(qū):
1.類型的全限定名
2.類型超類的全限定名(java.lang.Object沒有超類)
3.類型是類類型還是接口類型
4.類型的訪問修飾符
5.任何直接超接口的全限定名有序列表
除了上述基本類型信息,還將保存如下信息:
6.類型的常量池
7.字段信息(包括字段名、字段類型、字段修飾符)
8.方法信息(包括方法名、返回類型、參數(shù)的數(shù)量和類型、方法修飾符,如果方法不是抽象和本地的,還將保存方法的字節(jié)碼、操作數(shù)棧和該方法棧幀中的局部變量區(qū)的大小和異常表)
9.常量以外的所有類變量(其實就是類的靜態(tài)變量,因為靜態(tài)變量是所有實例共享的,且與類型直接相關(guān),所以他們是類一級的變量,作為類的成員被保存在方法區(qū))
10.一個到類ClassLoader的引用
//返回的就是剛才保存的ClassLoader引用 String.class.getClassLoader(); 一個到Class類的引用 //將返回剛才保存的Class類的引用 String.class;
注意,方法區(qū)也是可以被垃圾回收器回收的。
堆
Java程序在運行時創(chuàng)建的所有類實例或數(shù)組都放在同一個堆中,而每一個Java虛擬機也是有一個堆空間,所有線程共享一個堆(這就是一個多線程的Java程序會產(chǎn)生對象訪問的同步問題的原因了)。
由于每一種Java虛擬機都有對虛擬機規(guī)范的不同實現(xiàn),所以我們可能不知道每一種Java虛擬機在堆中是以何種形式表示對象實例的,不過我們可以通過下面這可能的實現(xiàn)來一窺端倪:

程序計數(shù)器
對于運行中的Java程序而言,每一個線程都有自己的PC(程序計數(shù)器)寄存器,它是在該線程啟動時創(chuàng)建的,大小為一個字長,用來保存需要被執(zhí)行的下一行代碼的位置。
Java棧
每一個線程都有一個Java棧,以棧幀為單位保存線程的運行狀態(tài)。虛擬機對Java棧的操作有兩種:壓棧和出棧,二者都已幀為單位。棧幀保存了傳入?yún)?shù)、局部變量、中間運算結(jié)果等數(shù)據(jù),在方法完成時被彈出,然后釋放。
看一下兩個局部變量相加時棧幀的內(nèi)存快照

本地方法棧
這是 Java 調(diào)用操作系統(tǒng)本地庫的地方,用來實現(xiàn) JNI(Java Native Interface,Java 本地接口)
執(zhí)行引擎
Java虛擬機的核心,控制裝入 Java 字節(jié)碼并解析;對于運行中的Java程序而言,每一個線程都是一個獨立的虛擬機執(zhí)行引擎的實例,從線程生命周期的開始到結(jié)束,他要么在執(zhí)行字節(jié)碼,要么在執(zhí)行本地方法。
本地接口
連接了本地方法棧和操作系統(tǒng)庫。
注:文中所有提到”Java虛擬機”的地方都是指”JavaEE和JavaSE平臺的Java虛擬機規(guī)范”。
虛擬機內(nèi)存優(yōu)化實踐
既然提到內(nèi)存,就不得不說到內(nèi)存泄露。眾所周知,Java是從C++的基礎(chǔ)上發(fā)展而來的,而C++程序的很大的一個問題就是內(nèi)存泄露難以解決,盡管Java的JVM有一套自己的垃圾回收機制來回收內(nèi)存,在許多情況下并不需要java程序開發(fā)人員操太多的心,但也是存在泄露問題的,只是比C++小一點。比如說,程序中存在被引用但無用的對象:程序引用了該對象,但后續(xù)不會或者不能再使用它,那么它占用的內(nèi)存空間就浪費了。
我們先來看看GC是如何工作的:監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,當該對象不再被引用時,釋放對象(GC本文的重點,不做過多闡述)。很多Java程序員過分依賴GC,但問題的關(guān)鍵是無論JVM的垃圾回收機制做得多好,內(nèi)存總歸是有限的資源,因此就算GC會為我們完成了大部分的垃圾回收,但適當?shù)刈⒁饩幋a過程中的內(nèi)存優(yōu)化還是很必要的。這樣可以有效的減少GC次數(shù),同時提升內(nèi)存利用率,最大限度地提高程序的效率。
總體而言,Java虛擬機的內(nèi)存優(yōu)化應(yīng)從兩方面著手:Java虛擬機和Java應(yīng)用程序。前者指根據(jù)應(yīng)用程序的設(shè)計通過虛擬機參數(shù)控制虛擬機邏輯內(nèi)存分區(qū)的大小以使虛擬機的內(nèi)存與程序?qū)?nèi)存的需求相得益彰;后者指優(yōu)化程序算法,降低GC負擔,提高GC回收成功率。
通過參數(shù)優(yōu)化虛擬機內(nèi)存的參數(shù)如下所示:
Xms
初始Heap大小
Xmx
java heap最大值
Xmn
young generation的heap大小
Xss
每個線程的Stack大小
上面是三個比較常用的參數(shù),還有一些:
XX:MinHeapFreeRatio=40
Minimum percentage of heap free after GC to avoid expansion.
XX:MaxHeapFreeRatio=70
Maximum percentage of heap free after GC to avoid shrinking.
XX:NewRatio=2
Ratio of new/old generation sizes. [Sparc -client:8; x86 -server:8; x86 -client:12.]-client:8 (1.3.1+), x86:12]
XX:NewSize=2.125m
Default size of new generation (in bytes) [5.0 and newer: 64 bit VMs are scaled 30% larger; x86:1m; x86, 5.0 and older: 640k]
XX:MaxNewSize=
Maximum size of new generation (in bytes). Since 1.4, MaxNewSize is computed as a function of NewRatio.
XX:SurvivorRatio=25
Ratio of eden/survivor space size [Solaris amd64: 6; Sparc in 1.3.1: 25; other Solaris platforms in 5.0 and earlier: 32]
XX:PermSize=
Initial size of permanent generation
XX:MaxPermSize=64m
Size of the Permanent Generation. [5.0 and newer: 64 bit VMs are scaled 30% larger; 1.4 amd64: 96m; 1.3.1 -client: 32m.]
下面所說通過優(yōu)化程序算法來提高內(nèi)存利用率,并降低內(nèi)存風險,完全是經(jīng)驗之談,僅供參考,如有不妥,請指正,謝謝!
1.盡早釋放無用對象的引用(XX = null;)
看一段代碼:
public List<PageData> parse(HtmlPage page) {
List<PageData> list = null;
try {
List valueList = page.getByXPath(config.getContentXpath());
if (valueList == null || valueList.isEmpty()) {
return list;
}
//需要時才創(chuàng)建對象,節(jié)省內(nèi)存,提高效率
list = new ArrayList<PageData>();
PageData pageData = new PageData();
StringBuilder value = new StringBuilder();
for (int i = 0; i < valueList.size(); i++) {
HtmlElement content = (HtmlElement) valueList.get(i);
DomNodeList<HtmlElement> imgs = content.getElementsByTagName("img");
if (imgs != null && !imgs.isEmpty()) {
for (HtmlElement img : imgs) {
try {
HtmlImage image = (HtmlImage) img;
String path = image.getSrcAttribute();
String format = path.substring(path.lastIndexOf("."), path.length());
String localPath = "D:/images/" + MD5Helper.md5(path).replace("\\", ",").replace("/", ",") + format;
File localFile = new File(localPath);
if (!localFile.exists()) {
localFile.createNewFile();
image.saveAs(localFile);
}
image.setAttribute("src", "file:///" + localPath);
localFile = null;
image = null;
img = null;
} catch (Exception e) {
}
}
//這個對象以后不會在使用了,清除對其的引用,等同于提前告知GC,該對象可以回收了
imgs = null;
}
String text = content.asXml();
value.append(text).append("<br/>");
valueList=null;
content = null;
text = null;
}
pageData.setContent(value.toString());
pageData.setCharset(page.getPageEncoding());
list.add(pageData);
//這里 pageData=null; 是沒用的,因為list仍然持有該對象的引用,GC不會回收它
value=null;
//這里可不能 list=null; 因為list是方法的返回值,否則你從該方法中得到的返回值永遠為空,而且這種錯誤不易被發(fā)現(xiàn)、排除
} catch (Exception e) {
}
return list;
}
2.謹慎使用集合數(shù)據(jù)類型,如數(shù)組,樹,圖,鏈表等數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)對GC來說回收更復雜。
3.避免顯式申請數(shù)組空間,不得不顯式申請時,盡量準確估計其合理值。
4.盡量避免在類的默認構(gòu)造器中創(chuàng)建、初始化大量的對象,防止在調(diào)用其自類的構(gòu)造器時造成不必要的內(nèi)存資源浪費
5.盡量避免強制系統(tǒng)做垃圾內(nèi)存的回收,增長系統(tǒng)做垃圾回收的最終時間
6.盡量做遠程方法調(diào)用類應(yīng)用開發(fā)時使用瞬間值變量,除非遠程調(diào)用端需要獲取該瞬間值變量的值。
7.盡量在合適的場景下使用對象池技術(shù)以提高系統(tǒng)性能
相關(guān)文章
將本地jar包安裝進入maven倉庫(實現(xiàn)方法)
下面小編就為大家?guī)硪黄獙⒈镜豭ar包安裝進入maven倉庫(實現(xiàn)方法)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06
Springboot項目打war包docker包找不到resource下靜態(tài)資源的解決方案
今天小編就為大家分享一篇關(guān)于Springboot項目打war包docker包找不到resource下靜態(tài)資源的解決方案,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
詳解Java高并發(fā)編程之AtomicReference
此篇文章主要介紹了AtomicReference的出現(xiàn)背景,AtomicReference的使用場景,以及介紹了AtomicReference的源碼,重點方法的源碼分析2021-06-06
springboot中rabbitmq實現(xiàn)消息可靠性機制詳解
這篇文章主要介紹了springboot中rabbitmq實現(xiàn)消息可靠性機制詳解,本文通過實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2021-09-09

