JVM調優:基本概念

作者:和你在一起
來源:https://www.iteye.com/blog/pengjiaheng-518623


一. JVM的運行數據區


首先我簡單來畫一張 JVM的結構原理圖,如下。

JVM調優:基本概念

我們重點關注 JVM在運行時的數據區,你可以看到在程序運行時,大致有5個部分。

1.方法區

不止是存“方法”,而是存儲整個 class文件的信息,JVM運行時,類加載器子系統將會提取 class文件裡面的類信息,並將其存放在方法區中。例如類的名稱、類的類型(枚舉、類、接口)、字段、方法等等。

2.堆( Heap)

熟習 c/c++編程的同學們應該相當熟習 Heap了,而對於Java而言,每個應用都唯一對應一個JVM實例,而每一個JVM實例唯一對應一個堆。堆主要包括關鍵字 new的對象實例、 this指針,或者者數組都放在堆中,並由應用所有的線程共享。堆由JVM的自動內存管理機制所管理,名為垃圾回收—— GC(garbage collection)。

3.棧( Stack)

操作系統內核為某個進程或者者線程建立的存儲區域,它保存著一個線程中的方法的調用狀態,它具備先進後出的特性。在棧中的數據大小與生命週期嚴格來說都是確定的,例如在一個函數中公告的int變量便是存儲在 stack中,它的大小是固定的,在函數退出後它的生命週期也從此結束。在棧中,每一個方法對應一個棧幀,JVM會對Java棧執行兩種操作:壓棧和出棧。這兩種操作在執行時都是以棧幀為單位的。還有少量即時編譯器編譯後的代碼等數據。

4.PC寄存器

pc寄存器用於存放一條指令的地址,每一個線程都有一個PC寄存器。

5.本地方法棧

用來調用其餘語言的本地方法,例如 C/C++寫的本地代碼, 這些方法在本地方法棧中執行,而不會在Java棧中執行。


二. 數據類型


Java虛擬機中,數據類型可以分為兩類:基本類型和引用類型。


  • 基本類型的變量保存原始值,即:他代表的值就是數值本身;
  • 而引用類型的變量保存引用值。“引用值”代表了某個對象的引用,而不是對象本身,對象本身存放在這個引用值所表示的地址的位置。
  • 基本類型包括:byte,short,int,long,char,float,double,Boolean,returnAddress。
  • 引用類型包括:類類型,接口類型和數組。


三. 堆與棧


堆和棧是程序運行的關鍵,很有必要把他們的關係說清楚。


JVM調優:基本概念


棧是運行時的單位,而堆是存儲的單位。


棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。


在Java中一個線程就會相應有一個線程棧與之對應,這點很容易理解,因為不同的線程執行邏輯有所不同,因此需要一個獨立的線程棧。而堆則是所有線程共享的。


棧因為是運行單位,因此裡面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。


為什麼要把堆和棧區分出來呢?棧中不是也可以存儲數據嗎?


第一,從軟件設計的角度看,棧代表了處理邏輯,而堆代表了數據。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。


第二,堆與棧的分離,使得堆中的內容可以被多個棧共享(也可以理解為多個線程訪問同一個對象)。這種共享的收益是很多的。一方面這種共享提供了一種有效的數據交互方式(如:共享內存),另一方面,堆中的共享常量和緩存可以被所有棧訪問,節省了空間。


第三,棧因為運行時的需要,比如保存系統運行的上下文,需要進行地址段的劃分。由於棧只能向上增長,因此就會限制住棧存儲內容的能力。而堆不同,堆中的對象是可以根據需要動態增長的,因此棧和堆的拆分,使得動態增長成為可能,相應棧中只需記錄堆中的一個地址即可。


第四,面向對象就是堆和棧的完美結合。其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。但是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近於自然方式的思考。當我們把對象拆開,你會發現,對象的屬性其實就是數據,存放在堆中;而對象的行為(方法),就是運行邏輯,放在棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。不得不承認,面向對象的設計,確實很美。


在 Java 中,main 函數就是棧的起始點,也是程序的起始點。


程序要運行總是有一個起點的。同 C 語言一樣,java 中的 main 就是那個起點。無論什麼 java 程序,找到 main 就找到了程序執行的入口。


四. Java對象的大小


基本數據的類型的大小是固定的,這裡就不多說了。對於非基本類型的Java對象,其大小就值得商榷。


在Java中,一個空Object對象的大小是8byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

