我們首先了解下什么是JVM。
JVM(Java Virtual Machine),簡而言之就是java程序的運行環境(java二進制字節碼的運行環境)。以下表格比較了JVM、JRE和JDK之間的關系:
JVM | Java Virtual Machine |
JRE | JVM+基礎類庫 |
JDK | JVM+基礎類庫+編譯工具 |
開發javase程序 | JDK+IDE工具 |
開發javaee程序 | JDK+IDE工具+應用服務器 |
JVM的內存可以分為5大塊:程序計數器、虛擬機棧、本地方法棧、堆以及方法區
1、程序計數器,也稱為寄存器。我們知道,java程序的執行順序是jvm指令->解釋器->機器碼->CPU,那么程序計數器的作用就是在程序執行的過程中,記住下一條JVM指令的執行地址。當執行完當前JVM指令之后,會在程序計數器中獲取到下一條JVM指令的地址,以此去尋找下一條指令。
要記住,程序計數器是每條線程私有的。當線程因為某種原因暫停執行后,該條線程的程序計數器會記錄下條指令的地址,等結束暫停后,線程可以從停止的地方繼續執行。而且,程序計數器不會存在內存溢出。
2、虛擬機棧,就是每個線程運行需要的內存空間。一個棧由多個棧幀組成,棧幀對應著每個方法運行時需要的內存(參數,局部變量,返回地址等),每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法。


當方法結束調用后,對應的棧幀就會被移除出棧。
我們以debug方式運行代碼:


查看debug界面可以發現,main、method1和method2方法都以上文的方式被壓入棧中:


所以可以知道,垃圾回收不涉及棧內存,因為方法調用完被移除之后,內存就會被釋放掉。我們可以通過-Xss設定棧內存的大小,但是請注意,因為我們的物理內存是固定的,所以棧內存并不是越大越好,棧內存設置的越大,我們的線程數量反而越少。
既然可以分配棧的大小,那么什么情況下會出現棧內存溢出呢?第一種是棧幀過多導致棧內存溢出,第二種就是棧幀過大導致棧內存溢出。請看如下代碼:
public class Demo1_3 {
public static void main(String args[]) {
try {
m1();
}catch (Exception e) {
e.printStackTrace();
}
}
private static void m1() {
m1();
}
}
由于對m1進行了遞歸調用,且沒有設置退出條件,所以運行后會拋出棧內存溢出錯誤:
Exception in thread "main" java.lang.StackOverflowError
這里還有一個問題,如果多個線程執行m1方法,內部的變量x是線程安全的嗎?答案是肯定的,因為每個線程都有自己的棧,棧內的棧幀都會存在自己的變量x,所以方法內的局部變量是線程安全的。
public class Demo1_2 {
static void m1() {
int x = 0;
for (int i=0; i<5000; i++) {
x ++;
}
System.out.println(x);
}
}
但是如果變量x是每個方法公有的,那就需要考慮線程安全的問題了,比如用static修飾:
static int x = 0;
3、本地方法棧,就是給本地方法的運行提供運行空間。本地方法,指那些不是由Java代碼編寫的方法,可以通過本地方法去調用解釋器、即時編譯器或者垃圾回收器。比如Object類中的clone()方法,真正實現的是c和c++:
protected native Object clone() throws CloneNotSupportedException;
4、堆。通過new創建的對象都會使用堆內存,可以通過-Xmx設定堆空間大小。堆有兩大特點,一是線程共享,堆中的對象都需要考慮線程安全的問題,二是它有垃圾回收機制。
我們首先看一下堆內存溢出的問題,請看如下代碼:
public class Demo1_4{
public static void main(String args[]) {
try {
List<String> list = new ArrayList<>();
String a = "hello";
while(true) {
list.add(a);
a = a + a;
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
運行后拋出堆內存溢出錯誤:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
5、方法區,是所有JVM共享的區域,存儲了跟類的結構相關的信息:運行時常量池,類的成員變量,方法數據,以及成員方法和構造器方法的代碼等。方法區是在JVM啟動時被創建的,可以通過-XX:MaxMetaspaceSize=10m設置方法區的大小。下圖就是JDK1.8中的內存結構:


可以看到,Metaspace作為方法區的實現,包含了Class、ClassLoader和常量池。方法區也會有內存溢出,即元空間的內存溢出:
public class Demo1_5 extends ClassLoader{
public static void main(String args[]) {
try {
Demo1_5 test = new Demo1_5();
//加載10000個新的類
for (int i=0; i<10000; i++) {
//生成類的二進制字節碼
ClassWriter cw = new ClassWriter(0);
//參數含義:版本號,public,類名,包名,父類,接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class"+i, null, "java/lang/object", null);
//返回類的byte數組
byte[] code = cw.toByteArray();
//執行類的加載
test.defineClass("Class"+i, code, 0, code.length);
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
上述案例演示了加載的類數量過多導致元空間內存溢出,以下是運行后結果:
Error occurred during initialization of VM
MaxMetaspaceSize is too small.
版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。