Java高級編程基礎:可以從這個思路去理解JVM和性能調優

2019-07-22     道以致遠

雖然網上現在有很多對於JVM內部體系結構進行詳細介紹的文章,可是我還是從我個人的理解來說一下它。

因為每個人對於一件東西的理解思路可能都不太一樣,我不想純粹的去複述官方文檔的說明,只是想從我學習使用Java語言編程經驗的角度去說一下,希望能夠給各位想學習Java的小夥伴提供一個思考路徑。

JVM是個什麼東西?

對於JVM又稱Java虛擬機,其實它最初為了解決跨平台編程而設計出的一套規範的實現,這套規範從數據類型,文件存儲結構,到內存和線程處理等都做了詳細的約定,而實現這個約定的程序就是Java虛擬機,目前使用最多的流行Java虛擬機是Hotspot VM,其中Oracle的JDK和OpenJDK都使用了該類型的虛擬機。

你可以將虛擬機理解成一個運行在作業系統上的進程,它包括了對數據類型的對接,對本地原生函數的對接,對CPU和線程的調度管理的抽象等等,這些內容幾乎是在原有作業系統上面又抽象出了一個作業系統,這也就是為什麼說它被稱為Java虛擬機。

編程基礎

JVM跟我們常見的JRE,JDK等是什麼關係

我們一般的電腦要想能夠運行Java程序就必須先安裝一個稱為Java運行時環境的程序,它就是JRE,只要有它,我們的Java應用程式就能夠啟動運行。

我們在JRE的基礎上增加了一些開發調試的工具以及類庫,就構成了我們開發人員需要安裝的Java 開發工具包,JDK。

所以JVM,JRE和JDK它們三者之間的關係是JVM單指實現程序,JRE包含JVM作為底層內核,JDK有包含JRE。

它與我們編寫的Java程序有什麼關係?

我們知道用Java編寫的應用程式是.java類型文件,我們通過Java編譯器將其編譯為.class文件,其內容是位元組碼

Java正是依靠這個位元組碼來實現了跟平台無關的開發和部署。因為針對任何架構的處理器,Java的位元組碼都可以通過JIT來將位元組碼編譯為對應平台的指令代碼並執行。如此以來就讓Java應用程式的開發不必再去考慮平台問題。

JVM的內部結構

說起JVM的內部結構,實際上它就是Java虛擬機程序在我們計算機內存上創建了一個大塊內存空間。這塊空間由它負責管理。

由於我們要運行應用程式首先被啟動的是類加載器,它的作用就是將我們應用程式用到的所有存儲在各類環境里的位元組碼文件找出來,讀取其內容,讓其內容轉換成一個個Class類對象實例,保存在指定的內存空間裡。其中包括JVM啟動需要的基礎環境類,擴展類和我們應用程式包含的自己編寫的類以及引用的第三方的類,當然這個加載過程不是一次性全部加載,這些類中部分是需要預加載的,還有就是需要在運行時動態加載的。

理解了這些以後,我們就拿一個Java應用程式的運行過程來看,首先我們通過系統的一個句柄來開啟一個主應用程式進程,該進程乾的第一件事就是啟動JVM的類加載模塊,將相關的JVM的基礎類和擴展類以及應用程式的Classpath下的所有類都進行尋址讀取內容,創建Class類實例。

然後,再去創建應用程式的子線程,創建子線程過程就是在應用程式管理的總空間內創建一個單向操作的棧空間,將應用程式中定義的方法和參數都封裝成幀然後推入到執行的棧中。我們將存放棧內容的空間稱為棧,它隨著應用程式的執行,不斷的推入和移出棧幀。每個幀其實就是一個方法執行的上下文,存儲本地變量,臨時計算結果等。

而我們JVM管理的最大一塊公共的內存空間叫做,這是一塊自由使用的內存空間,我們應用程式運行過程中要使用的對象實例都是在這個空間中被創建和實例化出來的。而且這裡的對象實例往往是因為某種運算需要而臨時這幾生成的,所以他們很少有能夠持續被在整個應用程式生命周期中使用的,這就或造成應用程式管理的內存空間不足問題,所以要求JVM必須有一個專門的線程來處理這個問題,辦法就是垃圾收集器,一般就是對這塊空間的不再使用的類實例進行清理,對於還在使用的對象予以保留了,甚至有些內容可能會保存到整個應用程式結束。

Java入口

堆空間管理

有時候為了給較大的應用程式對象分配空間,可能會對享有的零散空間進行複製轉移,壓縮,以騰出足夠的空間為未來程序執行需要其它程序。這個過程被稱為垃圾收集,因為收集過程中涉及到如何判別哪些對象還在被使用,哪些不再被使用,為了能夠獲取更大的連續空間,整理存儲碎片,可能需要對空間進行複製轉移,壓縮等操作。