Object ob = new Object();


這樣在程序中完成了一個Java對象的生命,但是它所佔的空間為:4byte+8byte。4byte是上面部分所說的Java棧中保存引用的所需要的空間。而那8byte則是Java堆中對象的信息。


因為所有的Java非基本類型的對象都需要默認繼承Object對象,因此不論什麼樣的Java對象,其大小都必須是大於8byte。


有了Object對象的大小,我們就可以計算其他對象的大小了。


Class NewObject {

int count;

boolean flag;

Object ob;

}


其大小為:空對象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因為Java在對對象內存分配時都是以8的整數倍來分,因此大於17byte的最接近8的整數倍的是24,因此此對象的大小為24byte。


這裡需要注意一下基本類型的包裝類型的大小。因為這種包裝類型已經成為對象了,因此需要把他們作為對象來看待。包裝類型的大小至少是12byte(聲明一個空Object至少需要的空間),而且12byte沒有包含任何有效信息,同時,因為Java對象大小是8的整數倍,因此一個基本類型包裝類的大小至少是16byte。


這個內存佔用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的內存佔用更是誇張(隨便想下就知道了)。因此,可能的話應儘量少使用包裝類。在JDK5.0以後,因為加入了自動類型裝換,因此,Java虛擬機會在存儲方面進行相應的優化。

五. 引用類型


對象引用類型分為強引用、軟引用、弱引用和虛引用。


強引用:就是我們一般聲明對象是時虛擬機生成的引用,強引用環境下,垃圾回收時需要嚴格判斷當前對象是否被強引用,如果被強引用,則不會被垃圾回收


軟引用:軟引用一般被做為緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。


如果剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;如果剩餘內存相對富裕,則不會進行回收。


換句話說,虛擬機在發生OutOfMemory時,肯定是沒有軟引用存在的。


弱引用:弱引用與軟引用類似,都是作為緩存來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,因此其生命週期只存在於一個垃圾回收週期內。


強引用不用說,我們系統一般在使用時都是用的強引用。而“軟引用”和“弱引用”比較少見。他們一般被作為緩存使用,而且一般是在內存大小比較受限的情況下做為緩存。


因為如果內存足夠大的話,可以直接使用強引用作為緩存即可,同時可控性更高。因而,他們常見的是被使用在桌面應用系統的緩存。


六.Java中參數傳遞時傳值還是傳引用?


要說明這個問題,先要明確兩點:

1. 不要試圖與C進行類比,Java中沒有指針的概念

2. 程序運行永遠都是在棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

明確以上兩點後。Java在方法調用傳遞參數時,因為沒有指針,所以它都是進行傳值調用(這點可以參考C的傳值調用)。因此,很多書裡面都說Java是進行傳值調用,這點沒有問題,而且也簡化的C中複雜性。

但是傳引用的錯覺是如何造成的呢?在運行棧中,基本類型和引用的處理是一樣的,都是傳值,所以,如果是傳引用的方法調用,也同時可以理解為“傳引用值”的傳值調用,即引用的處理跟基本類型是完全一樣的。但是當進入被調用方法時,被傳遞的這個引用的值,被程序解釋(或者查找)到堆中的對象,這個時候才對應到真正的對象。如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是堆中的數據。所以這個修改是可以保持的了。


JVM調優:基本概念


JVM調優:基本概念

對象,從某種意義上說,是由基本類型組成的。可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則為樹的葉子節點。程序參數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個對象引用),則可以修改這個節點下面的所有內容

堆和棧中,棧是程序運行最根本的東西。程序運行可以沒有堆,但是不能沒有棧。而堆是為棧進行數據存儲服務,說白了堆就是一塊共享的內存。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能。

Java中,棧的大小通過-Xss來設置,當棧中存儲數據比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法返回的遞歸,因為此時棧中保存的信息都是方法返回的記錄點。

七. Java虛擬機中對象的訪問及存放


舉個實例Student stu=new Student();

這份代碼中Student stu是一個引用變量所以存放在java虛擬機棧上,new Student()是一個實例對象存放在java堆上。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。

由於reference 類型在Java 虛擬機規範裡面只規定了一個指向對象的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針。如果使用句柄訪問方式Java 堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息,如下圖所示。


JVM調優:基本概念

指針方式

Java 堆對象的佈局中就必須考慮如何放置訪問類型


JVM調優:基本概念

這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而引用對象本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。


分享到:


相關文章: