JVM理解
1、JVM的基本介紹
JVM,即 Java Virtual Machine ,是Java 程序的運行環境(Java 二進制字節碼的運行環境)。
JVM的作用:
- 一次編寫,到處運行
- 自動內存管理,垃圾回收功能
- 數組下標越界檢查
- 多態
1.1、JVM、JRE、JDK三者的比較
JVM、JRE、JDK 的關系如下圖所示。
- JDK(Java Development Kit):java開發工具包,在JRE的基礎上增加編譯工具,如javac
- JRE(Java Runtime Environment):java的運行時環境,在JVM的基礎上結合一些基礎類庫
- JVM:java虛擬機, 可以屏蔽java代碼與底層虛擬機之間的關系

1.2、常見的JVM

1.3、JVM的整體架構

2、程序計數器
- 是線程私有的。每個線程都有自己的程序計數器,隨著線程創建而創建,隨線程銷毀而銷毀
- 不會存在內存溢出

3、虛擬機棧(線程內存)

3.1、虛擬機棧基本介紹
每個棧由多個棧幀(Frame)組成,對應著該線程內各個方法調用時所占用的內存,即線程內每個方法的調用都會創建一個新的棧幀(Stack Frame)。每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法。

- 不會。棧幀內存 在每次方法調用結束后會自動彈出棧(自動回收),不需要回收(垃圾回收回收堆內存中無用對象,不會回收棧內存)
- 不是。因為服務器中物理內存是固定大小的,單個棧內存大了,可創建的線程數就少了。雖然棧內可進行更多次方法調用,但由于線程數減少,所以并不會提高效率。
- 可以通過 -Xss 參數來設置棧內存大小,JDK1.5+ 中默認是 1M,一般來說使用默認值即可
- 如果方法內的局部變量沒有逃離方法的作用范圍,那么它是線程安全的
- 如果是局部變量引用了對象,并逃離了方法的作用范圍,需要考慮線程安全
如下:
package JVM; public class Demo01 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); } /** * 不會有線程安全問題。因為StringBuilder是線程內局部變量,屬于線程私有,其他線程無法訪問 */ public static void m1() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(1); stringBuilder.append(2); stringBuilder.append(3); System.out.println(stringBuilder.toString()); } /** * 不是線程安全的。StringBuilder作為參數傳入,StringBuilder可能被其他線程共享,不是線程安全 */ public static void m2(StringBuilder stringBuilder) { stringBuilder.append(1); stringBuilder.append(2); stringBuilder.append(3); System.out.println(stringBuilder.toString()); } /** * 不是線程安全的。雖然StringBuilder是作為局部變量,但是返回結果為StringBuilder,可能被其他線程修改 */ public static StringBuilder m2() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(1); stringBuilder.append(2); stringBuilder.append(3); return stringBuilder; } }
3.1.1、棧幀代碼演示
代碼如下:
/** * 演示棧幀 */ public class Demo1_1 { public static void main(String[] args) throws InterruptedException { method1(); } private static void method1() { method2(1, 2); } private static int method2(int a, int b) { int c = a + b; return c; } }
開啟 debug 模式,執行 main 主方法,當調試執行到 method2 方法時,可以看到創建了三個棧幀。當方法 main、method1、method2 執行結束后,棧幀依次被銷毀。

3.2、棧內存溢出(StackOverflowError)
- 棧幀過多導致棧內存溢出。比如遞歸調用方法未正確結束遞歸
- 棧幀過大導致棧內存溢出。
如下分別為棧幀過多和棧幀多大的示例圖:

代碼示例,如下是演示棧幀過多導致棧內存溢出的情況:
package cn.itcast.jvm.t1.stack; /** * 演示棧內存溢出 報錯信息:java.lang.StackOverflowError * 可以通過設置 JVM 參數來設置棧內存,如:-Xss256k */ public class Demo1_2 { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); } }
執行以上 main 方法,可以看到報錯如下:

3.3、線程運行診斷
3.3.1、CPU占用過高
通過跑一段無限循環代碼來使系統的 CPU 不斷飆升,演示如何通過命令來診斷出導致 CPU 過高的線程。
代碼示例:
package cn.itcast.jvm.t1.stack; /** * 演示 cpu 占用過高 */ public class Demo1_16 { public static void main(String[] args) { new Thread(null, () -> { System.out.println("1..."); while(true) { } }, "thread1").start(); new Thread(null, () -> { System.out.println("2..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread2").start(); new Thread(null, () -> { System.out.println("3..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread3").start(); } }
代碼編譯后,傳入 Linux 系統中,通過 java cn.itcast.jvm.t1.stack.Demo1_16 命令來運行該段程序。
然后通過 TOP 命令可以定位哪個進程對cpu的占用過高,如下:

通過 ps H -eo pid,tid,%cpu | grep 進程id 命令進一步定位是哪個線程引起的cpu占用過高,如下:
(注意,左邊是進程id,右邊是線程id)


如上找到 CPU 占用過高的線程,并且可以定位到具體的代碼類名和行數。
3.3.2、程序阻塞運行很久沒有結果
如下,通過一段代碼演示程序發生線程死鎖。
代碼如下:
package cn.itcast.jvm.t1.stack; /** * 演示線程死鎖 */ class A{}; class B{}; public class Demo1_3 { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("show a and b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("show a and b 222"); } } }).start(); } }
將該代碼放置到 Linux 環境上執行,可以看到很久都沒有輸出結果。

當我們通過 jstack 命令來查看該進程的線程時,可以發現已經發生了死鎖。


4、本地方法棧
在 java 虛擬機調用一些本地方法時需要給本地方法提供的內存空間。
- 本地方法:由于java有限制,不可以直接與操作系統底層交互,所以需要一些用c/c++編寫的本地方法與操作系統底層的API交互,java可以間接的通過本地方法來調用底層功能。本地方法是由其它語言編寫的,編譯成和處理器相關的機器代碼。本地方法保存在動態鏈接庫中,即.dll(windows系統)文件中,格式是各個平臺專有的。
舉例:Object的clone()、hashCode()、notify()、notifyAll()、wait()等,一個Native Method就是一個java調用非java代碼的接口。

5、堆內存(Heap,線程共享)
5.1、堆內存的基本介紹(新生代、老年代、永久代)
特點:
- 它是線程共享的,堆中對象都需要考慮線程安全的問題。堆跟根程序計數器和虛擬機棧不同的是,后兩者都是線程私有的,而堆是線程同享的
- 有垃圾回收機制。當一個對象不再被使用時,該對象就會被垃圾回收機制回收,即該對象內存會被垃圾回收掉。
堆內存區域介紹:

在jvm的堆內存中有三個區域:
- 年輕代:用于存放新產生的對象。
- 老年代:用于存放被長期引用的對象。
- 持久帶(或元空間):用于存放Class,method元信息(1.8之后改為元空間)。
詳細介紹如下:
年輕代:年輕代中包含兩個區:Eden 和survivor,并且用于存儲新產生的對象,其中有兩個survivor區。
老年代:年輕代在垃圾回收多次都沒有被GC回收的時候就會被放到老年代,以及一些大的對象(比如緩存,這里的緩存是弱引用),這些大對象可以不進入年輕代就直接進入老年代
持久代:持久代用來存儲class,method元信息,大小配置和項目規模,類和方法的數量有關。
元空間:JDK1.8之后,取消perm永久代,轉而用元空間代替。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于元空間并不在虛擬機中,而是使用本地內存,并且可以動態擴容。
為什么分代?
因為不同對象的生命周期是不一樣的。80%-98%的對象都是“朝生夕死”,生命周期很短,大部分新對象都在年輕代,可以很高效地進行回收,不用遍歷所有對象。而老年代對象生命周期一般很長,每次可能只回收一小部分內存,回收效率很低。
年輕代和老年代的內存回收算法完全不同,因為年輕代存活的對象很少,標記清楚再壓縮的效率很低,所以采用復制算法將存活對象移到survivor區,更高效。而老年代則相反,存活對象的變動很少,所以采用標記清楚壓縮算法更合適。
5.2、堆內存溢出
堆內存溢出模擬代碼:

package cn.itcast.jvm.t1.heap; import java.util.ArrayList; import java.util.List; /** * 演示堆內存溢出 java.lang.OutOfMemoryError: Java heap space * 可以通過配置JVM參數:-Xmx8m 來設置最大堆內存 */ public class Demo1_5 { public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>(); String a = "hello"; while (true) { list.add(a); // hello, hellohello, hellohellohellohello ... a = a + a; // hellohellohellohello i++; } } catch (Throwable e) { e.printStackTrace(); System.out.println(i); } } }
6、方法區(Method Area)
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域。方法區存儲類的結構的相關信息,如運行時常量池、成員變量、方法數據、成員方法和構造器的代碼等。
方法區在虛擬機啟動時創建,其邏輯上是堆的一個組成部分,但在實現時不同的JVM廠商可能會有不同的實現。方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
組成如下:以Oracle的HotSpot為例
- jdk1.6:永久代(PermGen space),占用JVM內存空間

- jdk1.8:元空間(Metaspace),移出JVM內存(除StringTable),放入操作系統內存

6.1、方法區內存溢出
通過不斷創建類來演示產生方法區內存溢出,如下:
package cn.itcast.jvm.t1.metaspace; import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.Opcodes; /** * 元空間內存溢出 java.lang.OutOfMemoryError: Metaspace * 設置元空間大小:-XX:MaxMetaspaceSize=8m * 永久代內存溢出 java.lang.OutOfMemoryError: PermGen space * 設置永久代內存大小:-XX:MaxPermSize=8m */ public class Demo1_8 extends ClassLoader { // 可以用來加載類的二進制字節碼 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); for (int i = 0; i < 10000; i++, j++) { // ClassWriter 作用是生成類的二進制字節碼 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); // Class 對象 } } finally { System.out.println(j); } } }
當使用 jdk1.8 及之后的版本時,內存溢出報錯提示:java.lang.OutOfMemoryError: Metaspace。當使用 jdk1.8 之前的版本時,內存溢出報錯提示:java.lang.OutOfMemoryError: PermGen space
(默認的元空間內存大小為操作系統的內存大小,可能沒那么容易產生內存溢出,可以通過設置 jvm 參數限制元空間內存大小來演示內存溢出現象)

浙公網安備 33010602011771號