說明:本篇博客屬于讀書筆記,大量參考《深入理解Java虛擬機》這本書
JVM的內(nèi)存
程序計數(shù)器
- 程序計數(shù)器是線程私有的,每一個線程都有自己的一個程序計數(shù)器,并且互不干擾,程序計數(shù)器相當(dāng)于當(dāng)前代碼所執(zhí)行指令的指針,控制了當(dāng)前線程的執(zhí)行流程,當(dāng)Java程序在執(zhí)行Java方法的時候,程序計數(shù)器記錄的是當(dāng)前執(zhí)行代碼的指令地址,當(dāng)Java程序正在執(zhí)行Native方法,程序計數(shù)器則為空(Undefined),程序計數(shù)器是不會拋出OOM異常的
Java虛擬機棧
- Java虛擬機棧也是線程私有的,它的生命周期與線程的生命周期相同,Java程序在執(zhí)行一個方法的時候都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量,方法出口等信息,每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)一個棧幀在虛擬機棧中的入棧到出棧的過程,虛擬機棧中存儲這Java的基本數(shù)據(jù)類型以及對象的引用,在Java虛擬機棧中會拋出StackOverflowError異常和OOM異常,下面來看一個demo:
public class DemoMain {
public static void main(String[] args) {
System.out.println("test");
DemoMain.testMethod();
System.out.println("end");
}
public static void testMethod() {
testMethod();
}
}
如上的應(yīng)用程序運行之后,在我的機器上會拋出StackOverflowError:
test
Exception in thread "main" java.lang.StackOverflowError
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
當(dāng)虛擬機在執(zhí)行方法testMethod的時候,這時候就會在Java虛擬機棧上創(chuàng)建一個棧幀,然后入棧,然而在testMethod方法內(nèi)又不斷的遞歸調(diào)用testMethod方法,導(dǎo)致Java虛擬機棧不斷的嵌套執(zhí)行testMethod方法,不斷的創(chuàng)建testMethod的棧幀,然后入棧,而testMethod并沒有執(zhí)行完成,所以testMethod對應(yīng)的棧幀不會出棧,當(dāng)Java虛擬機棧中的棧深度超過了虛擬機允許的深度,這時候就拋出了StackOverflowError異常了,如果虛擬機可以動態(tài)拓展,在新的棧幀入棧的時候再去申請內(nèi)存,要是申請不到足夠的內(nèi)存,此時就會拋出OOM異常了
本地方法棧
- 本地方法棧是線程私有的,存儲Native方法的信息,這個內(nèi)存區(qū)域也會拋出StackOverflowError和OOM異常
Java堆
- Java堆是線程共享的,這是虛擬機中內(nèi)存最大的一塊,它唯一目的就是用來存放對象實例的,就是:
Object obj = new Object();
obj是對象的引用,存儲在Java虛擬機棧中,而new出來的Object對象實例就存儲在Java堆中,obj引用指向Java堆中實例的地址,Java堆是垃圾回收管理的主要區(qū)域,Java堆的內(nèi)存空間不需要物理上的連續(xù),只需要邏輯上的連續(xù)即可,Java堆也會拋出StackOverflowError 和OOM異常
方法區(qū)
- 方法區(qū)是線程共享的內(nèi)存區(qū)域,它用來存儲已經(jīng)被虛擬機加載的類信息(類名,類字段,方法名等),常量(final修飾),靜態(tài)變量(static修飾)等,此區(qū)域也會拋出OOM異常
運行時常量池
- 運行時常量池是方法區(qū)的一部分,常量池用于存放編譯期生成的各種字面量(文本字符串、聲明為final的常量值等)和符號引用(類和接口的完全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符),運行時常量池具有動態(tài)性,也就是說不一定是預(yù)置到class中的常量才能進(jìn)入運行時常量池,在運行期間也可能將新的常量放入池中,例如String類的intern(),該內(nèi)存區(qū)域也會拋出OOM異常
字符串常量池
- 字符串常量池也是方法區(qū)的一部分,用來存放字符串常量,舉個例子:
public class DemoMain {
public static void main(String[] args) {
System.out.println("test");
String s1 = "s1";
String s2 = "s1";
String s3 = new String("s1");
System.out.println(s1 == s2);
System.out.println("end");
}
}
以上運行的結(jié)果是:
test
true
end
也就是說是s1指向的地址和s2指向的地址是一樣的,為什么?因為“s1”這個字符串常量被存儲到了字符串常量池中了,虛擬機發(fā)現(xiàn)了s1對象指向“s1”,s2對象也指向“s1”,因此不會再次創(chuàng)建一個“s1”,而是將s1和s2對象都指向存儲于常量址的“s1”,這里就做到了常量池的對象共享,節(jié)省內(nèi)存
對象創(chuàng)建的過程
- 在JVM中當(dāng)收到一個new指令的時候首先會去常量池中檢查是否存在這個類的符號引用,并檢查這個類是否已經(jīng)被加載,解析和初始化過,如果沒有,那就先執(zhí)行類加載過程
- 類加載檢查過后,接下來JVM就會為新生的對象分配內(nèi)存,對象所需要的內(nèi)存空間大小在類加載的時候就能夠確定,內(nèi)存分配其實就是在Java堆中開辟一塊確定大小的內(nèi)存出來,Java堆的內(nèi)存分配有兩種,第一種是“指針碰撞”,當(dāng)Java堆中的內(nèi)存是規(guī)整的,即用過的內(nèi)存都在一邊,空閑的內(nèi)存在另一邊,那么此時的內(nèi)存分配就是把指針指向空閑內(nèi)存空間挪動一段于對象大小相同的距離;第二種是“空閑列表”,當(dāng)Java堆中使用內(nèi)存和空閑內(nèi)存相互交錯的時候,此時JVM必須維護(hù)一個列表,記錄哪些內(nèi)存是可用的,在分配內(nèi)存的時候從列表中尋找一塊足夠大的空間劃分給對象,并更新列表上的記錄,具體選擇哪一種內(nèi)存分配的方式取決于Java堆內(nèi)存是否規(guī)整,而Java堆內(nèi)存是否規(guī)整取決于GC回收器是否又壓縮整理功能
- 對象內(nèi)存的分配過程還要注意多線程問題,假如在給一個對象分配內(nèi)存的時候,指針還沒來得及修改,此時又要操作指針給另一個對象也分配內(nèi)存,解決這個問題又兩種方案,一種是堆內(nèi)存分配動作進(jìn)行同步操作,另一種是預(yù)先給每一個線程在Java堆中分配一塊小內(nèi)存,成為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),那么只有在分配TLAB的時候才需要同步處理,對象內(nèi)存分配完了之后就要對對象中的值進(jìn)行初始化為零值,最后再執(zhí)行<init>方法,也就是構(gòu)造方法,來初始化對象中的值,這樣一個對象才算完全創(chuàng)建成功
-
所以總結(jié)下對象創(chuàng)建的過程大致分為以下幾個階段
image.png
對象的訪問定位
- Java虛擬機棧中存儲的是對象的引用,Java堆中存儲的才是對象的實際數(shù)據(jù),對象的訪問定位通常有兩種,一種是句柄訪問,一種是指針訪問,句柄訪問就是在Java堆中有一個句柄池,句柄池中才是存儲了對象地址,而JVM棧中的對象引用存的是對象的句柄地址,也就是說reference指向句柄,句柄指向?qū)ο?,這么做的好處就是對象要是移動,JVM棧中的reference不用做修改,只要修改句柄就行了;指針訪問就是JVM棧中的reference直接存的就是對象地址,reference直接指向JVM堆中的對象,這么做的好處就是訪問對象速度快,要是對象被頻繁的訪問,那指針訪問的方式將有明顯的效率提升
內(nèi)存溢出
Java堆內(nèi)存溢出
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new Object());
}
}
不斷的分配對象,并添加到list當(dāng)中,這樣對象就不會被回收,程序跑一會兒就報Java堆的OOM異常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
什么情況可能會導(dǎo)致Java堆的內(nèi)存泄漏?很明顯,內(nèi)存泄漏,一些對象創(chuàng)建后,一直被持有導(dǎo)致GCRoot一直存在,所以不會被回收
虛擬機棧和本地方法棧溢出
- 虛擬機棧和本地方法棧都會拋出StackOverflowError和OutOfMemoryError,對于StackOverflowError異常會有兩種情況,一種是虛擬機棧的深度大于虛擬機規(guī)定的最大深度,另一種是在申請棧幀內(nèi)存的時候沒有足夠的內(nèi)存,這時候也會拋出這個異常
public class DemoMain {
int i = 0;
public static void main(String[] args) {
DemoMain demoMain = new DemoMain();
try {
demoMain.test();
} catch (Throwable e) {
System.err.println("stack:" + demoMain.i);
e.printStackTrace();
}
}
public void test() {
i++;
test();
}
}
運行結(jié)果:
stack:34879
java.lang.StackOverflowError
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
- 線程創(chuàng)建造成的內(nèi)存溢出
public class DemoMain {
public static void main(String[] args) {
DemoMain demoMain = new DemoMain();
demoMain.createThread();
}
public void createThread() {
while (true) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:714)
at com.lhd.jvmdemo1.DemoMain.createThread(DemoMain.java:26)
at com.lhd.jvmdemo1.DemoMain.main(DemoMain.java:12)
這個是本地方法區(qū)拋出的OOM異常
方法區(qū)和常量池溢出
- 方法區(qū)拋出的OOM本機沒有模擬出來,不過方法區(qū)的OOM異常是:
java.lang.OutOfMemoryError:PermGen space
在android開發(fā)中,如果一個apk的類非常多,安裝這個apk的時候就可能出現(xiàn)方法區(qū)的內(nèi)存不夠用導(dǎo)致方法區(qū)內(nèi)存溢出
