JVM之--Java內存結構(第一篇)

最近在和同事朋友聊天的時候,發現一個很讓人思考的問題,很多人總覺得JVM將java和操作系統隔離開來,導致很多人不用熟悉操作系統,甚至不用瞭解JVM本身即可完全掌握Java這一門技術,其實個人的觀點是,Java由於有了JVM才使這門語言簡單上手,同時也正是因爲Java有了JVM才使的Java這門技術很難深入瞭解。

在C/C++中我們可以很方便的new內存,delete內存,在內存的使用中我們擁有至高的權利,而Java則不行,JVM這一扇大門死死的堵住了內存的操作細節,你無法直接操作內存,所以你能做的就是百分之百的信任JVM給你帶來的各種便利都是非常科學和合理的,但是有時候事實並非如此,JVM也不能百分之百的根據你的程序去猜想你所需要的內存資源更談不上分佈情況了,那麼JVM能做的就是以他自認爲比較合理的方式去爲申請,劃分內存。

舉個最簡單的例子,你瞭解OutOfMemoryError麼?籠統來說它就是內存不足引起的,可是他到底是那一塊內存溢出所導致的呢?我們只有掌握和了解了JVM的內存劃分,才能真的掌握關於內存出現問題的診斷,甚至可以很方便的調優,起到事半功倍的效果,當然也就會讓你不再抱怨JVM。

在本文中,我們將重點介紹如下內容:

  • Java的內存劃分
  • 各個內存的詳解
  • 創建一個對象後的內存分佈情況。

第一、Java內存劃分

JVM在運行java程序的時候會把內存劃分爲如下的幾部分,如下圖所示:


1.1 程序計數器:

首先來說說程序計數器,程序計數器是一個比較小的內存空間,他的作用是什麼呢?回想一下CPU的總線結構吧,CPU有三個總線,數據總線,控制總線,地址總線,RAM和CPU交互的時候其實就是逐條的通過一些命令字透過控制總線發送命令,並且將數據通過數據總線進行來回交互,Java在運行時期,其實也是內存在和cpu來回往返的發送各種命令字,並且交換數據,我們的java代碼會通過java編譯器最終轉換成一些底層的命令字(class文件->本地方法將class文件解析轉換成標準的命令字)既然是一堆命令字相關的東西,也就存在先運行什麼?調用哪個方法,獲取那個數據,進行如何的操作等等,在程序計數器中存放的就是這些東西。

我們知道cpu執行的執行時間和分配是由cpu隨機或者根據某種cpu的算法規則輪流切換執行某個命令字的,在某一個時刻,他始終只能執行一條命令字,在執行完畢某個命令字之後也需要能夠確保回到下一個執行命令字位置的正確性,因此java將這一塊內存設計成了私有的,也就是說一個執行的線程都會有一個自己私有的/獨享的程序計數器內存空間,該內存空間非常小,另外如果調用的是一個native的方法,則內存計數器不會分配內存空間,並且此內存空間不會出現OutofMemoryError的情況,也是唯一一個。

1.2 虛擬機棧:

虛擬機棧也是線程私有的,每一個方法被執行的時候都會創建一個棧幀,存放在虛擬機棧中,虛擬機棧的結構大致如下所示

每一個方法被調用直到完成的過程,就對應着一個棧幀在虛擬機棧中從如棧到出棧的過程,其中局部變量表就是很多人所說的棧(堆棧地址),他所存放的是基本類型數據和對象的引用類型(reference),其局部變量表中的數據在編譯時期就基本上已經確認了,操作棧主要就是壓棧或者彈棧,其中動態鏈接這一部分我個人的理解是動態尋找獲取下一個方法的入口地址信息等(個人理解的,有可能不準確)
大多數JVM的虛擬機棧都可以動態擴展的,當無法申請到足夠的內存時候會拋出OutOfMemoryError。

1.3 本地方法棧

本地方法棧和虛擬機棧基本上類似,只不過區別是這樣的,虛擬機棧是虛擬機本身爲java程序開闢的一段內存單元,而本地方法棧是虛擬機調用本地方法時所需要的內存空間。本地方法棧也存在着內存溢出的風險,在SUN提供的JDK中本地方法棧和虛擬機棧合二爲一!

1.4 堆

堆在java內存單元中佔據着比較大的比重,也是最大的一部分內存單元,在虛擬機啓動的時候,該部分的內存就會被創建,所有的對象創建,以及數組內存的申請分配都是在該內存單元上發生的。

由於堆內存所佔的比重比較大,因此他也就是java垃圾回收器最關注的一塊內存,因此該內存單元也被稱爲GC堆。如果以後您瞭解了GC機制,您會知道,Java允許內存單元不連續,只要邏輯上是連續的即可,這部分的內存也是可以進行擴展的,在啓動虛擬機時我們可以通過-Xmx,-Xms進行控制,當堆中的內存再也申請不到的時候就會拋出內存溢出的異常,另外該內存空間是線程共享的,我們經常使用到的鎖其實就是在這部分內存中活動。

1.5 方法區

方法區也是各個內存的共享區域,用於存放虛擬機的類加載信息,常量,變量,靜態變量等數據,每一個方法的執行其實就是在這部分內存中運行,要操作的數據是在虛擬機棧中獲取,也可以理解爲方法的活動區域。

1.6 運行時常量池

java在編譯的時候會將我們定義的final類型做自動的優化存放在常量池中,這樣可以提高訪問和尋址的速度,因爲常量不會再運行期間變化,也就是說他的數據單元地址不會發生改變,一次尋址即可,他其實是方法區的一部分,在android中執行完編譯之後除了有class文件還會有一個idx文件,該文件其中的一些數據就是將java文件中常量字面量,這也是爲什麼一些有經驗的人在編寫代碼的時候非常喜歡用final進行類型的修飾,在方法的參數中,方法體中,類變量中,只要是認爲不可變的都進行final聲明,試圖告訴java虛擬機,這樣的變量存放在常量池中,提高尋址速度。

1.7 直接內存

還記得《java NIO》一書中,作者在描述NIO爲什麼比傳統的IO快得原因麼?傳統的IO進行數據讀寫操作,首先是Java程序操作java堆中的內存,java堆又拷貝本地方法內存中的數據,java本地方法通過操作系統進行文件的操作,而直接內存就可以不通過java堆和本地方法堆的來回拷貝,而直接操作堆內存以外的內存,這樣會節省很多來回數據複製的時間。

2、對象訪問

其實上文中的很多內容,也是通過參閱jvm規範以及別人寫的jvm得來的,因爲它並不像我們進行一個加法運算或者方法調用那樣理所當然的去分析,如果碰到很底層的問題或者JVM內部的問題,儘可能的瞭解到,並且記住他的大概原理,如果要我自己去論證JVM的內存分佈是否真的是如此,確實是一個很艱鉅的任務,當然藉助一些工具也可以看到他們的大概分佈情況,在以後的文章中介紹JVM相關工具的時候我們在一起研究探索吧。
瞭解了上面的知識,爲了將概念性的東西,轉換爲比較直觀的東西,我們來看看對象訪問的一個過程,並且用圖解的方式來說明。
一個很簡單的Object obj = new Object()其實涉及的內存單元有虛擬機棧內存,堆內存,方法區,程序計數器等。當執行了new方法之後,jvm會在堆內存中開闢一塊內存單元存放obj信息,與此同時,obj的類型信息,父類,接口,方法等信息會被存放在方法區中,堆內存爲了能夠訪問到這些信息,除了存放obj的信息之外,還會存放訪問這些信息的地址信息。與此同時產生的引用,基本數據類型也會存放到棧內存之中,編譯時期生成的命令字也理所當然的存放到了程序計數器之中,如下圖所示。

上圖所描述的是通過句柄的方式訪問方法區,並且給棧內存提供訪問方式,下圖中將是通過指針的方式訪問方法區,提供棧內存訪問基本上沒有太多的變化,如下圖所示。
可以看到在對象實例中就包含了方法區的地址指針,而不用再存在一個句柄空間專門存放,這樣當棧訪問堆中的引用時,堆就可以直接獲取方法區的數據。

好了,本文就大概描述到這裏,JVM是一個非常神祕的東西,很多東西我們只有記住的份,因爲內存你說了不算,只有他纔是操作內存的入口。但是瞭解他的內存結構,我們就能有效的合理的分配堆棧內存,提高程序運行效率,在下一篇文章中,我們一起設計一些能直接影響到java不同內存的程序,一起分析他們的原因和如何規避,如何調試等。本文中肯定存在很多偏頗之處,希望各位能夠直言不諱,寫作博客的目的就是爲了進步,如果寫出來僅此而已,那麼意義不大,另爲,如果本文中有些欠妥的東西,千萬不要以訛傳訛。


發佈了32 篇原創文章 · 獲贊 21 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章