常常有人會有這麼一個疑惑,難道 Java 開發就必定要懂得 JVM 的原理嗎?我不懂 JVM ,但我照樣能夠開發。確實,但若是懂得了 JVM ,可讓你在技術的這條路上走的更遠一些。java
<!-- more -->git
JVM 的重要性
首先你應該知道,運行一個 Java 應用程序,咱們必需要先安裝 JDK 或者 JRE 。這是由於 Java 應用在編譯後會變成字節碼,而後經過字節碼運行在 JVM 中,而 JVM 是 JRE 的核心組成部分。github
優勢
JVM 不只承擔了 Java 字節碼的分析(JIT compiler)和執行(Runtime),同時也內置了自動內存分配管理機制。這個機制能夠大大下降手動分配回收機制可能帶來的內存泄露和內存溢出風險,使 Java 開發人員不須要關注每一個對象的內存分配以及回收,從而更專一於業務自己。算法
缺點
這個機制在提高 Java 開發效率的同時,也容易使 Java 開發人員過分依賴於自動化,弱化對內存的管理能力,這樣系統就很容易發生 JVM 的堆內存異常、垃圾回收(GC)的不合適以及 GC 次數過於頻繁等問題,這些都將直接影響到應用服務的性能。數組
內存模型
JVM 內存模型共分爲5個區:堆(Heap)
、方法區(Method Area)
、程序計數器(Program Counter Register)
、虛擬機棧(VM Stack)
、本地方法棧(Native Method Stack)
。多線程
其中,堆(Heap)
、方法區(Method Area)
爲線程共享
,程序計數器(Program Counter Register)
、虛擬機棧(VM Stack)
、本地方法棧(Native Method Stack)
爲線程隔離
。性能
堆(Heap)
堆是 JVM 內存中最大的一塊內存空間,該內存被全部線程共享,幾乎全部對象和數組都被分配到了堆內存中。優化
堆被劃分爲新生代和老年代,新生代又被進一步劃分爲 Eden 區和 Survivor 區,最後 Survivor 由 From Survivor 和 To Survivor 組成。線程
隨着 Java 版本的更新,其內容又有了一些新的變化: >在 Java6 版本中,永久代在非堆內存區;到了 Java7 版本,永久代的靜態變量和運行時常量池被合併到了堆中;而到了 Java8,永久代被元空間
(處於本地內存)取代了。code
爲何要用元空間
替換永久代呢?
- 爲了融合 HotSpot JVM 與 JRockit VM,由於 JRockit 沒有永久代,因此不須要配置永久代。
- 永久代內存常常不夠用或發生內存溢出(應該是 JVM 中佔用內存最大的一塊),產生異常
java.lang.OutOfMemoryError: PermGen
。在 JDK1.7 版本中,指定的 PermGen 區大小爲 8M,因爲 PermGen 中類的元數據信息在每次 FullGC 的時候均可能被收集,回收率都偏低,成績很難使人滿意;還有,爲 PermGen 分配多大的空間很難肯定,PermSize 的大小依賴於不少因素,好比,JVM 加載的 class 總數、常量池的大小和方法的大小等。
看到這兒,天然就想到了 GC 回收算法,不用急,我會在以後的文章中進行講解,如今仍是以 JVM 內存模型爲主。
方法區(Method Area)
什麼是方法區? >方法區主要是用來存放已被虛擬機加載的類相關信息,包括類信息
、常量池
(字符串常量池以及全部基本類型都有其相應的常量池)、運行時常量池
。這其中,類信息又包括了類的版本、字段、方法、接口和父類等信息。
類信息
JVM 在執行某個類的時候,必須通過加載、鏈接、初始化,而鏈接
又包括驗證、準備、解析三個階段。
在加載類的時候,JVM 會先加載 class 文件,而在 class 文件中便有類的版本、字段、方法和接口等描述信息,這就是類信息
。
常量池
在 class 文件中,除了類信息
,還有一項信息是常量池 (Constant Pool Table),用於存放編譯期間生成的各類字面量
和符號引用
。
那字面量
和符號引用
又是什麼呢?
字面量包括字符串(String a=「b」)、基本類型的常量(final 修飾的變量),符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、字段的名稱和描述符以及方法的名稱和描述符。
運行時常量池
當類加載到內存後,JVM 就會將 class 文件常量池
中的內容存放到運行時常量池
中;在解析階段,JVM 會把符號引用替換爲直接引用(對象的索引值)。
例如: >類中的一個字符串常量在 class 文件中時,存放在 class 文件常量池中的。 > >在 JVM 加載完類以後,JVM 會將這個字符串常量
放到運行時常量池
中,並在解析階段,指定該字符串對象的索引值。
運行時常量池
是全局共享的,多個類共用一個運行時常量池,所以,class 文件中常量池多個相同的字符串在運行時常量池只會存在一份。
講到這裏,你們是否是有些頭暈了,說實話,我在看到這些內容的時候,也是雲裏霧裏的,這裏舉個例子幫助你們理解:
public static void main(String[] args) { String str = "Hello"; System.out.println((str == ("Hel" + "lo"))); String loStr = "lo"; System.out.println((str == ("Hel" + loStr))); System.out.println(str == ("Hel" + loStr).intern()); }
其運行結果爲:
true false true
第一個爲 true,是由於在編譯成 class 文件時,可以識別爲同一字符串的, JVM 會將其自動優化成字符串常量,引用自同一 String 對象。
第二個爲 false,是由於在運行時建立的字符串具備獨立的內存地址,因此不引用自同一 String 對象。
最後一個爲 true,是由於 String 的 intern() 方法會查找在常量池中是否存在一個相等(調用 equals() 方法結果相等)的字符串,若是有則返回該字符串的引用,若是沒有則添加本身的字符串進入常量池。
涉及到的Error
OutOfMemoryError
出如今方法區沒法知足內存分配需求的時候,好比一直往常量池中加入數據,運行時常量池
就會溢出,從而報錯。
程序計數器(Program Counter Register)
程序計數器是一塊很小的內存空間,主要用來記錄各個線程執行的字節碼的地址,例如,分支、循環、跳轉、異常、線程恢復等都依賴於計數器。
因爲 Java 是多線程語言,當執行的線程數量超過 CPU 數量時,線程之間會根據時間片輪詢爭奪 CPU 資源。若是一個線程的時間片用完了,或者是其它緣由致使這個線程的 CPU 資源被提早搶奪,那麼這個退出的線程就須要單獨的一個程序計數器,來記錄下一條運行的指令。
因而可知,程序計數器和上下文切換有關。
虛擬機棧(VM Stack)
>虛擬機棧是線程私有的內存空間,它和 Java 線程一塊兒建立。 > >當建立一個線程時,會在虛擬機棧中申請一個線程棧,用來保存方法的局部變量、操做數棧、動態連接方法和返回地址等信息,並參與方法的調用和返回。 > >每個方法的調用都伴隨着棧幀的入棧操做,方法的返回則是棧幀的出棧操做。
能夠這麼理解,虛擬機棧針對當前 Java 應用中全部線程,都有一個其相應的線程棧,每個線程棧都互相獨立、互不影響,裏面存儲了該線程中獨有的信息。
涉及到的Error
StackOverflowError
出如今棧內存設置成固定值的時候,當程序執行須要的棧內存超過設定的固定值時會拋出這個錯誤。OutOfMemoryError
出如今棧內存設置成動態增加的時候,當JVM嘗試申請的內存大小超過了其可用內存時會拋出這個錯誤。
本地方法棧(Native Method Stack)
>本地方法棧跟虛擬機棧的功能相似,虛擬機棧用於管理 Java 方法的調用,而本地方法棧則用於管理本地方法的調用。 > >但本地方法並非用 Java 實現的,而是由 C 語言實現的。
也就是說,本地方法棧中並無咱們寫的代碼邏輯,其由native
修飾,由 C 語言實現。
總結
以上就是 JVM 內存模型的基本介紹,大體瞭解了一下5個分區及其相應的含義和功能,由此能夠繼續延伸出 Java 內存模型、 GC 算法等等,我也會在以後的文章中進行講解。若是你有什麼想法,歡迎在下方留言。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。
> 本文由博客一文多發平臺 OpenWrite 發佈!