jvm原理解析——不瘋魔不成活

摘要: 作為一名java開發人員,如果有人問你java是什麼?java是如何運行的?你該如何回答,事實上java是有Java語言、class文件、jvm、Java API共同組成。

jvm原理解析——不瘋魔不成活

java程序運行

*.java文件-->編譯器-->*.class文件-->線程啟動(main)-->jvm-->操作系統-->硬件

jvm原理解析——不瘋魔不成活

通過上面的流程我們可以看出java程序的執行順序,那麼jvm到底是什麼,class文件到底是如何在jvm中運行就顯得很重要了。

jvm原理

什麼是jvm

openjdk源碼地址http://hg.openjdk.java.net/jdk9

jvm原理解析——不瘋魔不成活

JVM是一個計算機模型,JVM對Java可執行代碼,即字節碼(Bytecode)的格式給出了明確的規格。這一規格包括操作碼和操作數的語法(也就是cpu指令集)和數值、標識符的數值表示方式、以及Java類文件中的Java對象、常量緩衝池在JVM的存儲映象。

JVM的組成

JVM指令系統、JVM寄存器、JVM 棧結構、JVM 碎片回收堆、JVM 存儲區

JVM指令

Java指令也是由操作碼和操作數兩部分組成,與RISC CPU採用的編碼方式是一致的,也就是精簡指令集,目前UNIX、Linux、MacOS系統使用RISC,我們目前知道的x86架構CPU使用的CISC編碼,也就是複雜指令集。

JVM寄存器

1.pc程序計數器

2.optop操作數棧頂指針

3.frame當前執行環境指針

4.vars指向當前執行環境中第一個局部變量的指針

jvm的裝載

windows操作系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境。

1.創建JVM裝載環境和配置

2.裝載JVM.dll(C:\Program Files\Java\jre1.8.0_151\bin\server linux在jre/lib/server下)

3.初始化JVM.dll並掛接到JNIENV(JNI調用接口)實例

4.調用JNIEnv實例裝載並處理class類。

JVM虛擬機相當於x86計算機系統,Java解釋器相當於x86CPU

JVM運行數據

JVM定義了若干個程序執行期間使用的數據區域。這個區域裡的一些數據在JVM啟動的時候創建,在JVM退出的時候銷燬。而其他的數據依賴於每一個線程,在線程創建時創建,在線程退出時銷燬。分別有程序計數器,堆,棧,方法區,運行時常量池

jvm原理解析——不瘋魔不成活

jvm原理解析——不瘋魔不成活

  • 程序計數器:每個線程一旦被創建就擁有了自己的程序計數器。當線程執行Java方法的時候,它包含該線程正在被執行的指令的地址。但是若線程執行的是一個本地的方法,那麼程序計數器的值就不會被定義。

  • 常量緩衝池和方法區:常量緩衝池用於存儲類名稱、方法和字段名稱以及串常量。方法區則用於存儲Java方法的字節碼。對於這兩種存儲區域具體實現方式在JVM規格中沒有明確規定。這使得Java應用程序的存儲佈局必須在運行過程中確定,依賴於具體平臺的實現方式。

  • 棧:Java棧是JVM存儲信息的主要方法。當JVM得到一個Java字節碼應用程序後,便為該代碼中一個類的每一個方法創建一個棧框架,以保存該方法的狀態信息。每個棧框架包括以下三類信息:

    局部變量:對應vars寄存器指向該變量表中的第一個局部變量,用於存儲一個類的方法中所用到的局部變量。

    執行環境:對應frame寄存器的當前執行環境指針,用於保存解釋器對Java字節碼進行解釋過程中所需的信息。它們是:上次調用的方法、局部變量指針和操作數棧的棧頂和棧底指針。執行環境是一個執行一個方法的控制中心。例如:如果解釋器要執行iadd(整數加法),首先要從frame寄存器中找到當前執行環境,而後便從執行環境中找到操作數棧,從棧頂彈出兩個整數進行加法運算,最後將結果壓入棧頂。

    操作數棧:對應optop寄存器的操作數棧頂指針,操作數棧用於存儲運算所需操作數及運算的結果。

jvm原理解析——不瘋魔不成活

  • 堆:JVM中最大的,應用的對象和數據都是存在這個區域,這塊區域也是線程共享的,也是 gc 主要的回收區,一個 JVM 實例只存在一個堆類存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,以方便執行器執行。

jvm原理解析——不瘋魔不成活

垃圾回收機制(GC)只發生在線程共享區,也就是堆和方法區,棧不需要回收,線程銷燬則棧也銷燬, 也就是上圖的heap space與method area會發生gc。

通過上圖可以發現heap space被分為兩部分:

  • Young Generation

    :又分為Eden space所有的類都是在Eden space被new出來的。From區(Survivor 0 space)和To區(Survivor 1 space)。當Eden space空間用完時,程序又需要創建對象,JVM的垃圾回收器將對Eden space進行垃圾回收(Minor GC),將Eden space中的剩餘對象移動到From區。若From區也滿了,再對該區進行垃圾回收,然後移動到To區。那如果To區也滿了呢,再移動到Old區。

  • Old Generation:若該區也滿了,那麼這個時候將產生Major GC(FullGCC),進行Tenured區的內存清理。若該區執行Full GC 之後發現依然無法進行對象的保存,產生異常java.lang.OutOfMemoryError: Java heap space。

  1. Java虛擬機的堆內存設置不夠,可以通過參數-Xms、-Xmx來調整。

  2. 代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)。

  • Permanent Generation:

    是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所佔用的內存。產生異常java.lang.OutOfMemoryError: PermGen space jdk1.8之後已經不會再報報這個錯誤了。因為類信息的卸載幾乎很少發生,這樣會影響GC的效率。於是PermGen便被拆分出去了。

  1. 程序啟動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。

  2. 大量動態反射生成的類不斷被加載,最終導致Perm區被佔滿。

jvm的算法

由於算法篇幅太長具體算法可自行查閱資料,主要介紹gc算法發生在什麼區。

分代蒐集算法:是由複製算法、標記/整理、標記/清除算法共同組成

複製算法發生在Young Generation

標記/整理和標記/清除算法發生在Old Generation和Permanent Generation

java驗證jvm

棧中一般存放的都是對象的指針和基本類型,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。

棧數據可以共享

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { int a=0; int b=0; System.out.print(a==b); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"trueProcess finished with exit code 0

編譯器先處理int a = 0;首先它會在棧中創建一個變量為a的引用,然後查找有沒有字面值為0的地址,沒找到,就開闢一個存放0這個字面值的地址,然後將a指向0的地址。接著處理int b = 0;在創建完b的引用變量後,由於在棧中已經有0這個字面值,便將b直接指向0的地址。這樣,就出現了a與b同時均指向0的情況

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { int a=0; int b=0; a=1; System.out.print("a="+a); System.out.print("b="+b); System.out.print(a==b); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" a=1b=0falseProcess finished with exit code 0

再令a=1;那麼,b不會等於1,還是等於0。在編譯器內部,遇到a=1;時,它就會重新搜索棧中是否有1的字面值,如果沒有,重新開闢地址存放1的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

String str = "abc"的工作原理

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; System.out.println(str1==str2); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"trueProcess finished with exit code 0
/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; System.out.println(str1 + "," + str2); System.out.println(str1==str2); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" bcd,abcfalseProcess finished with exit code 0

賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們將str1的值改為"bcd"時,JVM發現在棧中沒有存放該值的地址,便開闢了這個地址,並創建了一個新的對象,其字符串的值指向這個地址。

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; str1 = "bcd"; String str3 = str1; System.out.println(str3); String str4 = "bcd"; System.out.println(str1 == str4); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"bcdtrueProcess finished with exit code 0

str3這個對象的引用直接指向str1所指向的對象(注意,str3並沒有創建新對象)。當str1改完其值後,再創建一個String的引用 str4,並指向因str1修改值而創建的新的對象。可以發現,這回str4也沒有創建新的對象,從而再次實現棧中數據的共享。

堆驗證

String類

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String str1 = new String("abc"); String str2 = "abc";System.out.println(str1==str2);}}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"falseProcess finished with exit code 0

以上代碼說明,只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。

使用String str = "abc";的方式,可以在一定程度上提高程序的運行速度,因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對於String str = new String("abc");的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。這個思想應該是 享元模式的思想。

由於String類的性質,當String變量需要經常變換其值時,應該考慮使用StringBuffer類,以提高程序效率。

執行時間上寄存器 < 堆棧 < 堆

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args) { String s1 = "ja"; String s2 = "va"; String s3 = "java"; String s4 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3.equals(s4)); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"falsetrueProcess finished with exit code 0

是不是很矛盾啊!是不是又懵逼了?

打印false的原因是,java 重載了“+”,查看java字節碼可以發現“+”其實是調用了StringBuilder 所以使用了“+”其實是生成了一個新的對象。所以(s3 == s4)打印false

/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args){ long maxMemory = Runtime.getRuntime().maxMemory();//返回Java虛擬機試圖使用的最大內存量。 Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm實例佔用的內存。 System.out.println("MAX_MEMORY ="+maxMemory +"(字節)、"+(maxMemory/(double)1024/1024) + "MB"); System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字節)"+(totalMemory/(double)1024/1024) + "MB"); }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetailsMAX_MEMORY =1868038144(字節)、1781.5MBTOTAL_ MEMORY = 126877696(字節)121.0MBHeapPSYoungGen total 37888K, used 3932K [0x00000000d6400000, 0x00000000d8e00000, 0x0000000100000000) eden space 32768K, 12% used [0x00000000d6400000,0x00000000d67d7320,0x00000000d8400000) from space 5120K, 0% used [0x00000000d8900000,0x00000000d8900000,0x00000000d8e00000) to space 5120K, 0% used [0x00000000d8400000,0x00000000d8400000,0x00000000d8900000)ParOldGen total 86016K, used 0K [0x0000000082c00000, 0x0000000088000000, 0x00000000d6400000) object space 86016K, 0% used [0x0000000082c00000,0x0000000082c00000,0x0000000088000000)Metaspace used 3325K, capacity 4494K, committed 4864K, reserved 1056768K class space used 363K, capacity 386K, committed 512K, reserved 1048576KProcess finished with exit code 0

將jvm堆初始值改小,觸發gc回收

import java.util.Random;/*** Created by liustc on 2018/4/20.*/public class JvmTest { public static void main(String[] args){ long maxMemory = Runtime.getRuntime().maxMemory();//返回jvm試圖使用的最大內存量。 Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm實例的內存大小。 System.out.println("MAX_MEMORY ="+maxMemory +"(字節)、"+(maxMemory/(double)1024/1024) + "MB"); System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字節)"+(totalMemory/(double)1024/1024) + "MB"); String str = "www.baidu.com"; while(true){ str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999); } }} 
"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetailsMAX_MEMORY =1868038144(字節)、1781.5MBTOTAL_ MEMORY = 126877696(字節)121.0MB[GC (Allocation Failure) [PSYoungGen: 32247K->2729K(37888K)] 32247K->10124K(123904K), 0.0045031 secs] [Times: user=0.01 sys=0.03, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 32912K->4469K(70656K)] 40307K->26638K(156672K), 0.0121112 secs] [Times: user=0.00 sys=0.00, real=0.01 secs][GC (Allocation Failure) [PSYoungGen: 66160K->776K(70656K)] 88329K->59879K(156672K), 0.0141096 secs] [Times: user=0.03 sys=0.02, real=0.01 secs][Full GC (Ergonomics) [PSYoungGen: 776K->0K(70656K)] [ParOldGen: 59103K->37630K(116224K)] 59879K->37630K(186880K), [Metaspace: 3408K->3408K(1056768K)], 0.0143902 secs] [Times: user=0.03 sys=0.00, real=0.01 secs][Full GC (Ergonomics) [PSYoungGen: 60370K->0K(70656K)] [ParOldGen: 96726K->74565K(172032K)] 157096K->74565K(242688K), [Metaspace: 3409K->3409K(1056768K)], 0.0598124 secs] [Times: user=0.08 sys=0.00, real=0.06 secs][GC (Allocation Failure) [PSYoungGen: 60382K->32K(95744K)] 1257771K->1226968K(1463808K), 0.0227293 secs] [Times: user=0.06 sys=0.01, real=0.02 secs][GC (Allocation Failure) [PSYoungGen: 32K->32K(131584K)] 1226968K->1226968K(1499648K), 0.0037586 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 32K->0K(131584K)] [ParOldGen: 1226936K->355271K(483840K)] 1226968K->355271K(615424K), [Metaspace: 3409K->3409K(1056768K)], 0.1616835 secs] [Times: user=0.19 sys=0.09, real=0.16 secs][GC (Allocation Failure) [PSYoungGen: 2499K->32K(158208K)] 1303306K->1300838K(1526272K), 0.0037952 secs] [Times: user=0.06 sys=0.00, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 32K->32K(158208K)] 1300838K->1300838K(1526272K), 0.0036491 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 32K->0K(158208K)] [ParOldGen: 1300806K->473463K(622080K)] 1300838K->473463K(780288K), [Metaspace: 3409K->3409K(1056768K)], 0.1641897 secs] [Times: user=0.30 sys=0.06, real=0.16 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(250880K)] 946230K->946230K(1618944K), 0.0027229 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] 946230K->946230K(1626624K), 0.0027747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs][Full GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] [ParOldGen: 946230K->709846K(879104K)] 946230K->709846K(1137664K), [Metaspace: 3409K->3409K(1056768K)], 0.1013768 secs] [Times: user=0.28 sys=0.02, real=0.10 secs][GC (Allocation Failure) [PSYoungGen: 0K->0K(353280K)] 709846K->709846K(1721344K), 0.0049384 secs] [Times: user=0.00 sys=0.00, real=0.01 secs][Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space[PSYoungGen: 0K->0K(353280K)] [ParOldGen: 709846K->709816K(900608K)] 709846K->709816K(1253888K), [Metaspace: 3409K->3409K(1056768K)], 0.1792920 secs] [Times: user=0.39 sys=0.00, real=0.18 secs]Heapat java.util.Arrays.copyOf(Arrays.java:3332) PSYoungGen total 353280K, used 14028K [0x00000000d6400000, 0x00000000ec700000, 0x0000000100000000)at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) eden space 352768K, 3% used [0x00000000d6400000,0x00000000d71b3070,0x00000000ebc80000)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674) from space 512K, 0% used [0x00000000ec680000,0x00000000ec680000,0x00000000ec700000)at java.lang.StringBuilder.append(StringBuilder.java:208) to space 4608K, 0% used [0x00000000ebe00000,0x00000000ebe00000,0x00000000ec280000)at JvmTest.main(JvmTest.java:15) ParOldGen total 1368064K, used 709816K [0x0000000082c00000, 0x00000000d6400000, 0x00000000d6400000)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) object space 1368064K, 51% used [0x0000000082c00000,0x00000000ae12e1e8,0x00000000d6400000) Metaspace used 3440K, capacity 4494K, committed 4864K, reserved 1056768Kat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) class space used 377K, capacity 386K, committed 512K, reserved 1048576Kat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)Process finished with exit code 1


分享到:


相關文章: