一、java運行時數據區
也可以稱為java內存區域,這是一種規范,具體實現和使用哪種虛擬機有關。運行時數據區和java內存模型不是一回事,不要弄混。
官方文檔地址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
1.1、方法區
線程共享,類裝載過程中產生的java.lang.Class對象保存在方法區,而不是堆,請參考《深入理解java虛擬機》P215。
jdk1.8之前HotSpot通過永久帶實現方法區,為了對方法區的GC可以像堆一樣管理內存,能夠復用代碼,其他虛擬機沒有永久帶的概念,永久代的設計實現方法去並不是一個好的選擇,因為更容易出現內存溢出。
方法區主要存放類信息、常量、靜態變量、即時編譯后的代碼等。
垃圾回收主要是針對常量池回收和類型的卸載,這塊區域的回收很難,尤其是類型卸載,可以選擇不進行垃圾回收,但是回收很有必要的。
PS:jdk1.8及以后,方法區被移除,通過Metaspace實現,而Metaspace使用的是直接內存,可以使用參數:-XX:MetaspaceSize
來指定元
數據區的大小。
Tomcat中配置打印GC相關信息:可以證明MetaSpace的存在
JAVA_OPTS="-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log"
然后直接打開,或者通過GC分析工具打開,就可以發現存在Metaspace內存,Server模式下默認使用Parallel垃圾收集器
MetaSpace實現方法區和之前通過PermGen Space實現的區別
1、MetaSpace使用的是本地內存,PermGen使用的是jvm內存
2、java.lang.OutOfMemoryError: PermGen Space這個異常不存在了
3、字符串常量池存放在PermGen,容易出現性能問題和內存溢出,所以jdk1.7移動到了堆中
4、類、方法等信息大小比較難確定,所以很難直接設置PermGen的大小
5、永久代會為 GC 帶來不必要的復雜度,並且回收效率偏低
6、還有一個很重要的點,要合並HotSpot和JRockit的代碼,而JRockit沒有MetaSpace,事實也證明了通過PermGen實現是一個錯誤的選擇
1.2、虛擬機棧
線程私有,生命周期和線程相同,每執行一個方法都會創建一個棧幀,從執行到結束,對應着棧幀在虛擬機棧的入棧到出棧過程,可以類比數據結構中的棧,java方法兩種返回方式:
1、return語句
2、拋出異常
這兩種方式都會導致棧幀被彈出。
棧幀:
保存着局部變量表、操作數棧、方法出入口等。
局部變量表:
用來保存方法參數和返回值,也就是基本數據類型、對象的引用、returnAddress類型(指向一個字節碼指令的地址)。
double和long占用兩個局部變量空間(variable slot),其余占用1個,slot空間大小在編譯期間就確定,方法運行期間無法改變,也就是當程序發生異常,打印的堆棧信息,就是虛擬機棧。
舉個栗子:解釋局部變量表和操作數棧
public static Integer f1() { int a = 1; int b = 2; return a + b; }
我們通過javap進行反編譯查看字節碼知道,1和2這種int類型保存在局部變量表,而a+b的操作是從局部變量表中load數據到操作數棧,然后完成加法的操作。
PS:可能出現Stack OverflowError、OutOfMemoryError錯誤。
1.3、本地方法棧
線程私有,和虛擬機棧相似,一個為Java方法服務,一個為了本地方法服務,本地方法棧中方法實現的語言、方式等沒有規定,由具體的虛擬機確定,在HotSpot中只有棧,沒有虛擬機棧和本地方法棧的區別,其他的虛擬機如J9、JRocket等可能實現就不同,我們默認使用都是HotSpot。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用於存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。
PS:可能出現Stack OverflowError、OutOfMemoryError錯誤
1.4、堆
線程共享,這是虛擬機內存最大的一塊區域,也是GC的主要區域,幾乎所有的對象和數組都保存在這里。
內存回收的角度分為:
新生代:Eden Space、From Survivor、To Survivor
老年代:
1、主要用來保存大對象(可以通過-XX:PretenureSizeThreshold 設置大對象的閥值)。
2、或者從新生代經過15次 minor GC存活下來的對象(-XX:MaxTenuringThreshold)。
3、第二條不是絕對的,VM動態判斷,如果Survivor空間中相同年齡所有對象大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold要求的年齡。
默認Eden Space:From Survivor:To Survivor=8:1:1,可以通過-XX:SurvivorRatio調節,不同垃圾收集器的調優策略不同的,所以不要百度到需要調節這個參數,就認為一定有效。
-XX:NewRatio:為Young區和Old區的比例
PS:進一步划分的目的是更好地回收內存,或者更快地分配內存。
1.5、程序計數器
線程私有,是一塊很小的內存區域,記錄着當前虛擬機字節碼指令的地址(對於JNI,值為undefined),字節碼解釋器通過改變計數器的值來選擇下一條執行的字節碼,分支、循環、跳轉、異常處理、線程恢復等功能都要依靠計數器。
線程上下文切換的時候,為了能夠恢復到正確的執行位置,需要每個線程都擁有一個獨立的線程計數器,互不影響。
PS:唯一一個沒有規定OOM的區域
1.6、直接內存
不屬於Java內存區域,有可能出現OOM,jdk1.4出現了NIO,它可以通過Native Libraries分配堆外內存,通過Java堆中的DirectByteBuffer對象作為引用進行操作,在某些場景明顯提高性能,以為避免了Java堆和Native堆來回復制數據。
直接內存的分配不受Java堆大小限制,而是收到本機總內存的限制。
二、jvm的內存結構
上面說了jvm運行時數據區是一種規范,而對於HotSpot來說,堆區和非堆區(就是jdk1.8之前的方法區)的內存結構如下:
堆區的結構在上面有介紹過,在jdk1.8中,方法區由Metaspace實現,包含CCS(壓縮類空間),只有啟用短指針才會存在這部分內存。
Metaspace:
存放的就是方法區的的數據。包含Class、Package、Method、Field、字節碼、常量池、符號引用等
CCS:
堆中的對象都有一個指向class對象的指針,64位系統中每個指針都是64位,為了性能考慮,使用短指針32位的,如果使用短指針就會啟用壓縮類空間,將這些class對象保存在CCS當中。也就是保存着32位指針的Class。
CodeCache:
JIT即使編譯的Native代碼、JNI使用的C代碼
我們可以驗證一下CCS的啟用與關閉:默認開啟
通過jps -l查找Java進程,然后通過
[root@iZuf6fkfhthmdm1nwdg5isZ bin]# jstat -gc 23631 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 20480.0 19968.0 0.0 0.0 283136.0 144816.6 52736.0 13931.5 35416.0 34236.6 4480.0 4208.6 10 0.215 2 0.220 0.435
然后在Catalina.sh中JAVA_OPTS加入-XX:-UseCompressedClassPointers進行禁用,然后重啟
[root@iZuf6fkfhthmdm1nwdg5isZ bin]# jstat -gc 24008 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 20992.0 20480.0 0.0 0.0 282112.0 160448.0 51712.0 14654.7 35416.0 34133.3 0.0 0.0 10 0.192 2 0.153 0.345
驗證CodeCache的存在:
因為CodeCache保存的是即時編譯的代碼的代碼,我們通過-Xint解釋執行的方式啟動,當然啟動肯定會變慢的,因為默認以Mix方式啟動然后重啟Tomcat
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
19456.0 20480.0 14800.5 0.0 282624.0 260677.0 52224.0 22069.4 32000.0 30489.7 0.0 0.0 10 0.140 1 0.065 0.205
我們發現MC(Metaspace Capacity)變小了,也證明了CodeCache的存在
三、常量池
因為大家一般都是通過周志明老師的書里面學習jvm的內容,對於常量池的部分的講解很容易讓人搞懵逼,最開始說Class文件中包含常量池,指的是Class常量池,又說運行時常量池可以通過intern()動態添加字符串,看到這里我都蒙了,intern()是操作字符串常量池的,難道字符串常量池是運行時常量池的一部分嗎?
而實現方法區的內存溢出是就是通過intern()實現,說的也是運行時常量池導致的內存溢出,所以我只能認為字符串常量池是運行時常量池的一部分,而且jdk1.7之后,運行時常量池和字符串常量池都從方法區移到堆中,所以,我不得不相信
不知道理解的對不對,有不同意見的可以評論提出來。。。。
2.1、字符串常量池
在HotSpot中通過StringTable類實現功能,StringTable是一個hash表,默認長度大小1009,被所有類共享。字符串常量由字符組成,保存在StringTable上面。在jdk1.6當中,StringTable的長度是固定1009,如果存放在StringTable中的字符串很多,造成hash沖突的幾率很大,鏈表過長,當通過String.intern()查找String Pool時,性能就會降低。
在jdk1.7當中,StringTable的長度可以通過-XX:StringTableSize設置
存放的內容:
String.intern()主要是為了復用字符串常量,節省內存空間
在jdk1.6及以前的版本,存放的都是字符串常量,使用""聲明的字符串都存儲在這,例如:String str = "abc";
在jdk1.7之后,String.intern()發生變化,如果字符串常量池不存在這個String對象,如果堆區存在這個對象,直接復制到字符串常量池,否則還是要創建字符串,然后返回字符串對象的引用。因此除了字符串常量,也可以存放堆中字符串常量的引用
PS:在jdk1.7之后,字符串常量池從方法區轉移到堆中
2.2、class常量池
首先java代碼通過javac編譯成class文件,class文件中保存着類的相關信息(版本,類、字段、方法、接口等信息),除此之外,還有Class常量池,用來存放編譯器產生的各種字面量(Literal)和符號引用(Symbolic References),每個class文件都有一個class常量池。
字面量:1.String 2.基本數據類型 3.聲明final的常量
符號引用:1.類和方法的全限定名(類似於com.cfets.**.**這種) 2.字段的名稱和描述符 3.方法的名稱和描述符
2.3、運行時常量池
就是class常量池被加載到方法區之后的版本,區別就是:字面量可以通過String.intern()動態添加,符號引用解析為直接引用(類加載的解析階段)
當類加載到內存之中,jvm會把class常量池的內存存放到運行時常量池,所以,運行時常量池也是每個class都有的。
符號引用:上述有說明。以一組符號來描述所引用的目標,只要能定位到目標,無論是任何形式的字面量,和jvm實現的內存布局無關。
直接引用:直接指向目標的指針、相對偏移量或者間接定位到目標的句柄,句柄和指針對應對象的訪問定位方式,和jvm實現的內存布局有關。
PS:JDK1.7及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池.所以jdk1.7之后,運行時常量池和字符串常量池都從方法區移到堆中
四、HotSpot中編譯代碼的方式
解釋執行:
逐條翻譯字節碼為可運行的機器碼,優勢在於不用等待。
即時編譯:
以方法為單位將字節碼翻譯成機器碼,實際運行當中效率更高。
在HotSpot中默認采用混合模式,其先解釋執行字節碼,然后將其中的熱點代碼(多次執行,循環等)直接編譯成機器碼,下次就不用再編譯了,讓其更快速地運行。使用混合模式的原因有二八定律和jvm優化的考慮在里面
通過java -version命令可以查看:
# java -version java version "1.8.0_102" Java(TM) SE Runtime Environment (build 1.8.0_102-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)
通過-Xint(解釋執行), -Xcomp(即時編譯), 和-Xmixed設置編譯方式,不過一般情況下不需要修改。