1 什么是JVM
JVM是JavaVirtualMachine(Java虛擬機(jī))的縮寫,JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī),是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。
2. JVM與操作系統(tǒng)
為什么要在程序和操作系統(tǒng)中間添加一個(gè)JVM? Java是一門抽象程度特別高的語言,提供了自動內(nèi)存管理等一系列的特性。這些特性直接在操作系統(tǒng)上實(shí)現(xiàn)是不太可能的,所以就需要JVM進(jìn)行一番轉(zhuǎn)換。
從圖中可以看到,有了JVM這個(gè)抽象層之后,Java就可以實(shí)現(xiàn)跨平臺了。JVM只需要保證能夠正確執(zhí)行.class文件,就可以運(yùn)行在諸如Linux、Windows、MacOS等平臺上了。 而Java跨平臺的意義在于一次編譯,處處運(yùn)行,能夠做到這一點(diǎn)JVM功不可沒。比如我們在Maven倉庫下載同一版本的jar包就可以到處運(yùn)行,不需要在每個(gè)平臺上再編譯一次。 現(xiàn)在的一些JVM的擴(kuò)展語言,比如Clojure、JRuby、Groovy等,編譯到最后都是.class文件,Java語言的維護(hù)者,只需要控制好JVM這個(gè)解析器,就可以將這些擴(kuò)展語言無縫的運(yùn)行在JVM之上了。
應(yīng)用程序、JVM、操作系統(tǒng)之間的關(guān)系
JVM上承開發(fā)語言,下接操作系統(tǒng),它的中間接口就是字節(jié)碼。
3. JVM、JRE、JDK的關(guān)系
JVM是Java程序能夠運(yùn)行的核心。但需要注意,JVM自己什么也干不了,你需要給它提供生產(chǎn)原料(.class文件)。 僅僅是JVM,是無法完成一次編譯,處處運(yùn)行的。它需要一個(gè)基本的類庫,比如怎么操作文件、怎么連接網(wǎng)絡(luò)等。而Java體系很慷慨,會一次性將JVM運(yùn)行所需的類庫都傳遞給它。JVM標(biāo)準(zhǔn)加上實(shí)現(xiàn)的一大堆基礎(chǔ)類庫,就組成了Java的運(yùn)行時(shí)環(huán)境,也就是我們常說的JRE(JavaRuntimeEnvironment) 對于JDK來說,就更龐大了一些。除了JRE,JDK還提供了一些非常好用的小工具,比如javac、java、jar等。它是Java開發(fā)的核心。 我們也可以看下JDK的全拼,JavaDevelopmentKit。JVM、JRE、JDK它們?nèi)咧g的關(guān)系,可以用一個(gè)包含關(guān)系表示。
4. Java虛擬機(jī)規(guī)范和Java語言規(guī)范的關(guān)系
左半部分是Java虛擬機(jī)規(guī)范,其實(shí)就是為輸入和執(zhí)行字節(jié)碼提供一個(gè)運(yùn)行環(huán)境。右半部分是我們常說的Java語法規(guī)范,比如switch、for、泛型、lambda等相關(guān)的程序,最終都會編譯成字節(jié)碼。而連接左右兩部分的橋梁依然是Java的字節(jié)碼。 如果.class文件的規(guī)格是不變,這兩部分是可以獨(dú)立進(jìn)行優(yōu)化的。但Java也會偶爾擴(kuò)充一下.class文件的格式,增加一些字節(jié)碼指令,以便支持更多的特性。 我們簡單看一下一個(gè)Java程序的執(zhí)行過程,它到底是如何運(yùn)行起來的。
這里的Java程序是文本格式的。比如下面這段HelloWorld.java,它遵循的就是Java語言規(guī)范。其中,我們調(diào)用了System.out等模塊,也就是JRE里提供的類庫。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
使用JDK的工具javac進(jìn)行編譯后,會產(chǎn)生HelloWorld的字節(jié)碼。 我們一直在說Java字節(jié)碼是溝通JVM與Java程序的橋梁,下面使用javap來稍微看一下字節(jié)碼到底長什么樣子。
0 getstatic#2<java/lang/System.out> //getstatic獲取靜態(tài)字段的值
3 ldc#3<HelloWorld> //ldc常量池中的常量值入棧
5 invokevirtual#4<java/io/PrintStream.println> //invokevirtual運(yùn)行時(shí)方法綁定調(diào)用方法
8 return //void函數(shù)返回
Java虛擬機(jī)采用基于棧的架構(gòu),其指令由操作碼和操作數(shù)組成。這些字節(jié)碼指令,就叫作opcode。其中, getstatic、ldc、invokevirtual、return等,就是opcode,可以看到是比較容易理解的。 JVM就是靠解析這些opcode和操作數(shù)來完成程序的執(zhí)行的。當(dāng)我們使用Java命令運(yùn)行.class文件的時(shí)候,實(shí)際上就相當(dāng)于啟動了一個(gè)JVM進(jìn)程。然后JVM會翻譯這些字節(jié)碼,它有兩種執(zhí)行方式。常見的就是解釋執(zhí)行,將opcode+操作數(shù)翻譯成機(jī)器代碼;另外一種執(zhí)行方式就是JIT,也就是我們常說的即時(shí)編譯,它會在一定條件下將字節(jié)碼編譯成機(jī)器碼之后再執(zhí)行。
Java語言的一個(gè)非常重要的特點(diǎn)就是與平臺的無關(guān)性。而使用Java虛擬機(jī)(JVM)是實(shí)現(xiàn)這一特點(diǎn)的關(guān)鍵。JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī),是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。之前市場上主要有三種主流的JVM,
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的J9 JVM
后來Sun公司和BEA公司都被oracle收購,并且oracle采用Sun公司的HotSpot和BEA公司的JRockit兩個(gè)JVM中精華退出了 jdk1.8 的JVM。
下圖是JVM的結(jié)構(gòu),非常重要,尤其是運(yùn)行時(shí)區(qū)域。
運(yùn)行時(shí)數(shù)據(jù)區(qū)
堆
JVM中最大的一塊,主要用來存放對象實(shí)例和數(shù)組,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。線程共享,內(nèi)部會劃分出多個(gè)線程私有的分配緩沖區(qū)(TLAB)。可以位于物理上不連續(xù)的空間,但是邏輯上要連續(xù)。Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的內(nèi)存區(qū)域,在虛擬機(jī)啟動的時(shí)候創(chuàng)建。
此內(nèi)存區(qū)域的目的就是存儲對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代理論設(shè)計(jì)的,所以Java堆中經(jīng)常出現(xiàn)“新生代”、“老年代”、“永久代”、“Eden空間”、“From Survivor空間”、“To Survivor空間”等名詞,這些區(qū)域劃分僅僅是部分垃圾收集器的共同特性或者設(shè)計(jì)風(fēng)格,而非某個(gè)Java虛擬機(jī)的具體實(shí)現(xiàn)的固有內(nèi)存布局,更不是《Java虛擬機(jī)規(guī)范》里對Java堆的進(jìn)一步劃分。后邊講到的G1垃圾收集器就不是基于分代理論設(shè)計(jì)的。
Java堆是線程共享的,它的目的是存放對象實(shí)例。同時(shí)它也是GC所管理的主要區(qū)域,因此常被稱為GC堆。根據(jù)虛擬機(jī)規(guī)范,Java堆可以存在物理上不連續(xù)的內(nèi)存空間,就像磁盤空間邏輯上是連續(xù)的即可。它的內(nèi)存大小可以設(shè)置為固定大小,也可以擴(kuò)展。當(dāng)前主流的虛擬機(jī)如HotSpot都能按擴(kuò)展實(shí)現(xiàn)(通過設(shè)置 -Xmx和-Xms,默認(rèn)堆內(nèi)存大小為服務(wù)器內(nèi)存的1/4),如果堆中沒有內(nèi)存完成實(shí)例分配,而且堆無法擴(kuò)展,則會報(bào)OOM錯(cuò)誤(OutOfMemoryError)。
2、虛擬機(jī)棧
每個(gè)方法被執(zhí)行的時(shí)候都會同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。線程私有,生命周期和線程一致。虛擬機(jī)棧是每個(gè)Java方法的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會創(chuàng)建一個(gè)"棧幀",用于存儲局部變量表(包括參數(shù))、操作棧、方法出口等信息。每個(gè)方法被調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧從入棧到出棧的過程。
平時(shí)說的棧一般指的是局部變量表部分。局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它并不等同于對象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對象的句柄或者其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址),這些數(shù)據(jù)類型在局部變量表中以槽(slot)來表示,其中64位長度的long和double類型的數(shù)據(jù)會占用兩個(gè)變量槽,其余的數(shù)據(jù)類型只占用一個(gè)。
局部變量表所需要的空間在編譯期完成分配,當(dāng)執(zhí)行一個(gè)方法的時(shí)候,該方法需要在棧幀中分配多大的局部變量表的空間完全是可以確定的,因此在方法運(yùn)行的期間不會改變局部變量表的大小,這里說的“大小”是指變量槽的數(shù)量,虛擬機(jī)真正使用多大的內(nèi)存空間來實(shí)現(xiàn)一個(gè)變量槽是由具體的虛擬機(jī)實(shí)現(xiàn)自行決定的事情。
該區(qū)域就是我們常說的Java內(nèi)存中的棧區(qū)域,該區(qū)域的局部變量表存儲的是基本類型、對象的引用類型,在對象的引用類型中存儲的是指向?qū)ο蟮亩芽臻g的地址。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(字節(jié)碼)服務(wù),本地方法棧為虛擬機(jī)使用到的native方法分為,底層調(diào)用的是C或者C++的方法。
《Java虛擬機(jī)規(guī)范》中對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)沒有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,HotSpot虛擬機(jī)直接就把本地方法棧和虛擬機(jī)棧合二為一來使用,與虛擬機(jī)棧一樣本地方法棧也會在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowError和OutOfMemoryError異常。
方法區(qū)(非堆)
屬于共享內(nèi)存區(qū)域,存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)是一個(gè)抽象的概念,JDK7及之前被稱為“永久代”,JDK8及以后被稱為“元空間”,它用于存儲虛擬機(jī)加載的類型信息、常量、靜態(tài)變量(JDK7及之前,JDK8及之后就把靜態(tài)變量與Class對象放到了堆中)、即時(shí)編譯器編譯(JIT)后的代碼等數(shù)據(jù),是各個(gè)線程的共享內(nèi)存區(qū)域。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫作“非堆”(Non-Heap),目的是與Java堆區(qū)分開來。
JDK8之前,很多人把方法區(qū)又稱為永久代(Permanent Generation),或?qū)烧呋鞛橐徽劊举|(zhì)上是不對等的,因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把收集器的分代擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去了專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是這種設(shè)計(jì)更會導(dǎo)致Java應(yīng)用更容易遇到內(nèi)存溢出的問題(永久代有-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小,而像J9和JRockit只要沒有觸碰到進(jìn)程可用內(nèi)存的上限,就不會有問題),在JDK6的時(shí)候HotSpot團(tuán)隊(duì)就有放棄方法區(qū),逐步改為采用本地內(nèi)存(Native Memory)來實(shí)現(xiàn)方法區(qū)的計(jì)劃,到了JDK7已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,到了JDK8就完全廢除了永久代的概念,改用JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Meta-space),把JDK7中永久代剩余的內(nèi)容(主要是類型信息)全部移到元空間中。
方法區(qū)存儲每個(gè)類信息,如:
在JDK8之前的HotSpot JVM,存放這些“永久的”區(qū)域叫做“永久代(permanent generation)”。永久代是一片連續(xù)的堆空間,在JVM啟動前通過在命令行設(shè)置參數(shù)-XX:MaxPermSize來設(shè)定永久代最大可分配的內(nèi)存空間,默認(rèn)大小為64M(64位的JVM默認(rèn)是85M)。
方法區(qū)或永生代相關(guān)參數(shù)配置
- -XX:PermSize=64MB最小尺寸,初始分配
- -XX:MaxPermSize=256最大允許分配尺寸
- 按需分配-XX:+CMSClassUnloadingEnabled
- -XX:+CMSPermGenSweepingEnabled設(shè)置垃圾不回收
- -server選項(xiàng)下默認(rèn)MaxPermSize為64MB,-client選項(xiàng)下默認(rèn)MaxPermSize為32MB
Java虛擬機(jī)規(guī)范堆方法區(qū)限制非常的寬松,可選擇不垃圾回收,以及不需要連續(xù)的內(nèi)存和可擴(kuò)展的大小。這個(gè)區(qū)域主要是針對于常量池的回收以及對類型的卸載,當(dāng)方法區(qū)無法分配到足夠的內(nèi)存的時(shí)候也會報(bào)OOM。
常量池
Class文件常量池
以下使用實(shí)際代碼及反編譯Class文件講解
反編譯命令:javap -verbose StringTest.class
public class StringTest {
private static String s1 = "static";
public static void main(String[] args) {
String hello1 = new String("hell") + new String("o");
String hello2 = new String("he") + new String("llo");
String hello3 = hello1.intern();
String hello4 = hello2.intern();
System.out.println(hello1 == hello3);
System.out.println(hello1 == hello4);
}
}
Classfile /E:/workspace/VariousCases/target/classes/cn/onenine/jvm/constantpool/StringTest.class
Last modified 2021-8-3; size 1299 bytes
MD5 checksum 338bd0034155ec3bf8d608540a31761c
Compiled from "StringTest.java"
public class cn.onenine.jvm.constantpool.StringTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // cn/onenine/jvm/constantpool/StringTest
#2 = Utf8 cn/onenine/jvm/constantpool/StringTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 s1
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = String #11 // static
#11 = Utf8 static
#12 = Fieldref #1.#13 // cn/onenine/jvm/constantpool/StringTest.s1:Ljava/lang/String;
#13 = NameAndType #5:#6 // s1:Ljava/lang/String;
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 <init>
#17 = Methodref #3.#18 // java/lang/Object."<init>":()V
#18 = NameAndType #16:#8 // "<init>":()V
#19 = Utf8 this
#20 = Utf8 Lcn/onenine/jvm/constantpool/StringTest;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Class #24 // java/lang/StringBuilder
#24 = Utf8 java/lang/StringBuilder
#25 = Class #26 // java/lang/String
#26 = Utf8 java/lang/String
#27 = String #28 // hell
#28 = Utf8 hell
#29 = Methodref #25.#30 // java/lang/String."<init>":(Ljava/lang/String;)V
#30 = NameAndType #16:#31 // "<init>":(Ljava/lang/String;)V
#31 = Utf8 (Ljava/lang/String;)V
#32 = Methodref #25.#33 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#33 = NameAndType #34:#35 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#34 = Utf8 valueOf
#35 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
#36 = Methodref #23.#30 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#37 = String #38 // o
#38 = Utf8 o
#39 = Methodref #23.#40 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = NameAndType #41:#42 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = Utf8 append
#42 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = Methodref #23.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#44 = NameAndType #45:#46 // toString:()Ljava/lang/String;
#45 = Utf8 toString
#46 = Utf8 ()Ljava/lang/String;
#47 = String #48 // he
#48 = Utf8 he
#49 = String #50 // llo
#50 = Utf8 llo
#51 = Methodref #25.#52 // java/lang/String.intern:()Ljava/lang/String;
#52 = NameAndType #53:#46 // intern:()Ljava/lang/String;
#53 = Utf8 intern
#54 = Fieldref #55.#57 // java/lang/System.out:Ljava/io/PrintStream;
#55 = Class #56 // java/lang/System
#56 = Utf8 java/lang/System
#57 = NameAndType #58:#59 // out:Ljava/io/PrintStream;
#58 = Utf8 out
#59 = Utf8 Ljava/io/PrintStream;
#60 = Methodref #61.#63 // java/io/PrintStream.println:(Z)V
#61 = Class #62 // java/io/PrintStream
#62 = Utf8 java/io/PrintStream
#63 = NameAndType #64:#65 // println:(Z)V
#64 = Utf8 println
#65 = Utf8 (Z)V
#66 = Utf8 args
#67 = Utf8 [Ljava/lang/String;
#68 = Utf8 hello1
#69 = Utf8 hello2
#70 = Utf8 hello3
#71 = Utf8 hello4
#72 = Utf8 StackMapTable
#73 = Class #67 // "[Ljava/lang/String;"
#74 = Utf8 SourceFile
#75 = Utf8 StringTest.java
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String static
2: putstatic #12 // Field s1:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
public cn.onenine.jvm.constantpool.StringTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #17 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/onenine/jvm/constantpool/StringTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=5, args_size=1
0: new #23 // class java/lang/StringBuilder
3: dup
4: new #25 // class java/lang/String
7: dup
8: ldc #27 // String hell
10: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
13: invokestatic #32 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #36 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: new #25 // class java/lang/String
22: dup
23: ldc #37 // String o
25: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #39 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: new #23 // class java/lang/StringBuilder
38: dup
39: new #25 // class java/lang/String
42: dup
43: ldc #47 // String he
45: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
48: invokestatic #32 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
51: invokespecial #36 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
54: new #25 // class java/lang/String
57: dup
58: ldc #49 // String llo
60: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
63: invokevirtual #39 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
66: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
69: astore_2
70: aload_1
71: invokevirtual #51 // Method java/lang/String.intern:()Ljava/lang/String;
74: astore_3
75: aload_2
76: invokevirtual #51 // Method java/lang/String.intern:()Ljava/lang/String;
79: astore 4
81: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
84: aload_1
85: aload_3
86: if_acmpne 93
89: iconst_1
90: goto 94
93: iconst_0
94: invokevirtual #60 // Method java/io/PrintStream.println:(Z)V
97: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
100: aload_1
101: aload 4
103: if_acmpne 110
106: iconst_1
107: goto 111
110: iconst_0
111: invokevirtual #60 // Method java/io/PrintStream.println:(Z)V
114: return
LineNumberTable:
line 13: 0
line 14: 35
line 15: 70
line 16: 75
line 17: 81
line 18: 97
line 20: 114
LocalVariableTable:
Start Length Slot Name Signature
0 115 0 args [Ljava/lang/String;
35 80 1 hello1 Ljava/lang/String;
70 45 2 hello2 Ljava/lang/String;
75 40 3 hello3 Ljava/lang/String;
81 34 4 hello4 Ljava/lang/String;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 93
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "StringTest.java"
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項(xiàng)信息是常量表(Constant Pool Table),用于存放編譯器生成的各種字面量和引用符號,這部分內(nèi)容將在類加載后放到運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對于Class文件常量池的另外一個(gè)特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行時(shí)期也可以將新的常量放入運(yùn)行時(shí)常量池,如String#intern()方法。
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)的內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存時(shí),就會拋出OutOfMemoryErro異常。
全局字符串常量池
HotSpot VM里,記錄intered字符串的一個(gè)全局表叫做String Table,它本質(zhì)上就是一個(gè)HashSet,是一個(gè)純運(yùn)行時(shí)的結(jié)構(gòu),而且是惰性維護(hù)的。
只存儲對java.lang.String實(shí)例的引用,而不存儲實(shí)際的String對象,根據(jù)這個(gè)引用可以找到實(shí)際的String對象。
直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError 異常出現(xiàn)。在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲在Java堆里面的DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava 堆和Native 堆中來回復(fù)制數(shù)據(jù)。直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁使用,而且也可能導(dǎo)致OOM。
本機(jī)內(nèi)存直接內(nèi)存分配不會受到Java堆大小的限制,但是既然是內(nèi)存,肯定會受到物理機(jī)內(nèi)存的限制,當(dāng)我們通過-Xmx設(shè)置堆的最大內(nèi)存時(shí),不要忘了還有直接內(nèi)存,如果堆內(nèi)存設(shè)置過大,將會導(dǎo)致直接內(nèi)存不夠用,導(dǎo)致動態(tài)擴(kuò)展時(shí)發(fā)生OOM。
直接內(nèi)存的容量大小可以通過-XX:MaxDirectMemorySize參數(shù)來指定,如果不指定,則默認(rèn)與Java堆的最大值(-Xmx)一致。
直接內(nèi)存導(dǎo)致的OOM不會在Heap Dump文件中看到什么明顯的異常,如果發(fā)現(xiàn)內(nèi)存溢出后的Dump文件很小而程序中又直接或間接使用了DirectMemory(典型的間接使用就是NIO),那就可以考慮重點(diǎn)檢查一下直接內(nèi)存方面的原因了。
堆的分代收集
JVM有多種垃圾回收算法,其中目前在用最經(jīng)典的就是分代收集算法,這里優(yōu)先記錄下。
- 永久代(Perm):主要保存class,method,field等對象,該空間大小,取決于系統(tǒng)啟動加載類的數(shù)量,一般該區(qū)域內(nèi)存溢出均是啟動時(shí)溢出。java.lang.OutOfMemoryError: PermGen space
- 老年代(Old):一般是經(jīng)過多次垃圾回收(GC)沒有被回收掉的對象。
- 新生代(Eden):新創(chuàng)建的對象。
- 新生代(Survivor0):經(jīng)過垃圾回收(GC)后,沒有被回收掉的對象。
- 新生代(Survivor1):同Survivor0相同,大小空間也相同,同一時(shí)刻Survivor0和Survivor1只有一個(gè)在用,一個(gè)為空。
Java堆是垃圾收集器管理的主要區(qū)域,按照分代收集算法的劃分,堆內(nèi)存空間可以繼續(xù)細(xì)分為年輕代,老年代。年輕代又可以劃分為較大的Eden區(qū),兩個(gè)同等大小的From Survivor,To Survivor區(qū)。默認(rèn)的Eden區(qū)和Survivor區(qū)的大小比例為8:1:1。在為新創(chuàng)建的對象分配內(nèi)存的時(shí)候先將對象分配到Eden區(qū)和From Survivor區(qū),在立即回收時(shí),會將Eden區(qū)和Survivor區(qū)還存活的對象復(fù)制到To Survivor區(qū)中,如果To Survivor區(qū)的大小不能容納存活的對象,會把存活的對象分配到老年區(qū)。總體來說,新創(chuàng)建的小對象會放在年輕代,年輕代的對象大多在下一次垃圾回收時(shí)被回收,老年代存儲大的對象和存活時(shí)間長的對象。
附錄:相關(guān)概念
1、基本類型和引用類型
JVM中,數(shù)據(jù)類型可以分為兩類:基本類型和引用類型。基本類型的變量保存原始值,即:他代表的值就是數(shù)值本身;而引用類型的變量保存引用值。“引用值”代表了某個(gè)對象的引用,而不是對象本身,對象本身存放在這個(gè)引用值所表示的地址的位置。
2、堆和棧
棧是運(yùn)行時(shí)的單位,而堆是存儲的單位。在Java中一個(gè)線程就會相應(yīng)有一個(gè)線程棧與之對應(yīng),這點(diǎn)很容易理解,因?yàn)椴煌木€程執(zhí)行邏輯有所不同,因此需要一個(gè)獨(dú)立的線程棧。而堆則是所有線程共享的。棧因?yàn)槭沁\(yùn)行單位,因此里面存儲的信息都是跟當(dāng)前線程(或程序)相關(guān)信息的。包括局部變量、程序運(yùn)行狀態(tài)、方法返回值等等;而堆只負(fù)責(zé)存儲對象信息。
**棧代表了處理邏輯,而堆代表了數(shù)據(jù)。 **
堆中存的是對象,棧中存的是基本數(shù)據(jù)類型和堆中對象的引用。
在Java中,Main函數(shù)就是棧的起始點(diǎn),也是程序的起始點(diǎn)。
3、引用類型
對象引用類型分:強(qiáng)引用、軟引用、弱引用和虛引用。
強(qiáng)引用:不會被回收。就是我們一般聲明對象是時(shí)虛擬機(jī)生成的引用,強(qiáng)引用環(huán)境下,垃圾回收時(shí)需要嚴(yán)格判斷當(dāng)前對象是否被強(qiáng)引用,如果被強(qiáng)引用,則不會被垃圾回收。
軟引用:內(nèi)存充足,則不回收;不足則回收。軟引用一般被做為緩存來使用。與強(qiáng)引用的區(qū)別是,軟引用在垃圾回收時(shí),虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的剩余內(nèi)存來決定是否對軟引用進(jìn)行回收。如果剩余內(nèi)存比較緊張,則虛擬機(jī)會回收軟引用所引用的空間;如果剩余內(nèi)存相對富裕,則不會進(jìn)行回收。換句話說,虛擬機(jī)在發(fā)生OutOfMemory時(shí),肯定是沒有軟引用存在的。
弱引用:會被回收。弱引用與軟引用類似,都是作為緩存來使用。但與軟引用不同,弱引用在進(jìn)行垃圾回收時(shí),是一定會被回收掉的,因此其生命周期只存在于一個(gè)垃圾回收周期內(nèi)。