第2章 垃圾收集器與記憶體配置設定政策
1.java虛拟機在執行java程式時會把它所管理的記憶體會分為若幹個不同的資料區域,
這些區域都有各自的用途,以及建立和銷毀的時間,有的區域随着虛拟機程序的啟動而存在,
有些區域則是在以來使用者線程的啟動和結束而建立和銷毀。
根據《java虛拟機規範》,包括以下幾個運作時資料區域:
//此處應有類圖,但是畫起來太麻煩!
程式計數器(program counter register)
方法區(method area)
虛拟機棧(vm stack)
本地方法棧(native method stack)
堆(heap)
2.pcr 程式計數器
//在作業系統中學習過program counter,程式計數器是用于存放下一條指令所在單元的位址的地方。
在虛拟機中,可以看做是目前線程所執行的位元組碼的行号訓示器。位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉等基礎功能都需要依賴計數器完成。
為了多線程切換過程中能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間的計數器互不影響,獨立存儲,成為“線程私有”的記憶體。并且,程式計數器是唯一一個在java虛拟機規範中沒有規定任何outofmemory情況的區域。
3.java虛拟機棧 java virtual machine stacks
與程式計數器一樣,java虛拟機棧也是線程私有的,生命周期和線程相同。
虛拟機棧描述的是java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,用于存儲局部變量表,操作棧,動态連結,方法出口等資訊。
//每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中從入棧到出站的過程。
虛拟機棧會抛出 stackoverflowerror 和 outofmemoryerror異常。
4.本地方法棧 native method stacks
虛拟機棧為虛拟機執行java方法服務,而本地方法棧則是為虛拟機使用到的native方法服務。
5.java堆
java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此區域的唯一目的就是 存放對象執行個體。
java堆是垃圾收集器管理的主要區域,很多時候被稱為“gc堆(garbage collected heap)”。
如果在堆中沒有記憶體完成執行個體配置設定,并且無法繼續擴充時,會抛出outofmemoryerror異常。
6.方法區
方法區和java堆一樣,是各個線程共享的記憶體區域,用于存儲已被虛拟機加載的類資訊,常量,靜态變量,即時編譯器編譯後的代碼等資料,
//運作時常量池,是方法區的一部分。
7.直接記憶體 diret memory
直接記憶體并不是java虛拟機規範中定義的記憶體區域,但是頻繁使用,也可能導緻outofmemoryerror異常出現。
nio的部分操作,使用native函數庫直接配置設定堆外記憶體,然後通過一個存儲在java堆裡面的directbytebuffer對象,作為這塊記憶體的引用進行操作,避免了在java堆和native堆中來回複制資料,一些場景中可以顯著提高性能。
8.主流的對象通路方式,使用句柄和直接指針。
使用句柄通路方式的好處就是reference中存儲的是穩定的句柄位址,在對象被移動時隻會改變句柄中的執行個體資料指針,而reference本身不需要被修改。
使用指針通路速度更快,節省了一次指針定位的時間開銷。
第3章 虛拟機性能監控與故障處理
//了解gc和記憶體配置設定有哪些意義?
當需要排查各種記憶體溢出、記憶體洩露問題時,當垃圾收內建為系統達到更高并發量的瓶頸時,就需要對這些“自動化”的技術實施必要的監控和調節。
1.引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;引用失效時,計數器值減1‘任何時刻計數器都為零的對象就是不可能再被使用的。
但是,引用計數法有一個很大的不足,很難解決對象之間的互相循環引用問題。
下面的代碼描述了引用計數法的一個情景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<code>public</code> <code>class</code> <code>referencecountgc {</code>
<code> </code><code>public</code> <code>object instance=</code><code>null</code><code>;</code>
<code> </code><code>private</code> <code>static</code> <code>final</code> <code>int</code> <code>_1mb=</code><code>1024</code><code>*</code><code>1024</code><code>;</code>
<code> </code><code>private</code> <code>byte</code><code>[] bigsize=</code><code>new</code> <code>byte</code><code>[</code><code>2</code><code>*_1mb];</code>
<code> </code><code>public</code> <code>static</code> <code>void</code> <code>testgc(){</code>
<code> </code><code>referencecountgc obja=</code><code>new</code> <code>referencecountgc();</code>
<code> </code><code>referencecountgc objb=</code><code>new</code> <code>referencecountgc();</code>
<code> </code><code>obja.instance=objb;</code>
<code> </code><code>objb.instance=obja;</code>
<code> </code><code>obja=</code><code>null</code><code>;</code>
<code> </code><code>objb=</code><code>null</code><code>;</code>
<code> </code><code>}</code>
<code>}</code>
如果單純的通過引用計數法來判斷對象的狀态,因為互相循環引用,這兩個對象都不會被gc回收。
2.根搜尋算法
通過一系列的名為“gc roots”的對象作為起始點,從這些節點開始向下搜尋,當到達不在gc roots引用鍊上的對象時,即使它們之間互相關聯,仍然判定為可回收對象。
用圖論的理論,可以表示為gc roots到該對象不可達。
3.更好的了解“引用”
java中的引用,最開始的定義很傳統:如果reference類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表着一個引用。
這樣的定義在垃圾收集時太過絕對,後來對引用的概念進行了擴充,将引用分為強引用,軟引用,若引用,虛引用,四種引用強度逐漸減弱。
隻要強引用還存在,gc永遠不會回收這些被引用的對象,其他的引用類型,垃圾收集器會根據情況進行對象回收。
4.不可達對象的“自我拯救”
在根搜尋方法過程中,真正宣告一個對象死亡,至少經曆兩次标記過程:如果對象在進行根檢索後發現沒有與gc roots相連接配接的引用鍊,那它将會被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。
5.回收方法區
6.垃圾收集算法
//标記-清除算法
//複制算法
複制算法将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,把還存活着的對象複制到另一塊記憶體上,然後把使用過的記憶體空間一次清理掉。實作簡單,運作高效。隻是空間上将記憶體縮小為原來的一半,代價較高。
目前商業虛拟機中較多的使用複制算法來回收新生代,并且不需要按照1:1的比例劃分記憶體空間。而是将記憶體分為一塊較大的eden空間和兩塊較小的survior空間,每次使用eden和其中的一塊survivor。
比如hotspot虛拟機預設eden和survivor的大小比例是8:1,也就是每次新生代中可用記憶體為整個新生代容量的90%。

//标記-整理算法
複制收集算法在對象存活率較高時需要執行較多的複制操作,效率變低。
//分代收集算法
根據對象的存活周期的不同将記憶體劃分為幾塊。一般是把java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最适當的手機算法。新生代時,每次垃圾收集都發現有大批對象死去,隻有少量存活,那就選用複制算法。老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用“标記-清理”或“标記-整理”算法來進行回收。
7.垃圾收集器
//垃圾收集器可以搭配使用。
8.記憶體配置設定與回收政策
8.1 對象優先在eden配置設定
大多數情況下,對象在新生代eden區中配置設定。當eden區沒有足夠的空間進行配置設定時,虛拟機将發起一次minir gc。
虛拟機提供了
-xx:+printgcdetails 這個收集器日志參數。
-xx:survivorratio=8 設定新生代中eden區與survivor區的比例是8:1。
8.2 大對象直接進入老年代
大對象指,需要大量連續記憶體空間的java對象,典型的比如很長的字元串及數組。
8.3 長期存活的對象将進入老年代
采用分代收集的思想來管理記憶體,必須有個規則來确定哪些對象放在新生代,哪些對象放在老年代中,虛拟機給每個對象指定了一個對象年齡計數器。
8.4 動态對象年齡判定
為了能更好的适應不同程式的記憶體狀況,虛拟機在分代放置對象時,并不總是固定的,并不總是要求對象的年齡達到maxtenuringthreshold才能進入老年代。
8.5 空間配置設定擔保
//學習了虛拟機記憶體配置設定與回收技術的理論,還要掌握如何在實際工作中應用。
//介紹了随jdk釋出的6個指令行工具和2個可視化的故障處理工具。
1.jdk的指令行工具
jdk的bin目錄下有許多工具,可以用于監視虛拟機和故障處理。
很多小工具的命名都類似unix指令,比如jps,功能和linux的ps相似。
常用的jdk指令行工具:
jps/jinfo/jmap/jhat/jstack
jstat:虛拟機統計資訊監視工具
2.jdk的可視化工具
jconsole:java監視與管理控制台
visualivm:多合一故障處理工具
//分享幾個比較有代表性的實際案例。
1.高性能硬體上的程式部署政策
在高性能硬體上部署程式,主要有兩種方式:
通過64位jdk來使用大記憶體;
使用若幹個32位虛拟機建立;邏輯叢集來利用硬體資源。
對于使用者互動性強,對停頓時間敏感的系統,給虛拟機配置設定超大堆的前提是有把握把應用程式的full gc頻率控制得足夠低,不能影響使用者使用。
控制full gc頻率的關鍵是看應用中絕大多數對象能否符合"朝生夕滅"的原則,即大多數對象的生存時間不應當太長,尤其是不能産生成批量的、長生存時間的大對象,這樣才能保障老年代空間的穩定。
在使用64位jdk管理記憶體時,還需要考慮下面可能面臨的問題:
記憶體回收導緻的長時間停頓;
性能可能普遍較低;
因為指針膨脹及資料類型對齊補白等因素,相同的程式在64位jdk中消耗的記憶體一般比32位jdk大。
2.叢集間同步導緻的記憶體溢出
3.堆外記憶體導緻的溢出錯誤
//大量使用nio操作會占用很多堆外記憶體。
下面是一些實踐經驗,除了java堆和永久代之外的記憶體:
direct memory:可通過-xx:maxdirectmemorysize 調整大小,記憶體不足時抛出outofmemoryerror
或者outofmemoryerror:direct buffer memory。
線程堆棧:可通過-xss調整大小,記憶體不足時抛出stackoverflowerror(縱向無法配置設定,即無法配置設定新的棧幀)
socket緩存區:每個socket連接配接都receive和send兩個緩存區,分别占大約37kb和25kb的記憶體。
jni代碼:
虛拟機和gc:
4.外部指令導緻系統緩慢
5.伺服器jvm程序崩潰
6.實戰:eclipse運作速度調優
第6章 類檔案結構
//實作語言無關性的基礎仍然是虛拟機和位元組碼存儲格式,使用java編譯器可以把java代碼編譯為存儲位元組碼的class檔案,使用jruby等其他語言的編譯器一樣可以把程式代碼編譯成class檔案,虛拟機并不關心class的來源是什麼語言,隻要它符合class檔案應有的結構就可以在java虛拟機中運作。
1.class類檔案的結構
class檔案是一組以8位位元組為基礎機關的二進制流,各個資料項目嚴格按照順序緊湊的排列在class檔案之中,中間沒有分隔符。
class檔案格式采用一種類似于c語言結構體的僞結構來存儲,隻有兩種資料類型:無符号數和表。
每個class檔案的頭4個位元組成為魔數,唯一作用是用于确定這個檔案是否為一個能被虛拟機接受的class檔案。
主次版本号之後的是常量池入口,常量池是class檔案結構中與其他項目關聯最多的資料類型。主要存放兩大類常量,即字面量(literal)和符号引用(symbolic references)。
第7章 虛拟機類加載機制
//代碼編譯的結果是從本地機器碼轉變為位元組碼,是存儲格式發展的一小步,卻是程式設計語言發展的一大步。
在class檔案中描述的各種資訊,被加載到虛拟機中之後才能被運作和使用。而虛拟機是如何加載class檔案,class檔案中的資訊進入到虛拟機後會發生什麼變化?
虛拟機讀取class檔案,把描述類的資料從class檔案加載到記憶體中,并對資料進行校驗,轉換解析和初始化,然後形成可以被虛拟機直接使用操作的java類型,即虛拟機的類加載機制。
//java中天生可以動态擴充的語言特性就是依賴運作期動态加載和動态連接配接這個特點實作的。
1.類加載的時機
類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止。整個生命周期包括七個階段。
加載、驗證、準備、解析、初始化、使用和解除安裝七個階段。
驗證、準備和解析統稱為連接配接(linking)。
2.類加載的過程
2.1 加載階段
加載階段,特别是加載時擷取類的二進制位元組流的動作,是開發期可控性最強的階段,因為既可以使用系統的類加載器,也可以自定義類加載器實作,并且通過自定義類加載器控制位元組流的擷取方式。
2.2 驗證階段
確定class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。
2.3 準備階段
正式為類變量配置設定記憶體并設定類變量初始值,這些記憶體都将在方法區中進行配置設定。
2.4 解析階段
虛拟機将常量池内的符号引用替換為直接引用的過程。
2.5 初始化階段
初始化階段,才真正開始執行類中定義的java程式代碼,即位元組碼。
3.類加載器
//類加載階段的"通過一個類的全限定名來擷取描述此類的二進制位元組流"這個動作被放到虛拟機外部實作,
//即應用程式自己控制擷取,實作這個動作的代碼子產品被稱為"類加載器"。
類加載器在類層次劃分,osgi,熱部署,代碼加密等領域都應用到。
3.1 類與類加載器
3.2 雙親委派模型
從虛拟機的角度,存在兩種不同的類加載器:一種是啟動類加載器(bootstrap classloader),這個類使用c++語言實作,是虛拟機自身的一部分,另一種是所有其他的類加載器,全部繼承自抽象類java.lang.classloader。
雙親委派模型的工作過程,即一個類加載器收到類加載的請求,首先會委派給父類加載器去完成,所有的加載請求都傳送到頂層的啟動類加載器,如果父類加載器回報無法完成加載請求,會繼續交由子類加載。
這樣加載的好處是java類随着它的類加載器擁有了帶優先級的層次關系,避免了出現一個類加載多次。
第8章 虛拟機位元組碼執行引擎
1.運作時棧幀結構
2.方法調用
3.基于棧的位元組碼解釋執行引擎
第9章 類加載及執行子系統的案例與實戰
//在class檔案格式與執行引擎這部分裡,主要是由虛拟機直接控制的行為,能通過程式進行操作的,主要是位元組碼生成與類加載器這兩部分。
1.案例分析
tomcat:正統的類加載器結構
主流的java web伺服器,如tomcat、weblogic等都實作了自己定義的類加載器。
一個功能完備的web伺服器,要解決下面的問題:
部署在同一個伺服器上的多個web應用程式使用的java類庫既可以實作互相隔離,又可以互相共享。