1、結(jié)合字節(jié)碼指令理解Java虛擬機(jī)棧和棧幀
棧幀:每個(gè)棧幀對(duì)應(yīng)一個(gè)被調(diào)用的方法,可以理解為一個(gè)方法的運(yùn)行空間。
每個(gè)棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、指向運(yùn)行時(shí)常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
局部變量表:方法中定義的局部變量以及方法的參數(shù)存放在這張表中,局部變量表中的變量不可直接使用,如需要使用的話,必須通過相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)使用。
操作數(shù)棧:以壓棧和出棧的方式存儲(chǔ)操作數(shù)的。
動(dòng)態(tài)鏈接:每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。
方法返回地址:當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式可以退出,一種是遇到方法返回的字節(jié)碼指令;一種是遇見異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理。
class Person{
private String name="Jack";
private int age;
private final double salary=100;
private static String address;
private final static String hobby="Programming";
public void say(){
System.out.println("person say...");
}
public static int calc(int op1,int op2){
op1=3;
int result=op1+op2;
return result;
}
public static void order(){
}
public static void main(String[] args){
calc(1,2);
order();
}
}
Compiled from "Person.java" class Person {
...
public static int calc(int, int);
Code:
0: iconst_3 //將int類型常量3壓入[操作數(shù)棧]
1: istore_0 //將int類型值存入[局部變量0]
2: iload_0 //從[局部變量0]中裝載int類型值入棧
3: iload_1 //從[局部變量1]中裝載int類型值入棧
4: iadd //將棧頂元素彈出棧,執(zhí)行int類型的加法,結(jié)果入棧
【For example, the iadd instruction (§iadd) adds two int values together. It requires that the int values to be added be the top two values of the operand stack, pushed there by previous instructions. Both of the int values are popped from the operand stack. They are added, and their sum is pushed back onto the operand stack. Subcomputations may be nested on the operand stack, resulting in values that can be used by the encompassing computation.】
5: istore_2 //將棧頂int類型值保存到[局部變量2]中
6: iload_2 //從[局部變量2]中裝載int類型值入棧
7: ireturn //從方法中返回int類型的數(shù)據(jù)
...
}
2、深入分析
2.1 棧指向堆
如果在棧幀中有一個(gè)變量,類型為引用類型,比如 Object obj=new Object(),這時(shí)候就是典型的棧中元素指向堆中的對(duì)象。
2.2 方法區(qū)指向堆
方法區(qū)中會(huì)存放靜態(tài)變量,常量等數(shù)據(jù)。如果是下面這種情況,就是典型的方法區(qū)中元素指向堆中的對(duì)象。
private static Object obj=new Object();
2.3 堆指向方法區(qū)
方法區(qū)中會(huì)包含類的信息,堆中會(huì)有對(duì)象,那怎么知道對(duì)象是哪個(gè)類創(chuàng)建的呢?
思考:一個(gè)對(duì)象怎么知道它是由哪個(gè)類創(chuàng)建出來的?怎么記錄?這就需要了解一個(gè)Java對(duì)象的具體信息咯。
2.4 Java對(duì)象內(nèi)存布局
一個(gè)Java對(duì)象在內(nèi)存中包括3個(gè)部分:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。
3、內(nèi)存模型
3.1 圖解
一塊是非堆區(qū),一塊是堆區(qū)。
堆區(qū)分為兩大塊,一個(gè)是Old區(qū),一個(gè)是Young區(qū)。 Young區(qū)分為兩大塊,一個(gè)是Survivor區(qū)(S0+S1),一塊是Eden區(qū)。 Eden:S0:S1=8:1:1 S0和S1一樣大,也可以叫From和To。
根據(jù)之前對(duì)于Heap的介紹可以知道,一般對(duì)象和數(shù)組的創(chuàng)建會(huì)在堆中分配內(nèi)存空間,關(guān)鍵是堆中有這么多區(qū)域,那一個(gè)對(duì)象的創(chuàng)建到底在哪個(gè)區(qū)域呢?
3.2 對(duì)象創(chuàng)建所在區(qū)域
一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū),一些特殊的大的對(duì)象會(huì)直接分配到Old區(qū)。
比如有對(duì)象A,B,C等創(chuàng)建在Eden區(qū),但是Eden區(qū)的內(nèi)存空間肯定有限,比如有100M,假如已經(jīng)使用了 100M 或者達(dá)到一個(gè)設(shè)定的臨界值,這時(shí)候就需要對(duì)Eden內(nèi)存空間進(jìn)行清理,即垃圾收集(Garbage Collect), 這樣的GC我們稱之為Minor GC,Minor GC指的是Young區(qū)的GC。
經(jīng)過GC之后,有些對(duì)象就會(huì)被清理掉,有些對(duì)象可能還存活著,對(duì)于存活著的對(duì)象需要將其復(fù)制到Survivor 區(qū),然后再清空Eden區(qū)中的這些對(duì)象。
3.3 Survivor區(qū)詳解
由圖解可以看出,Survivor區(qū)分為兩塊S0和S1,也可以叫做From和To。 在同一個(gè)時(shí)間點(diǎn)上,S0和S1只能有一個(gè)區(qū)有數(shù)據(jù),另外一個(gè)是空的。
接著上面的GC來說,比如一開始只有Eden區(qū)和From中有對(duì)象,To中是空的。 此時(shí)進(jìn)行一次GC操作,F(xiàn)rom區(qū)中對(duì)象的年齡就會(huì)+1,我們知道Eden區(qū)中所有存活的對(duì)象會(huì)被復(fù)制到To區(qū),
From區(qū)中還能存活的對(duì)象會(huì)有兩個(gè)去處。
若對(duì)象年齡達(dá)到之前設(shè)置好的年齡閾值,此時(shí)對(duì)象會(huì)被移動(dòng)到Old區(qū),????Eden????From??沒有達(dá)到閾值的 對(duì)象會(huì)被復(fù)制到To區(qū)。 此時(shí)Eden區(qū)和From區(qū)已經(jīng)被清空(被GC的對(duì)象肯定沒了,沒有被GC的對(duì)象都有了各自的去處)。
這時(shí)候From和To交換角色,之前的From變成了To,之前的To變成了From。 也就是說無論如何都要保證名為To的Survivor區(qū)域是空的。
Minor GC會(huì)一直重復(fù)這樣的過程,直到To區(qū)被填滿,然后會(huì)將所有對(duì)象復(fù)制到老年代中。
3.4 Old區(qū)詳解
從上面的分析可以看出,一般Old區(qū)都是年齡比較大的對(duì)象,或者相對(duì)超過了某個(gè)閾值的對(duì)象。
在Old區(qū)也會(huì)有GC的操作,Old區(qū)的GC我們稱作為Major GC。
3.5 對(duì)象的一輩子理解
我是一個(gè)普通的Java對(duì)象,我出生在Eden區(qū),在Eden區(qū)我還看到和我長的很像的小兄弟,我們?cè)贓den區(qū)中玩了挺長時(shí)間。有 一天Eden區(qū)中的人實(shí)在是太多了,我就被迫去了Survivor區(qū)的“From”區(qū),自從去了Survivor區(qū),我就開始漂了,有時(shí)候在 Survivor的“From”區(qū),有時(shí)候在Survivor的“To”區(qū),居無定所。直到我18歲的時(shí)候,爸爸說我成人了,該去社會(huì)上闖闖了。于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認(rèn)識(shí)了很多人。在年老代里,我生活了20年(每次 GC加一歲),然后被回收。
3.6 常見問題
- 如何理解Minor/Major/Full GC
Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代
- 為什么需要Survivor區(qū)?只有Eden不行嗎?
如果沒有Survivor,Eden區(qū)每進(jìn)行一次Minor GC,存活的對(duì)象就會(huì)被送到老年代。 這樣一來,老年代很快被填滿,觸發(fā)Major GC(因?yàn)镸ajor GC一般伴隨著Minor GC,也可以看做觸發(fā)了Full GC)。 老年代的內(nèi)存空間遠(yuǎn)大于新生代,進(jìn)行一次Full GC消耗的時(shí)間比Minor GC長得多。執(zhí)行時(shí)間長有什么壞處?頻發(fā)的Full GC消耗的時(shí)間很長,會(huì)影響大型程序的執(zhí)行和響應(yīng)速度。
可能你會(huì)說,那就對(duì)老年代的空間進(jìn)行增加或者較少咯。假如增加老年代空間,更多存活對(duì)象才能填滿老年代。雖然降低Full GC頻率,但是隨著老年代空間加大,一旦發(fā)生Full GC,執(zhí)行所需要的時(shí)間更長。
假如減少老年代空間,雖然Full GC所需時(shí)間減少,但是老年代很快被存活對(duì)象填滿,F(xiàn)ull GC頻率增加。
所以Survivor的存在意義,就是減少被送到老年代的對(duì)象,進(jìn)而減少Full GC的發(fā)生,Survivor的預(yù)篩選保證,只有經(jīng)歷16 次Minor GC還能在新生代中存活的對(duì)象,才會(huì)被送到老年代。
- 為什么需要兩個(gè)Survivor區(qū)?
最大的好處就是解決了碎片化。也就是說為什么一個(gè)Survivor區(qū)不行?第一部分中,我們知道了必須設(shè)置Survivor區(qū)。假設(shè),現(xiàn)在只有一個(gè)Survivor區(qū),我們來模擬一下流程:
剛剛新建的對(duì)象在Eden中,一旦Eden滿了,觸發(fā)一次Minor GC,Eden中的存活對(duì)象就會(huì)被移動(dòng)到Survivor區(qū)。這樣繼續(xù)循環(huán)下去,下一次Eden滿了的時(shí)候,問題來了,此時(shí)進(jìn)行Minor GC,Eden和Survivor各有一些存活對(duì)象,如果此時(shí)把Eden區(qū)的,存活對(duì)象硬放到Survivor區(qū),很明顯這兩部分對(duì)象所占有的內(nèi)存是不連續(xù)的,也就導(dǎo)致了內(nèi)存碎片化。
永遠(yuǎn)有一個(gè)Survivor space是空的,另一個(gè)非空的Survivor space無碎片。
- 新生代中Eden:S1:S2為什么是8:1:1?
新生代中的可用內(nèi)存:復(fù)制算法用來擔(dān)保的內(nèi)存為9:1
可用內(nèi)存中Eden:S1區(qū)為8:1
即新生代中Eden:S1:S2 = 8:1:1
4、驗(yàn)證
4.1 堆內(nèi)存溢出
程序:
@RestController
public class HeapController {
List<Person> list=new ArrayList<Person>();
@GetMapping("/heap")
public String heap() throws Exception{
while(true){
list.add(new Person());
Thread.sleep(1);
}
}
}
記得設(shè)置參數(shù)比如-Xmx20M -Xms20M
運(yùn)行結(jié)果:
Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded
4.2 方法區(qū)內(nèi)存溢出
比如向方法區(qū)中添加Class的信息
4.2.1 asm依賴和Class代碼
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
public class MyMetaspace extends ClassLoader {
public static List<Class<?>> createClasses() {
List<Class<?>> classes = new ArrayList<Class<?>>();
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0); mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V"); mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
4.2.2 代碼
@RestController
public class NonHeapController {
List<Class<?>> list=new ArrayList<Class<?>>();
@GetMapping("/nonheap")
public String nonheap() throws Exception{
while(true){
list.addAll(MyMetaspace.createClasses());
Thread.sleep(5);
}
}
}
設(shè)置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
運(yùn)行結(jié)果:
java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191] at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]
4.3 虛擬機(jī)棧
4.3.1 代碼演示StackOverFlow
public class StackDemo {
public static long count=0;
public static void method(long i){
System.out.println(count++);
method(i);
}
public static void main(String[] args) {
method(1);
}
}
運(yùn)行結(jié)果:
4.3.2 理解和說明
Stack Space用來做方法的遞歸調(diào)用時(shí)壓入Stack Frame(棧幀)。所以當(dāng)遞歸調(diào)用太深的時(shí)候,就有可能耗盡Stack Space,爆出StackOverflow的錯(cuò)誤。
-Xss128k:設(shè)置每個(gè)線程的堆棧大小。JDK 5以后每個(gè)線程堆棧大小為1M,以前每個(gè)線程堆棧大小為256K。根據(jù)應(yīng)用的線程所需內(nèi)存大小進(jìn)行調(diào)整。在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程。但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗(yàn)值在3000~5000左右。
線程棧的大小是個(gè)雙刃劍,如果設(shè)置過小,可能會(huì)出現(xiàn)棧溢出,特別是在該線程內(nèi)有遞歸、大的循環(huán)時(shí)出現(xiàn)溢出的可能性更大,如果該值設(shè)置過大,就有影響到創(chuàng)建棧的數(shù)量,如果是多線程的應(yīng)用,就會(huì)出現(xiàn)內(nèi)存溢出的錯(cuò)誤。
以上就是關(guān)于 Java 虛擬機(jī)棧和內(nèi)存模型深層次剖析的全部內(nèi)容,想要了解更多相關(guān) Java 虛擬機(jī)棧和內(nèi)存模型的其他內(nèi)容請(qǐng)搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持我們!