《LINUX入門:《深入理解Java虛擬機》 讀書筆記》要點:
本文介紹了LINUX入門:《深入理解Java虛擬機》 讀書筆記,希望對您有用。如果有疑問,可以聯系我們。
Java虛擬機運行時數據區
?
Java創建對象,在語言層面上使用new關鍵字.虛擬機遇到new關鍵字時,會檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經被加載、解析和初始化過.如果沒有,那就必須先執行類加載過程.類加載通過之后,虛擬機將會為新生對象分配內存.對象所需的內存在類加載完成后就能完全確定.分配內存的辦法有“指針碰撞”和“空閑列表”兩種方式,如果Java堆是規整的,則采用前者;否則,采用后者.Java堆是否規則和虛擬機有關.
深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版 高清PDF+源碼?
在虛擬機中,能夠發生OOM的有:虛擬機棧,本地辦法棧,Java堆,辦法區,運行時常量.分析OOM使用eclipse memory analyzer插件或者JProfiler工具.添加jvm參數(-XX:+PrintGCDetails)可以打印出GC的詳細日志,-Xloggc:gc.log 日志文件的輸出路徑.
幾乎所有的對象的視力都在這里分配內存.通過參數-Xmx(JVM最大可用內存)和-Xms(JVM初始可用內存)控制對的大小.如果在堆中沒有內存完成實例分配并且堆無法擴展,就會OOM.棧-Xss設置棧的容量大小.HotSpot不區分本地辦法棧和虛擬機棧.如果虛擬機在申請棧擴展時,沒有足夠的空間,則會OOM.
判斷對象是否存活,兩種方式:引用計數法和可達性分析.
引用計數法,不能辦理對象相互引用的情況,所以主流虛擬機都采用的是可達性分析.
可達性分析采用采用GCROOTS策略,可作為GCROOTS的有:
?
虛擬機 | 新生代 | 老年代 | 垃圾算法 | 備注 |
---|---|---|---|---|
Serial | ? | ? | 復制算法 | ? |
ParNew | ? | ? | 復制算法 | ? |
Parallel Scavenge | ? | ? | 復制算法 | ? |
Serial Old | ? | ? | 標志-整理算法 | ? |
Parallel Old | ? | ? | 標志-整理算法 | ? |
CMS | ? | ? | 標志-清除算法 | ? |
G1 | ? | ? | 綜合算法 | 目前最先進的垃圾回收器 |
下面是一次GC產生的日志(jvm參數-XX:+PrintGCDetails):
從日志中可以看到,System.gc()表示程序調用了System.gc()辦法觸發了垃圾回收,[PSYoungGen:3932k->592k(76288k)]中PSYoungGen代表了GC發生的區域,這個名稱和GC收集器密切相關:如果顯示的是[DefNew,則表明是Serial收集器的新生代;如果是ParNew則表明是ParNew收集器的新生代;我們這里是PSYoungGen,則表明是Parallel Scavenge收集器的新生代.再看后面是數字,3932k->592k(76288k)代表的是:GC前該內存已經使用的容量->GC后該內存區域使用的容量(該內存區域總容量).3932K->600K(251392K)表示GC前Java堆已經使用的容量->GC后Java堆已經使用的容量(Java堆總容量).后面的時間表示GC所花費的時間,單位s.?
大多數情況下,對象首先會被分配到Eden區,當Eden區滿了,會觸發一次Minor GC.
所謂的大對象是指,必要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象).虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配.這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存復制(復習一下:新生代采用復制算法收集內存).
對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到必定程度(默認為15歲),就將會被晉升到老年代中.對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置.
虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須比及MaxTenuringThreshold中要求的年齡.
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是平安的.如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗.如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的,如果擔保失敗則會進行一次Full GC;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC.
jps格式:jps [options] [hostid]
功能:列出正在執行的虛擬機進程,并顯示虛擬機執行主類(Main Class,main()函數所在的類)名稱以及這些進程的當地虛擬機唯一ID(Local Virtual Machine Identifier,LVMID).
選項 | 作用 |
---|---|
-q | 只輸出LVMID,省略主類的名稱 |
-m | 輸出虛擬機進程啟動時傳給主類main()的參數 |
-l | 輸出主類的全名,如果主類執行的是jar,則輸出jar的路徑 |
-v | 輸出虛擬機進程啟動時傳給jvm的參數 |
功能:它可以顯示當地或者遠程[1]虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據.
命令格式:jstat [option vmid [ interval [s|ms] [count] ] ],參數interval和count代表查詢間隔和次數.
其中,如果進程是當地虛擬機進程,則vmid與lvmid一致;如果是遠程虛擬機進程,則vmid格式為:[protocol:][//]lvmid[@hostname[:port]/servername]
舉例:
S0代表Survivor0;S1代表Survivor1;E代表Eden區;O代表Old區;M代表Metaspace元數據區域(JDK1.8用Metaspace代替了1.7以前的PermGen永久代);CCS表示的是NoKlass Metaspace的使用率也便是CCSU,/CCSC算出來的,具體可參看這里,總之在1.7之前,M所在的位置是P,表示永久代使用的比例;YGC代表程序運行以來共發生Minor GC;YGCT表示其所用的時間;FGC代表程序運行以來共發生Full GC;FGCT表示其所用的時間;GCT表示GC總共花費了所長時間.
格式:jinfo [option] pid
作用:實時的查看和調整java虛擬機各項參數.
格式:jmap [option] vmid
作用:用于生成堆轉儲快照(一般稱為heapdump或dump文件).
選項 | 作用 |
---|---|
-dump | 生成Java堆轉儲快照.格式:-dump:[live,] format -b,file=<filename>,其中live字參數說明是否只dump存活的對象. |
-heap | 顯示Java堆詳細信息,只在Linux/Solaris平臺下有效 |
-histo | 顯示Java堆中對象統計信息 |
-F | 當虛擬機對-dump選項沒有響應時,該參數可以強制生成dump快照.只在Linux/Solaris平臺下有效 |
作用:配合jmap命令,分析dump文件.jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果可以在瀏覽器中查看.
命令:jhat dumpfile
一般使用專業的工具進行分析,例如:VisualVM,Eclipse Memory Analyzer,IBM HeapAnalyzer等.
格式:jstack [option] vmid
作用:用于生成虛擬機當前時刻的線程快照,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、哀求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因.線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后臺做些什么事情,或者等待著什么資源.
任何語言,不局限于Java,都可以在虛擬機上運行.Java虛擬機不care .class文件來自何種語言.
Class文件是一組以8位字節為基??單位的二進制流,各個數據項目嚴格依照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符
根據Java虛擬機規范規定,Class文件中采用類似C語言結構體中的偽結構體來存儲數據,這種結構體中只存在兩種數據結構:無符號數和表.
每個Class文件的頭4個字節被稱為魔數,它的唯一作用是確定這個文件是否為一個能被虛擬機接受的Class文件.Class文件的魔數為:0xCAFFEEBABE.緊接著魔數的4個字節存儲的是Class文件的版本號:第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version).Java的版本號是從45開始的,JDK 1.1之后的每個JDK大版本發布主版本號向上加1(JDK 1.0~1.1使用了45.0~45.3的版本號),高版本的JDK能向下兼容以前版本的Class文件,但不能運行以后版本的Class文件,即使文件格式并未發生任何變化,虛擬機也必須拒絕執行超過其版本號的Class文件.?
緊接著主版本號后面的是常量池入口.
更多詳情見請繼續閱讀下一頁的精彩內容:
_baidu_page_break_tag_?
上圖所示,類加載過程包括:加載,驗證,準備,解析,初始化,使用,卸載.其中加載,驗證,準備,初始化,卸載這5個階段的順序是確定.而解析階段不一定,某些時候可以在初始化之后才進行解析,這是為了支持Java中動態綁定.什么時候開始類加載的一個過程:加載?虛擬機規范中沒有嚴格的約束,交給虛擬機的實現去具體把握.但是對于初始化,虛擬機規范中嚴格規定了5中場景必須進行初始化:
以上5種場景稱為主動引用,會首先進行初始化,除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用.
被動引用例子一:
1 public class SuperClass { 2 3 public static int value = 123; 4 5 static{ 6 System.out.println("SuperClass init !"); 7 } 8 } 9 10 public class SubClass extends SuperClass { 11 12 static{ 13 System.out.println("SubClass init !"); 14 } 15 } 16 17 public class TestInit { 18 public static void main(String[] args) { 19 System.out.println(SubClass.value); 20 } 21 }
以上執行的結果如下:
為什么只會打印出"SuperClass init !"?因為對于靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態,只會觸發父類的初始化而不會觸發子類的初始化.
被動引用例子二:
public class ConstClass { static { System.out.println("ConstClass Init ~~"); } public final static String HELLO_STRING = "HelloString"; } public class TestConst{ public static void main(String[] args) { System.out.println(ConstClass.HELLO_STRING); } }
以上執行結果也沒有輸出ConstClass Init ~~,原因是常量在編譯階段會存入調用類的常量池中,本色上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化.
“加載”是“類加載”過程中的一個階段.在加載階段,虛擬機會做3件事:
這三點并不具體,例如第一點,Java虛擬機規范并沒有指定二進制字節流要從哪一個Class文件中獲取,怎么去獲取.所以Java可以從:
驗證是連接階段的第一步.目的是確保Class文件的字節流中包含的信息不會危害到虛擬機自身的平安.包含:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證.
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在辦法區中進行分配.其中初始值“通常情況下”是數據類型的零值.假設一個類變量的定義為:
public static int value = 123;
那變量在準備階段過后的初始值是0而不是123,因為這時候尚未進行任何的Java辦法,而把value賦值為123是在初始化階段.但是如果上面的類變量定義為:
public static final int value = 123;
那么在編譯階段javac會為value生成ConstantValue屬性,而且在準備階段value就會被賦值為123.
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程.
類初始化階段是類加載的最后一步,初始化階段是執行類構造器<clinit>()辦法的過程,<clinit>()辦法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句合并產生的.
<clinit>()辦法與類的構造函數<init>()不同,它不需要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()辦法執行之前,父類的<clinit>()辦法已經執行完畢.所以在虛擬機中第一個被執行的<clinit>()辦法的類必定是java.lang.Object.
<clinit>()對于類或接口來說并不是必須的,如果類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器不會生成該辦法.
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口也會生成<clinit>().但接口和類不同的是:執行接口的<clinit>()不必要先執行父接口的<clinit>(),只有當父接口中定義的變量被使用時,父接口才會初始化.另外接口的實現類在初始化也一樣不會執行接口的<clinit>()
對于任意一個類,都需要由他的類加載器和這個類自己共同確立其在Java虛擬機中的唯一性.每一個類加載器,都擁有一個獨立的類名稱空間.簡言之,比較兩個類是否“相等”只有在這兩個類是由同一個類加載器加載的前提下才有意義.
如下例子:
1 /** 2 * @author zhouxuanyu 3 * @date 2017/06/03 4 */ 5 public class ClassLoaderTest { 6 public static void main(String[] args) { 7 ClassLoader myLoader = new ClassLoader() { 8 @Override 9 public Class<?> loadClass(String name) throws ClassNotFoundException { 10 try { 11 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; 12 InputStream is = getClass().getResourceAsStream(fileName); 13 14 if (is == null) { 15 return super.loadClass(name); 16 } 17 byte[] b = new byte[is.available()]; 18 is.read(b); 19 return defineClass(name, b, 0, b.length); 20 } catch (IOException e) { 21 throw new ClassNotFoundException(); 22 } 23 } 24 }; 25 26 try { 27 Object o = myLoader.loadClass("com.alibaba.jvm.ClassLoaderTest").newInstance(); 28 System.out.println(o.getClass()); 29 System.out.println(o instanceof com.alibaba.jvm.ClassLoaderTest); 30 } catch (InstantiationException e) { 31 e.printStackTrace(); 32 } catch (IllegalAccessException e) { 33 e.printStackTrace(); 34 } catch (ClassNotFoundException e) { 35 e.printStackTrace(); 36 } 37 } 38 }
最后的運行結果為:
1 class com.alibaba.jvm.ClassLoaderTest 2 false
可以看出,我們本身實現的類加載器確實加載并實例化了類com.alibaba.jvm.ClassLoaderTest的,但是實例化對象在與類com.alibaba.jvm.ClassLoaderTest做類型檢查的時候卻返回了false.因為虛擬機中存在兩個com.alibaba.jvm.ClassLoaderTest類,一個是系統類加載器加載,另一個是由我們本身實現的加載器加載的,雖然這兩個類來自同一個Class文件但是卻是兩個獨立的類.
從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種便是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader.
從Java開發人員的角度來看,有三種類加載器:
啟動類加載器(Bootstrap ClassLoader):負責加載<java_home>\lib目錄或者由參數-Xbootclasspath指定路徑中并且是虛擬機辨認的類庫加載到虛擬機內存中.
擴展類加載器(Extension ClassLoader):負責加載<java_home>\lib\ext目錄中或者被java.ext.dirs系統變量指定路徑中所有的類庫.
應用程序加載器(Application ClassLoader):負責加載由CLASSPATH指定的類庫,如果程序沒有自定義類加載器,程序默認使用該加載器.
下圖是類加載器的雙親委派模型:
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的哀求,它首先不會自己去嘗試加載這個類,而是把這個哀求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載哀求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載哀求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載.
Q:為什么要采用雙親委派模型?
A:例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類.相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶本身編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂.
實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()辦法之中,邏輯如下:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()辦法,若父加載器為空則默認使用啟動類加載器作為父加載器.如果父類加載失敗,拋出ClassNotFoundException異常后,再調用自己的findClass()辦法進行加載.下面是loadClass()辦法:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // First, check if the class has already been loaded 6 Class<?> c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) { 11 c = parent.loadClass(name, false); 12 } else { 13 c = findBootstrapClassOrNull(name); 14 } 15 } catch (ClassNotFoundException e) { 16 // ClassNotFoundException thrown if class not found 17 // from the non-null parent class loader 18 } 19 20 if (c == null) { 21 // If still not found, then invoke findClass in order 22 // to find the class. 23 long t1 = System.nanoTime(); 24 c = findClass(name); 25 26 // this is the defining class loader; record the stats 27 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 28 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 29 sun.misc.PerfCounter.getFindClasses().increment(); 30 } 31 } 32 if (resolve) { 33 resolveClass(c); 34 } 35 return c; 36 } 37 }
棧幀是用于支持虛擬機進行辦法調用和辦法執行的數據結構.棧幀存儲了辦法的局部變量表、操作數棧、動態連接和辦法返回地址等信息.每一個辦法從調用開始到執行完成的過程都對應著一個棧幀在虛擬機棧里面從入棧到出棧的過程.
局部變量表是一組變量值存儲空間,用于存放辦法參數和辦法內部定義的局部變量.Java程序編譯為Class文件時,就在辦法的Code屬性的max_locals數據項中確定了該辦法所需要分配的局部變量表的最大容量.局部變量表的銅梁以變量槽(Slot)為最小單位,Java虛擬機規范說一個Slot應該能存放一個boolean,byte,char,short,int,floot,reference以及returnAddress類型的數據.為了節省空間,Slot是可以重用的.
辦法調用不等同于辦法執行.辦法調用階段唯一的任務就是確定被調用辦法的版本,即調用哪一個辦法,不涉及辦法內部的具體運行過程.
所有辦法在調用中的目標辦法在Class文件中都是一個常量池中的符號引用,在類加載期間,會將其中部分符號引用轉化為直接引用.這類辦法必須滿足:辦法在程序真正運行之前就有一個可確定的調用版本,并且這個版本在運行期間是不可改變的.
在Java中符合“編譯器可知,運行期間不可變” 這個要求的辦法主要包括靜態辦法和私有辦法.因為這兩類辦法都不能被繼承或重寫.在JVM中,凡是能被invokestatic和invokespecial指令調用的辦法都可以在解析階段唯一確定調用版本,符合這個條件的辦法有:靜態辦法,私有辦法,實例構造辦法,父類辦法4類.
解析調用必定是一個靜態的過程,在編譯器就完全的確定,不會延遲到運行期間再去完成.
服務器都會辦理幾個問題:部署在同一個服務器上的兩個web應用程序所使用的java類庫可以實現相互隔離以及相互共享,服務器自身的類庫應該與應用程序的類庫隔離.
解決辦法:服務器會提供好幾個classpath路徑供用戶存放第三方類庫,被放置到不同路徑中的類庫,具備不同的拜訪范圍和服務對象.通常,每個目錄都會有一個相應的自定義類加載器去加載放在里面的Java類庫.以tomcat為例:tomcat目錄結構中,有4組目錄(/common/*,/server/*,/shared/*,/WEB-INF/*)可以存放Java類庫:
本文永久更新鏈接地址:
維易PHP培訓學院每天發布《LINUX入門:《深入理解Java虛擬機》 讀書筆記》等實戰技能,PHP、MYSQL、LINUX、APP、JS,CSS全面培養人才。