在設計垃圾收集算法時將整塊空間劃分為三塊區域,一種是用於創建對象的區域,被稱為伊甸區,在一次垃圾收集執行後,還被引用的對象會被移動到稱為存活區的空間裡,騰出創建區空間給接下來的新建對象使用,而對於兩次垃圾收集以後還存活的引用對象,將會被移至老年區空間存儲。這就是我們常說的垃圾收集算法。

由於我們整個應用程式在啟動時對JVM管理的內存空間是有限的,我們的應用程式必須在JVM的管理調度下在這個有限的空間內創建對象,計算,銷毀對象等不管努力,如果我們應用程式創建的對象需要空間太大,而我們總的堆空間太小,就會造成性能問題,嚴重還會造成內存溢出問題。

所以我們在編寫Java應用程式時需要通過一些檢查工具比如VisualVM,當然在我們安裝的JDK中有很多命令行工具也能用,來不斷的挑戰應用程式的堆內存使用率主要是我們在運行程序時,我們可以為這些空間的分配指定大小,具體參數我們可以去查找相關說明。通過對於代碼的優化,JVM運行參數的調整,讓我們的應用程式在執行過程中不會出現堆空間的浪費和不足,提高程序運行性能穩定性和效率,以及選擇適當的垃圾收集策略,這個過程就是我們常說的Java應用程式調優。

JVM的整體視角

在我們上面說過,JVM整體分為三大塊,第一塊是類文件加載生成系統,第二塊是應用程式數據存儲和管理區域,第三塊是底層代碼編譯和執行區域。

其中最重要的就是第二塊,應用程式數據存儲和管理區域,它嚴格分為堆空間和非堆空間,其中堆空間上面說過了,非堆空間就是我們常說的存放不怎麼變化的內容的區域它包括方法定義區,內部的常量,靜態變量等,這部分在Java 8後被稱為元數據區,另外還有一部分是代碼的緩衝區,方法執行的棧工作區等。

好了說完了關於JVM的整體結構,我們在進入一個重要的封裝體幀看一下它的結構,幀是棧的最小執行單位,棧是後進先出型,意味著它只能從一頭壓入幀,然後再從壓入口推出要執行的幀。而這個幀我們說了它是我們要執行方法的封裝體,它裡面的空間被範圍四大塊,分別是當前方法所在類的常量池引用區操作數棧方法本地變量區,以及返回值區等。 我們的方法會被封裝在它裡面進行操作處理。

Java與JVM

JVM如何參與Java程序執行

總體來說,整個JVM參與對我們編寫的Java應用程式執行的全過程,並在過程中對應用程式進行分解和結構布局。

首先,應用程式作為作業系統生一個進程被執行,開啟其第一個主線程,調用JVM的類文件加載模塊,將JVM的啟動類,擴展類,以及我們應用程式編寫的代碼類以及我們引用的第三方類庫通過加載模塊調用類加載器加載並啟動應用程式,其加載過程主要經歷尋址讀取位元組碼文件流,創建各種類型的Class對象,初始化靜態變量,建立本地對象和Class類連結,實例化部分預加載對象。在這個過程中,JVM會管理的內存空間會被分成三塊方法區,棧區,堆區。

方法區是我們初始類加載過程中重要的創建區,常量,類定義,接口定義,註解元數據等內容都會存放在這裡。

棧區是執行區,它主要是用來為線程創建執行棧空間的區域,它包括了所有線程的棧,以及本地方法棧。

最後就是堆區,是各類線程公共區,因為棧的特點是無法存放複雜的數據類型,所以我們應用程式運行過程中需要的所有引用類型,比如對象,數組,字符串等都會在這塊區域創建並保存直到銷毀。

總結一下

我們的應用程式在磁碟上體現為jar包的文件系統,而被讀取到內存中準備執行時,就會將位元組碼流轉換成Class類對象存儲備用,用來在需要時創建相應的實例對象。

它們在JVM里存儲分布上面已經說了,理解這個布局和執行過程,就是為了更好的調度和使用這些空間,讓我們的應用程式運行起來更加有效率,消耗的內存空間和資源越少,整體來說Java編程其實就是一個基於託管環境的編程,可以說是帶著鐐銬跳舞,我們在編寫代碼時主要是注意整個應用程式的需要JVM的管理內存空間大小,我們用於動態創建和存儲對象空間大小,我們對於垃圾收集器算法的選擇等等。這些都可在啟動程序時通過指定相關參數來調整,找到適合我們應用程式運行的最佳參數指定。

文章來源: https://twgreatdaily.com/lf0ELGwBmyVoG_1ZYJRM.html