您好,登錄后才能下訂單哦!
本篇內容主要講解“Java虛擬機棧和內存模型實例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java虛擬機棧和內存模型實例分析”吧!
棧幀:每個棧幀對應一個被調用的方法,可以理解為一個方法的運行空間。
每個棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向運行時常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
局部變量表:方法中定義的局部變量以及方法的參數存放在這張表中,局部變量表中的變量不可直接使用,如需要使用的話,必須通過相關指令將其加載至操作數棧中作為操作數使用。
操作數棧:以壓棧和出棧的方式存儲操作數的。
動態鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接(Dynamic Linking)。
方法返回地址:當一個方法開始執行后,只有兩種方式可以退出,一種是遇到方法返回的字節碼指令;一種是遇見異常,并且這個異常沒有在方法體內得到處理。
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壓入[操作數棧]
1: istore_0 //將int類型值存入[局部變量0]
2: iload_0 //從[局部變量0]中裝載int類型值入棧
3: iload_1 //從[局部變量1]中裝載int類型值入棧
4: iadd //將棧頂元素彈出棧,執行int類型的加法,結果入棧
【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類型的數據
...
}
如果在棧幀中有一個變量,類型為引用類型,比如 Object obj=new Object(),這時候就是典型的棧中元素指向堆中的對象。
方法區中會存放靜態變量,常量等數據。如果是下面這種情況,就是典型的方法區中元素指向堆中的對象。
private static Object obj=new Object();
方法區中會包含類的信息,堆中會有對象,那怎么知道對象是哪個類創建的呢?
思考:一個對象怎么知道它是由哪個類創建出來的?怎么記錄?這就需要了解一個Java對象的具體信息咯。
一個Java對象在內存中包括3個部分:對象頭、實例數據和對齊填充。
一塊是非堆區,一塊是堆區。
堆區分為兩大塊,一個是Old區,一個是Young區。 Young區分為兩大塊,一個是Survivor區(S0+S1),一塊是Eden區。 Eden:S0:S1=8:1:1 S0和S1一樣大,也可以叫From和To。
根據之前對于Heap的介紹可以知道,一般對象和數組的創建會在堆中分配內存空間,關鍵是堆中有這么多區域,那一個對象的創建到底在哪個區域呢?
一般情況下,新創建的對象都會被分配到Eden區,一些特殊的大的對象會直接分配到Old區。
比如有對象A,B,C等創建在Eden區,但是Eden區的內存空間肯定有限,比如有100M,假如已經使用了 100M 或者達到一個設定的臨界值,這時候就需要對Eden內存空間進行清理,即垃圾收集(Garbage Collect), 這樣的GC我們稱之為Minor GC,Minor GC指的是Young區的GC。
經過GC之后,有些對象就會被清理掉,有些對象可能還存活著,對于存活著的對象需要將其復制到Survivor 區,然后再清空Eden區中的這些對象。
由圖解可以看出,Survivor區分為兩塊S0和S1,也可以叫做From和To。 在同一個時間點上,S0和S1只能有一個區有數據,另外一個是空的。
接著上面的GC來說,比如一開始只有Eden區和From中有對象,To中是空的。 此時進行一次GC操作,From區中對象的年齡就會+1,我們知道Eden區中所有存活的對象會被復制到To區,
From區中還能存活的對象會有兩個去處。
若對象年齡達到之前設置好的年齡閾值,此時對象會被移動到Old區,????????Eden????????From????沒有達到閾值的 對象會被復制到To區。 此時Eden區和From區已經被清空(被GC的對象肯定沒了,沒有被GC的對象都有了各自的去處)。
這時候From和To交換角色,之前的From變成了To,之前的To變成了From。 也就是說無論如何都要保證名為To的Survivor區域是空的。
Minor GC會一直重復這樣的過程,直到To區被填滿,然后會將所有對象復制到老年代中。
從上面的分析可以看出,一般Old區都是年齡比較大的對象,或者相對超過了某個閾值的對象。
在Old區也會有GC的操作,Old區的GC我們稱作為Major GC。
我是一個普通的Java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有 一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在 Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認識了很多人。在年老代里,我生活了20年(每次 GC加一歲),然后被回收。
如何理解Minor/Major/Full GC
Minor GC:新生代
Major GC:老年代
Full GC:新生代+老年代
為什么需要Survivor區?只有Eden不行嗎?
如果沒有Survivor,Eden區每進行一次Minor GC,存活的對象就會被送到老年代。 這樣一來,老年代很快被填滿,觸發Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發了Full GC)。 老年代的內存空間遠大于新生代,進行一次Full GC消耗的時間比Minor GC長得多。執行時間長有什么壞處?頻發的Full GC消耗的時間很長,會影響大型程序的執行和響應速度。
可能你會說,那就對老年代的空間進行增加或者較少咯。假如增加老年代空間,更多存活對象才能填滿老年代。雖然降低Full GC頻率,但是隨著老年代空間加大,一旦發生Full GC,執行所需要的時間更長。
假如減少老年代空間,雖然Full GC所需時間減少,但是老年代很快被存活對象填滿,Full GC頻率增加。
所以Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16 次Minor GC還能在新生代中存活的對象,才會被送到老年代。
為什么需要兩個Survivor區?
最大的好處就是解決了碎片化。也就是說為什么一個Survivor區不行?第一部分中,我們知道了必須設置Survivor區。假設,現在只有一個Survivor區,我們來模擬一下流程:
剛剛新建的對象在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活對象就會被移動到Survivor區。這樣繼續循環下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活對象,如果此時把Eden區的,存活對象硬放到Survivor區,很明顯這兩部分對象所占有的內存是不連續的,也就導致了內存碎片化。
永遠有一個Survivor space是空的,另一個非空的Survivor space無碎片。
新生代中Eden:S1:S2為什么是8:1:1?
新生代中的可用內存:復制算法用來擔保的內存為9:1
可用內存中Eden:S1區為8:1
即新生代中Eden:S1:S2 = 8:1:1
程序:
@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);
}
}
}
記得設置參數比如-Xmx20M -Xms20M
運行結果:
Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded
比如向方法區中添加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;
}
}
@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);
}
}
}
設置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
運行結果:
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]
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);
}
}
Stack Space用來做方法的遞歸調用時壓入Stack Frame(棧幀)。所以當遞歸調用太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。
-Xss128k:設置每個線程的堆棧大小。JDK 5以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
線程棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更大,如果該值設置過大,就有影響到創建棧的數量,如果是多線程的應用,就會出現內存溢出的錯誤。
到此,相信大家對“Java虛擬機棧和內存模型實例分析”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。