從上帝視角看Java如何運行

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

The fact is that the world is out of everyone's expectation. But some learn to forget, but others insist.

事實上,這個世界不會符合所有人的夢想。只是有人學會遺忘,有人卻堅持。


每日掏心話

每一發奮努力的背後,必是有加倍的賞賜。莫要去找藉口失敗,只找理由成功。也許現在你正面臨一些困難,但是你別忘了,生活總是在向前,明天總是會更好。

來自:swayer | 責編:樂樂

鏈接:cnblogs.com/swayer/p/12665710.html

從上帝視角看Java如何運行

程序員小樂(ID:study_tech)第 832 次推文 圖片來自百度


往日回顧:前後端分離模式下的權限設計方案


正文


JVM內存結構

從上帝視角看Java如何運行

可以看出JVM從宏觀上可以分為 ‘內部’ 及 ‘外部’ 兩個部分(便於記憶理解):

‘內部’包含:線程共享(公有)數據區 和 線程隔離(私有)數據區

‘外部’包含:類加載子系統、垃圾回收器、執行引擎、本地庫接口、本地方法庫

以上部件構成了整個jvm,接下來我們一個一個零件拆開了看。

class文件


  一個java文件會通過編譯工具(javac)編譯成class字節碼文件,通過jvm進行加載運行。因為jvm屏蔽了底層操作系統的差異(平臺無關性),所以一次編譯到處運行。

類加載子系統


  類加載子系統:負責查找和裝載class文件,將其中的二進制數據加載到jvm中。

從上帝視角看Java如何運行

字節碼 --> 加載 --> 驗證 --> 準備 --> 解析 --> 初始化

加載:通過類的完全限定名找到類文件所在位置,根據其中的字節碼創建java.lang.Class對象,所以才會說萬物皆對象,我們也可以繼承ClassLoader,重寫findClass方法來自定義實現類加載器。默認情況下我們都使用AppClassLoader

驗證:確保加載的字節碼的是否符合虛擬機的要求,是java提供的一種自我保護機制,不讓其危害虛擬機安全。其主要包括四種驗證,字節碼驗證、文件格式驗證,元數據驗證、符號引用驗證。

準備:為類變量分配地址和初始化值,類變量會分配到方法區(元空間)中,這裡的初始化是指該數據類型的默認初始值,例如int對應的是0,long對應的0L,只有在初始化時才會動顯示賦值

解析:把類中的二進制數據中的符號引用轉換為直接引用;例如我們通過user.getInfo();這裡的.getInfo()就是符號引用,在解析階段會將它指向真正的內存位置,這就是直接引用

初始化:主要為類的靜態變量賦予正確的值,比如int num = 10; 這裡num的值會從準備階段的0變為10;並且若該類有父類,會對其進行初始化操作;如果類中有初始化語句,系統會按照順序進行初始化

雙親委派模式

從上帝視角看Java如何運行

雙親委派:自底向上檢查是否加載成功,自頂向下嘗試加載。


當一個類加載器收到類加載請求,它不會自己進行加載,而是將該請求丟給父類加載,如果父類還存在父類,則會依次向上請求,直到到達頂級加載器,如果父類加載器能加載完成就返回加載成功,否則子類加載器才會自己嘗試加載。









  • System.out.println(Test.class.getClassLoader());


  • System.out.println(Test.class.getClassLoader().getParent());


  • System.out.println(Test.class.getClassLoader().getParent().getParent());


  • System.out.println(String.class.getClassLoader());


通過代碼驗證,可以很輕鬆的瞭解 AppClassLoader -> ExtClassLoader -> BootstrapClassLoader 這三層的關係。

類加載的三種方式

1. new關鍵字加載



  • User user = new User();



  靜態加載,在運行時候通過new關鍵字創建類實例

2. Class.forName()加載





  • Class clazz = Class.forName(“User”);


  • Object user=clazz.newInstance();


   動態加載,通過Class.forName()來加載類,然後調用類的newInstance()方法實例化對象

3. ClassLoader 實例的 loadClass() 方法





  • Class clazz = classLoader.loadClass(“User”);


  • Object user=clazz.newInstance();


   動態加載,可通過繼承ClassLoader實現自定義類加載器

線程私有和線程公有

從上帝視角看Java如何運行

JVM內存區從宏觀上可以分為 線程私有 和 線程公有 兩塊。

線程私有部分

  這部分沒有線程安全問題,隨著線程執行結束而結束;包含程序計數器、虛擬機棧、本地方法棧三個部件。

程序計數器:

  程序計數器也叫PC寄存器,作用是cpu進行切換的時候,指向當前時刻需要獲取指令的位置。

特點:


  • 線程私有

  • 一塊較小的區域

  • 記錄程序執行的位置

  • 不存在內存溢出OutOfMemoryError


虛擬機棧:

  棧數據結構實現,入口和出口只有一個,稱之為入棧和出棧,先進後出(FILO)

從上帝視角看Java如何運行

  棧的作用主要是執行方法,先執行的方法在最下面,然後依次放入,方法執行完畢之後從上往下依次退出;所以方法執行就是壓棧,方法結束就是出棧(銷燬棧幀)。









  • public void start(){


  • say();


  • run();


  • }


從上帝視角看Java如何運行

虛擬機棧如何執行

從上帝視角看Java如何運行

棧幀

  棧幀存在Java虛擬機棧中,是虛擬機棧中的單位元素。方法執行會創建棧幀,一個方法就是一個棧幀,一個棧幀分為四個部分:

1. 局部變量表

存放方法參數或者內部定義的一組變量列表;例如方法中聲明的對象:



  • User user = new User(); //局部變量user



2. 操作數棧

執行字節碼指令的時候使用,通俗的講就是方法的執行在操作數棧中進行,通過壓棧和出棧進行訪問


3. 動態鏈接

Java運行期間是動態鏈接的,需要將指向方法的符號引用轉換為直接引用(內存地址);在類加載解析階段,將符號引用轉換為直接引用稱之為靜態解析。而此處正好就是動態鏈接

user.getInfo(); //找到這個getInfo()方法的內存位置


4. 返回地址


方法不管正常執行結束還是異常退出,需要返回方法被調用的位置

以上四個部分對應方法執行的過程。虛擬裡面包含很多個棧幀,每個方法對應一個棧幀。

將一個class文件,通過bin/javap.exe文件進行反彙編可以查看出以上四個部分。

棧溢出:當棧的深度大於虛擬機允許會報StackOverflowError,-Xss可設置大小



























  • /** 遞歸演示如何棧溢出 */


  • public static int num = 0;


  • public static void a(){


  • num++;


  • a();


  • }


  • public static void main(String[] args) {


  • try{


  • a();


  • }catch (Exception ex){


  • System.out.println("調用次數:"+num);


  • }


  • }


內存溢出:當棧需要擴展而無法申請空間會報OutOfMemoryError

本地方法棧


本地方法棧和虛擬機棧類似,區別在於虛擬機棧主要為jvm執行字節碼服務,而本地方法棧為Native方法服務,即本地方法服務;所以本地方法棧也是一塊內存私有區域,與虛擬機棧相同也有同樣的異常問題。

特點:


  • 與虛擬機棧基本類似

  • 區域在於本地方法棧為Native方法服務(windows下調用dll文件)

  • Sun HotSpot將虛擬機棧和本地方法棧合併

  • 有StackOverflowError和OutOfMemoryError


線程公有部分

從上帝視角看Java如何運行

  這部分存在線程安全問題,平常我們所指的內存優化,溢出等問題都是需要關注這個區域。包含堆、方法區(也叫元空間)兩個部件。


方法區(元空間)


  類加載器加載類的時候,會將一些類的元數據信息(字節碼)保存在這個區域,例如:類變量,靜態方法,普通方法等,方法區是線程共享的,多個線程能用到同一個類

  jdk1.7合併方法區到了堆裡面

  jdk1.8保留了方法區的概念,只不過實現方式不同,jdk1.8稱為元空間,與堆不相連,但是與堆共享物理內存,邏輯上可以認為是在堆中

特點:


  • 線程共享

  • 存儲類信息、常量、靜態變量、方法描述等信息

  • HotSpot虛擬機中稱之為永久代

  • GC很少回收這個區域

  • 存在OutOfMemoryError,可以通過-XX:MaxPermSize設置大小


從上帝視角看Java如何運行

  堆中用於存放所有實例化對象和數組,堆中信息線程共享,所有jvm部件中分配內存中最大的區域,在虛擬機啟動時就創建,垃圾回收器主要管理該區域,堆分為新生代(佔堆內存1/3)和老年代(佔堆內存2/3),新生代更細緻可以分為Eden、From Survivor、To Survivor空間,比例8:1:1 ;可以通過-Xmx、-Xms設置大小

在堆中產生了一個實例對象或數組,可以在棧中聲明一個變量,用於指向堆中的對象,該變量的取值等於堆中對象的內存地址,所以我們在打印變量名的時候是一串內存地址





  • Test test = new Test();


  • System.out.println(test); //輸出Test@1b6d3586


  

萬物皆對象,當我們在實際開發中,創建了許多對象,為了防止內存洩露,java確保有效的使用內存,會由java虛擬機自動垃圾回收器來管理;且把堆分為新生代和老年代進行管理


新生代與老年代


  新生代是Java對象出生的地方,是新對象分配內存的地方,大部分對象存活時間都不需要太久,這個區域會頻繁觸發MinorGC進行垃圾回收;
  而老年代存放的都是存活時間較久或者內存較大的對象,所以Full GC不會頻繁執行。


Minor GC


  發生在新生代中的垃圾回收機制,採用複製算法(掃描存活對象,複製到一塊新內存空間中),From Survivor 和 to Survivor是相對的,也就是說Minor GC發生時,Eden區和其中一個Survivor區會把一些仍然存活的對象放置另外一個Survivor 區,然後清理Eden區和之前的Survivor 區,下次同理,當達到一定 ‘年齡’ 後,新生代會把對象放入老年代(每發生一次Minor GC增加1歲,默認15歲)


Full GC

  發生在老年代中的垃圾回收機制,採用標記-清除(標記存活的對象,清除未標記的對象,即需要回收的對象),因為老年代中的對象較穩定,所以發生Full GC的頻率相對Minor GC較少,但是一次回收的時間會比Minor GC更長

從上帝視角看Java如何運行

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


猜你還想看


阿里、騰訊、百度、華為、京東最新面試題彙集

手把手教你 Netty 實現自定義協議!

一次項目代碼重構:使用Spring容器幹掉條件判斷

JDK 中定時器是如何實現的

關注訂閱號「程序員小樂」,收看更多精彩內容
嘿,你在看嗎?


分享到:


相關文章: