從頭捋捋jvm(-java虛擬機)

  • 2020 年 4 月 10 日
  • 筆記

jvm 是Java Virtual Machine(Java虛擬機)的縮寫,java 虛擬機作為一種跨平台的軟件是作用於操作系統之上的,那麼認識並了解它的底層運行邏輯對於java開發人員來說很有必要!

讓我們來看看它一次編譯,到處運行的牛叉之處!
廢話不多說,先看看jvm的架構圖(無論何時腦子裡要有這樣一張圖):

總概
從這副架構圖可以看出jvm由類裝載器、運行時數據區、執行引擎、本地方法接口還有垃圾回收器構成;其中垃圾回收器作用在整個jvm內存中(主要作用在堆和方法區),所以圖中沒有具體體現,但是我們要知道這麼個東西,後面會聊jvm內存調優主要是在調堆

接下來,我們再結合jvm架構圖一個個分析下:

類加載器

了解類加載器就是了解代碼編譯執行的機制

1.加載:查找並加載類的二進制數據

2.連接:

  • 驗證:保證被加載的類的正確性;

  • 準備:給類靜態變量分配內存空間,賦值一個默認的初始值;

  • 解析:把類中的符號引用轉換為直接引用

    把java編譯為class文件的時候,虛擬機並不知道所引用的地址;助記符:符號引用轉為真正的直接引用,找到對應的直接地址!

3.初始化:給類的靜態變量賦值正確的值;

來看下這行代碼的執行順序:

package jvm;    public class TestClassLoader {      public static int a = 1;      // 1、加載  編譯TestClassLoader文件為 .class 文件,通過類加載,加載到JVM        // 2、連接      //驗證(1)  保證Class類文件沒有問題      //準備(2)  給int類型分配內存空間,a = 0;      //解析(3)  符號引用轉換為直接引用        // 3、初始化      //經過這個階段的解析,把1 賦值給 變量 a;  }  

了解了這個加載執行順序後,我們再來看看下面這段代碼,請你說說程序是如何輸出的?

Demo1代碼:

package jvm;  /**  *JVM 參數:  *-XX:+TraceClassLoading // 用於追蹤類的加載信息並打印出來  *分析項目啟動為什麼這麼慢,快速定位自己的類有沒有被加載!  *rt.jar jdk 出廠自帶的,最高級別的類加載器要加載的!  */  public class Demo1 {      public static void main(String[] args) {          System.out.println(Children1.str2);          Children1.sayHello();          // 輸出:          //      Parent1 static          //      Children1 static          //      hello,str2          //      hello,str1      }  }  class Parent1{      public static String str1 = "hello,str1";      public static void sayHello(){          System.out.println(str1);      }        static {          System.out.println("Parent1 static");      }  }  class Children1 extends Parent1{      public static String str2 = "hello,str2";      public static void sayHello(){          System.out.println(str1);      }        static {          System.out.println("Children1 static");      }  }  

可以看出,子類繼承父類 會優先加載父類的 然後靜態塊是先於方法和靜態變量的.那麼常量又是怎樣的呢?

Demo2代碼:

package jvm;    public class Demo2 {      public static void main(String[] args) {          System.out.println(Parent2.STR2);          // 輸出:          // hello static final      }  }  class Parent2{      public static final String STR2 = "hello static final";      static {          System.out.println("Parent2 static"); // 這句話會輸出嗎?      }  }  

Demo3代碼:

package jvm;    import java.util.UUID;  public class Demo3 {      public static void main(String[] args) {          System.out.println(Parent3.STR3);          // 輸出: Parent3 static          //       ee33c452-70e2-47a6-946a-99261819a49d      }  }  class Parent3{      public static final String STR3 = UUID.randomUUID().toString();      static {          System.out.println("Parent3 static"); // 這句話會輸出嗎?      }  }  

理解:根據上面的jvm架構圖可以知道類信息,常量,靜態變量,編譯後運行代碼都是存在方法區里的,而常量是存在方法區的常量池中的。

而對於編譯期可以「確定值」的常量:例如Demo2中STR2 是存放在Demo2類調用者所在的常量池中的,當STR2放到常量池後Demo2與Parent2類的關係就沒有了!

對於編譯期「不確定值」的常量:例如Demo3中STR3 是不存在Demo3的調用者的常量池中的,在程序運行期間會主動使用Demo3所在的類,會加載其靜態塊!

上面的程序Parent2.STR2輸出後就退出了Parent2類,因為程序加載Parent2類是直接調用了常量池中關於STR2的引用,無需再加載類的其他信息包括靜態塊,說白了按我的理解就是常量池和類的其他信息不在一個內存區,程序根據指令掃描的時候無需掃描不相關的內存區!說到池(池?嗯,等等是不是和線程池和隊列有關,哈哈。學東西要養成發散思維的習慣,這樣就能串起來很多知識點,漸漸形成自己的知識面。)接下來就來擴展一下:

package jvm;  /**   * 思考:   * String == 比較的是什麼?   * 八大基本類型哪些實現了池化技術   */  public class Exclude {      public static void main(String[] args) {          String java = "java";          String java1 = new String("java");          String java2 = new String("java");          System.out.println(java == java1);//輸出false          System.out.println(java1 == java2);//輸出false          // 注⚠️:String用==比較的時候不僅比較的是對象的值還比較的是對象值的引用          // equals            int i = 128;          int i1 = 128;          System.out.println(i == i1);//輸出true            Integer integer = 128;          Integer integer1 = 128;          System.out.println(integer == integer1);//輸出false            Float f = 0.1f;          Float f1 = 0.1f;          System.out.println(f == f1);//輸出false      }  }  

注⚠️:java中基本類型的包裝類的大部分都實現了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean
另外兩種浮點數類型的包裝類Double Float則沒有實現。
另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值小於等於127時才可使用常量池,即對象不負責創建和管理大於127的這些類的對象

類加載器 分類

1、java虛擬機自帶的加載器

  • BootStrap 根加載器 (加載系統的包,JDK 核心庫中的類 rt.jar)
  • Ext 擴展類加載器 (加載一些擴展jar包中的類)
  • Sys/App 系統(應用類)加載器 (我們自己編寫的類)

2、用戶自己定義的加載器

  • ClassLoader,只需要繼承這個抽象類即可,自定義自己的類加載器

Demo4代碼:

package jvm;    public class Demo4 {      public static void main(String[] args) {          Object o = new Object(); // jdk 自帶的          Demo demo = new Demo();  // 實例化一個自己定義的對象            // null 在這裡並不代表沒有,只是Java觸及不到!          System.out.println(o.getClass().getClassLoader()); // null          System.out.println(demo.getClass().getClassLoader()); // AppClassLoader          System.out.println(demo.getClass().getClassLoader().getParent()); // ExtClassLoader          System.out.println(demo.getClass().getClassLoader().getParent().getParent()); // null            // jvm 中有機制可以保護自己的安全;          // 雙親委派機制 : 一層一層的讓父類去加載,如果頂層的加載器不能加載,然後再向下類推          // ClassLoader         04          // AppClassLoader      03          // ExtClassLoader      02          // BootStrap (最頂層)   01  java.lang.String  rt.jar            // 雙親委派機制 可以保護java的核心類不會被自己定義的類所替代      }  }  class Demo{    }  

本地接口庫

Native方法 JNI : Java Native Interface (Java 本地方法接口)

大家都知道java是從C過度過來的,C可以直接操作硬件
思考一個問題:線程是基於內存的,那麼應該是操作硬件 讓計算機cpu來劃分時間片來「同時」執行多個任務,那麼程序是怎麼怎麼達到底層的呢?java 能直接操作硬件嗎?我們來看看線程是怎麼啟動的,看源碼(面向源碼編程)

 public synchronized void start() {          /**           * This method is not invoked for the main method thread or "system"           * group threads created/set up by the VM. Any new functionality added           * to this method in the future may have to also be added to the VM.           *           * A zero status value corresponds to state "NEW".           */          if (threadStatus != 0)              throw new IllegalThreadStateException();            /* Notify the group that this thread is about to be started           * so that it can be added to the group's list of threads           * and the group's unstarted count can be decremented. */          group.add(this);            boolean started = false;          try {              start0();  //啟動 調用下面的native修飾的啟動方法              started = true;          } finally {              try {                  if (!started) {                      group.threadStartFailed(this);                  }              } catch (Throwable ignore) {                  /* do nothing. If start0 threw a Throwable then                    it will be passed up the call stack */              }          }      }        private native void start0();  

native : 只要是帶了這個關鍵字的,說明 java的作用範圍達不到,只能去調用底層 C 語言的庫!

這部分知識作為java開發只需要了解即可,如果想去研究也可以看看嵌入式C的相關東西(C語言->編譯器—>彙編語言—>驅動->硬件)。

運行時數據區

程序計數器

每個線程都有一個程序計數器,是線程私有的。

程序計數器就是一塊十分小的內存空間;幾乎可以不計

作用: 看做當前位元組碼執行的行號指示器

分支、循環、跳轉、異常處理!都需要依賴於程序計數器來完成!

bipush 將 int、float、String、常量值推送值棧頂

istore 將一個數值從操作數棧存儲到局部變量表

iadd

imul

方法區

Method Area 方法區 是 Java虛擬機規範中定義的運行是數據區域之一,和堆(heap)一樣可以在線程之間共享!

JDK1.7之前

永久代:用於存儲一些虛擬機加載類信息,常量,字符串、靜態變量等等,這些東西都會放到永久代中;

永久代大小空間是有限的:如果滿了 OutOfMemoryError:PermGen

JDK1.8之後

徹底將永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空間;

元空間就是方法區在 HotSpot jvm 的實現;

方法區主要是存:類信息,常量,字符串、靜態變量、符號引用、方法代碼。

元空間和永久代,都是對JVM規範中方法區的實現。

元空間和永久代最大的區別:元空間並不在Java虛擬機中,使用的是本地內存!

設置元空間大小:-XX:MetasapceSize10m

堆和棧

堆和棧都是內存中相對獨立的一塊區域,jvm內存分為5個區域,分別是寄存器、本地方法區、方法區、棧內存、堆內存!

注⚠️:每個線程啟動的時候,都會創建一個PC(Program Counter,程序計數器)寄存器,我們習慣上稱呼為程序計數器,並不是什麼其他的東西!

棧和堆的區別

  • 1.堆是不連續的,所以分配的內存是在運行期確認的,因此大小不固定;堆對於整個應用程序都是共享、可見的;堆內存存儲的是實體;堆內存存放的實體會被垃圾回收機制不定時的回收

  • 2.棧是連續的,所以分配的內存大小要在編譯期就確認,大小是固定的;棧只對於線程是可見的,所以也是線程私有,他的生命周期和線程相同(所以說,棧裏面是一定不會存在垃圾回收的問題);棧內存存儲一些基本類型的值,對象的引用,方法等;棧內存的更新速度要快於堆內存(僅次於寄存器),因為局部變量的生命周期很短;棧內存存放的變量生命周期一旦結束就會被釋放。

面試題:java中的基本數據類型都是存儲在棧中的嗎?

面試基本上問到這裡都是三連,說說八大基本數據類型?說說堆和棧?接下來就是這個有坑的問題

答:不是

一:在方法中聲明的變量,即該變量是局部變量,每當程序調用方法時,系統都會為該方法建立一個方法棧,其所在方法中聲明的變量就放在方法棧中,當方法結束系統會釋放方法棧,其對應在該方法中聲明的變量隨着棧的銷毀而結束,這就局部變量只能在方法中有效的原因

在方法中聲明的變量可以是基本類型的變量,也可以是引用類型的變量。

(1)當聲明是基本類型的對象時,其變量及變量的引用都在棧中。    (2)當聲明的是引用類型(new出來的)時,所聲明的變量(該變量實際上是在方法中存儲的是內存地址值)是放在方法的棧中,該變量所指向的對象是放在堆內存中的。  

二:在類中聲明的變量是成員變量,也叫全局變量,放在堆中的(因為全局變量不會隨着某個方法執行結束而銷毀)。

同樣在類中聲明的變量即可是基本類型的變量 也可是引用類型的變量

(1)當為基本類型時,其變量名及其值放在堆內存中的    (2)當為引用類型時,其聲明的變量仍然會存儲一個內存地址值,該內存地址值指向所引用的對象。引用變量名和對應的對象仍然存儲在相應的堆中  

擴展–>請根據以下代碼說說6個對象的在堆棧中是如何分配的?

String str1 = "abc";  String str2 = "abc";  String str3 = "abc";  String str4 = new String("abc");  String str5 = new String("333");  String str6 = new String("333");  

結合下面的圖來理解String 常量存放的位置:

題外話:JVM目前有 SUN公司 HotSpot、BEA公司 JRockit、IBM公司 J9VM
關於堆在垃圾回收器裏面介紹!

執行引擎

類裝載器裝載負責裝載編譯後的位元組碼,並加載到運行時數據區(Runtime Data Area),然後執行引擎執行會執行這些位元組碼。
位元組碼必須通過類加載過程加載到JVM後才能夠被執行,執行有三種模式:

  • 1.解釋執行
  • 2.JIT(Just-In-Time)編譯即執行
  • 3.JIT與解釋混合執行
    來看下執行引擎的簡要執行流程:

關於執行引擎這種底層的東西了解即可,說白了就是執行編譯後位元組碼的一套概念模型!執行引擎包含了眾多可以操作位元組碼的執行指令,使程序能夠按照一定的順序執行!關於執行引擎(推薦知乎層層剝開JVM——位元組碼執行引擎,亦可以推薦孤盡老師《碼出高效》一書中的第四章「走進JVM」來深入了解)

垃圾回收器

呼,長出了一口氣!終於寫到和堆相關的了
看堆之前我們先了解下JVM的內存布局:

物理上只有 新生、養老;元空間在本地內存中,不在JVM中!

GC 垃圾回收主要是在 新生區和養老區,又分為 普通的GC 和 Full GC,如果堆滿了,就會爆出 OutOfMemory(OOM);

新生區

新生區 就是一個類誕生、成長、消亡的地方!

新生區細分: Eden、s(from to),所有的類Eden被 new 出來的,慢慢的當 Eden 滿了,程序還需要創建對象的時候,就會觸發一次輕量級GC;清理完一次垃圾之後,會將活下來的對象,會放入倖存者區(),……. 清理了 20次之後,出現了一些極其頑強的對象,有些對象突破了15次的垃圾回收!這時候就會將這個對象送入養老區!運行了幾個月之後,養老區滿了,就會觸發一次 Full GC;假設項目1年後,整個空間徹徹底底的滿了,突然有一天系統 OOM,排除OOM問題,或者重啟;

Sun HotSpot 虛擬機中,內存管理(分代管理機制:不同的區域使用不同的算法!)

Eden from to

99% 的對象在 Eden 都是臨時對象;

養老區

15次都倖存下來的對象進入養老區,養老區滿了之後,觸發 Full GC

默認是15次,可以修改!

永久區(Perm)

放一些 JDK 自身攜帶的 Class、Interface的元數據;

幾乎不會被垃圾回收的;

OutOfMemoryError:PermGen 在項目啟動的時候永久代不夠用了?加載大量的第三方包!

JDK1.6之前: 有永久代、常量池在方法區;

JDK1.7:有永久代、但是開始嘗試去永久代,常量池在堆中;

JDK1.8 之後:永久代沒有了,取而代之的是元空間;常量池在元空間中;

閑聊:方法區和堆一樣,是共享的區域,是JVM 規範中的一個邏輯的部分,但是記住它的別名 非堆

jvm內存調優

來看以下代碼:

package jvm;    /**   * 環境:jdk1.8   * jvm:HotSpot   *   *  默認情況:   *  maxMemory : 1808.0MB (虛擬機試圖使用的最大的內存量  一般是物理內存的 1/4)   *  totalMemory : 123.0MB (虛擬機試圖默認的內存總量 一般是物理內存的 1/64)   *   *  自定義: -XX:+PrintGCDetails; // 輸出詳細的垃圾回收信息   *          -Xmx: 最大分配內存; 1/4   *          -Xms: 初始分配的內存大小; 1/64   *          -Xmx1024m -Xms1024m -XX:+PrintGCDetails   */  public class HeapDemo {      public static void main(String[] args) {          // 獲取堆內存的初始大小和最大大小          long maxMemory = Runtime.getRuntime().maxMemory();          long totalMemory = Runtime.getRuntime().totalMemory();            System.out.println("maxMemory="+maxMemory+"(位元組)、"+(maxMemory/1024/(double)1024)+"MB");          System.out.println("totalMemory="+totalMemory+"(位元組)、"+(totalMemory/1024/(double)1024)+"MB");      }  }  

輸出信息如下:
heapSpace
由計算得出(305664K+699392K)/1024 = 981.5MB
說明新生代和老年代內存相加為整個分配內存大小,也從側面證明了元空間不存在於jvm中,它存在於本地空間中!

–認識了堆後,我們再來聊聊堆的內存調優,這是每個java開發都需要掌握的!
模擬oom,看看GC詳情:

package jvm;    import java.util.Random;  /**   * 設置內存大小:-Xmx8m -Xms8m -XX:+PrintGCDetails   */  public class HeapDemo1 {      public static void main(String[] args) {          System.gc(); // 手動喚醒GC(),等待cpu的調用          String str = "talk is cheap,show me your code";          while (true){              str += str                      + new Random().nextInt(999999999)                      + new Random().nextInt(999999999);          }          // 出現問題:java.lang.OutOfMemoryError: Java heap space      }  }  

輸出如下:

"C:Program FilesJavajdk1.8.0_171binjava.exe" -Xmx8m -Xms8m -XX:+PrintGCDetails "-javaagent:D:ideaIntelliJ IDEA 2019.3.3libidea_rt.jar=63475:D:ideaIntelliJ IDEA 2019.3.3bin" -Dfile.encoding=UTF-8 -classpath "C:Program FilesJavajdk1.8.0_171jrelibcharsets.jar;C:Program FilesJavajdk1.8.0_171jrelibdeploy.jar;C:Program FilesJavajdk1.8.0_171jrelibextaccess-bridge-64.jar;C:Program FilesJavajdk1.8.0_171jrelibextcldrdata.jar;C:Program FilesJavajdk1.8.0_171jrelibextdnsns.jar;C:Program FilesJavajdk1.8.0_171jrelibextjaccess.jar;C:Program FilesJavajdk1.8.0_171jrelibextjfxrt.jar;C:Program FilesJavajdk1.8.0_171jrelibextlocaledata.jar;C:Program FilesJavajdk1.8.0_171jrelibextnashorn.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunec.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunjce_provider.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunmscapi.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunpkcs11.jar;C:Program FilesJavajdk1.8.0_171jrelibextzipfs.jar;C:Program FilesJavajdk1.8.0_171jrelibjavaws.jar;C:Program FilesJavajdk1.8.0_171jrelibjce.jar;C:Program FilesJavajdk1.8.0_171jrelibjfr.jar;C:Program FilesJavajdk1.8.0_171jrelibjfxswt.jar;C:Program FilesJavajdk1.8.0_171jrelibjsse.jar;C:Program FilesJavajdk1.8.0_171jrelibmanagement-agent.jar;C:Program FilesJavajdk1.8.0_171jrelibplugin.jar;C:Program FilesJavajdk1.8.0_171jrelibresources.jar;C:Program FilesJavajdk1.8.0_171jrelibrt.jar;E:sCloudstudyDemooutproductionstudyDemo" jvm.HeapDemo1  [GC (Allocation Failure) [PSYoungGen: 1536K->504K(2048K)] 1536K->632K(7680K), 0.0022005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [GC (System.gc()) [PSYoungGen: 877K->488K(2048K)] 1005K->712K(7680K), 0.0017541 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [Full GC (System.gc()) [PSYoungGen: 488K->0K(2048K)] [ParOldGen: 224K->645K(5632K)] 712K->645K(7680K), [Metaspace: 3441K->3441K(1056768K)], 0.0181221 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]  [GC (Allocation Failure) [PSYoungGen: 1249K->325K(2048K)] 1894K->971K(7680K), 0.0010536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [GC (Allocation Failure) [PSYoungGen: 1531K->259K(2048K)] 2960K->1689K(7680K), 0.0009587 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [GC (Allocation Failure) [PSYoungGen: 1532K->487K(2048K)] 2961K->2165K(7680K), 0.0117368 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]  [GC (Allocation Failure) [PSYoungGen: 1688K->128K(2048K)] 6500K->4940K(7680K), 0.1107265 secs] [Times: user=0.02 sys=0.00, real=0.11 secs]  [GC (Allocation Failure) [PSYoungGen: 128K->128K(2048K)] 4940K->4940K(7680K), 0.0211234 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]  [Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2048K)] [ParOldGen: 4812K->2550K(5632K)] 4940K->2550K(7680K), [Metaspace: 3939K->3939K(1056768K)], 0.0311921 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]  [GC (Allocation Failure) [PSYoungGen: 138K->32K(2048K)] 4256K->4149K(7680K), 0.0064069 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]  [GC (Allocation Failure) [PSYoungGen: 32K->32K(2048K)] 4149K->4149K(7680K), 0.0007456 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [Full GC (Allocation Failure) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 4117K->3332K(5632K)] 4149K->3332K(7680K), [Metaspace: 3942K->3942K(1056768K)], 0.0250859 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]  [GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 3332K->3332K(7680K), 0.0014623 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 3332K->3277K(5632K)] 3332K->3277K(7680K), [Metaspace: 3942K->3942K(1056768K)], 0.0241555 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]  Heap   PSYoungGen      total 2048K, used 42K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)    eden space 1536K, 2% used [0x00000000ffd80000,0x00000000ffd8a978,0x00000000fff00000)    from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)    to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)   ParOldGen       total 5632K, used 3277K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)    object space 5632K, 58% used [0x00000000ff800000,0x00000000ffb337e8,0x00000000ffd80000)   Metaspace       used 3974K, capacity 4568K, committed 4864K, reserved 1056768K    class space    used 437K, capacity 460K, committed 512K, reserved 1048576K  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  	at java.util.Arrays.copyOf(Arrays.java:3332)  	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)  	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)  	at java.lang.StringBuilder.append(StringBuilder.java:208)  	at jvm.HeapDemo1.main(HeapDemo1.java:11)    Process finished with exit code 1  // 分析:GC 普通GC(輕GC)、Full GC 重GC  // 1536K 執行 GC之前的大小,504K  執行 GC之後的大小  // (2048K) young 的total大小 ,(5632K)old 的total 大小 (7680K)整個堆已被佔用的大小  // 0.0241555 secs 清理的時間、user 總計GC所佔用CPU的時間  // sys OS調用等待的時間   real 應用暫停的時間  

好了,理解這些參數後基本知道怎麼調優了。
其實Idea 還有一款插件JProfiler 專門用來分析程序內存佔用情況的,用JProfiler 可以dump 內存快照幫助我們快速定位問題。教程百度上有,此處不做說明!

注⚠️:JVM在進行GC時,並非每次都是對三個區域進行掃描的!大部分的時候都是指的新生代!

  • 普通GC:只針對新生代 【GC】
  • 全局GC:主要是針對老年代,偶爾伴隨新生代! 【Full GC】

GC四大算法:

引用計數法

特點:每個對象都有一個引用計數器,每當對象被引用一次,計數器就+1,如果引用失效,則計數器-1,如果為0,則GC可以清理;

缺點:

  • 計數器維護麻煩!
  • 循環引用無法處理!

注⚠️:JVM 一般不採用這種方式,現在一般使用可達性算法,GC Root…

複製算法

1、一般普通GC 之後,差不多Eden幾乎都是空的了!

2、每次存活的對象,都會被從 from 區和 Eden區等複製到 to區,from 和 to 會發生一次交換;記住一個點就好,誰空誰是to,每當倖存一次,就會導致這個對象的年齡+1;如果這個年齡值大於15(默認值,後面我們會講解調整),就會進入養老區;

優點:沒有標記和清除的過程!效率高!沒有內存碎片!

缺點:需要浪費雙倍的空間

Eden 區,對象存活率極低! 統計:99% 對象都會在使用一次之後,引用失效!推薦使用 複製算法

標記清除算法

老年代一般使用這個,但是會和我們後面的整理壓縮一起使用!

優點:不需要額外的空間!

缺點:兩次掃描,耗時較為嚴重,會產生內存碎片,不連續!

標記清除壓縮

減少了上面標記清除的缺點:沒有內存碎片!但是耗時可能也較為嚴重!

那我們什麼時候可以考慮使用這個算法呢?

在我們這個要使用算法的空間中,假設這個空間中很少,不經常發生GC,那麼可以考慮使用這個算法!

總結:  內存效率:複製算法 > 標記清除算法 > 標記整理(時間複雜度!)  內存整齊度:複製算法=標記整理>標記清除算法  內存利用率:標記整理 = 標記清除算法 > 複製算法  

面試題:

最後再來看看幾個面試題:

1.JVM 垃圾回收的時候如何確定垃圾,GC Roots?

  • 引用計數法 (每引用一次就加一,為0或-1就會被GC清除)
  • 可達性分析算法 (如果從 GC Root 這個對象開始,對於沒有關聯的對象GC認為不可達的,就會被清理)

2.什麼是GC Root?

  • 1、虛擬機棧中引用的對象!
  • 2、類中靜態屬性引用的對象
  • 3、方法區中的常量
  • 4、本地方法棧中 Native 方法引用的對象!

3.jvm 常用參數有哪些,你都用過哪些?

  • 1、標配參數:-version,-help,-showversion

  • 2、X參數:-Xint # 解釋執行,-Xcomp # 第一次使用就編譯成本地的代碼,-Xmixed # 混合模式(Java默認)

  • 3、XX參數:+ 或者 – 某個屬性值, + 代表開啟某個功能,- 表示關閉了某個功能 例如:jps -l ,jinfo -flag PrintGCDetail 進程號 # 查看某個運行中的java 程序,-XX:+PrintGCDetails # 打印GC詳情

  • 4.XX 參數之key = value值;例如:元空間大小:-XX:MetaspaceSize=128m#設置元空間大小,-XX:MaxTenuringThreshold=15#設置新生代需要經歷多少次GC晉陞到老年代中的最大閾值(默認值是15,最大也是15)

  • 1).-Xms 初始堆的大小,等價: -XX:InitialHeapSize

  • 2).-Xmx 最大堆的大小 ,等價:-XX:MaxHeapSize

-XX:+PrintFlagsInitial 查看 java 環境初始默認值;這裏面只要顯示的值,我們都可以手動賦值,不建議修改,了解即可!

= 默認值

:= 就是被修改過的值

java -XX:+PrintCommandLineFlags -version 打印出用戶手動選項的 XX 選項

4.你常用的項目,發佈後配置過JVM 調優參數嗎?

-Xms  -Xmx  -Xss  : 線程棧大小設置,默認 512k~1024k  -Xmn  設置年輕代的大小,一般不用動!  -XX:MetaspsaceSize  設置元空間的大小,這個在本地內存中!  -XX:+PrintGCDetails  -XX:SurvivorRatio  設置新生代中的 s0/s1 空間的比例;  `uintx SurvivorRatio  = 8`  Eden:s0:s1 = 8:1:1  `uintx SurvivorRatio  = 4`  Eden:s0:s1 = 4:1:1  -XX:NewRatio  設置年輕代與老年代的佔比:  `NewRatio  = 2`   新生代1,老年代是2,默認新生代整個堆的 1/3;  `NewRatio  = 4`   新生代1,老年代+是4,默認新生代整個堆的 1/5;  -XX:MaxTenuringThreshold  進入老年區的存活閾值;  `MaxTenuringThreshold  = 15`  

小結:至此JVM的知識,基本已總結完成,推薦書《碼出高效》;買了很多書但是一直沒看,個人博客新增了欄目「拆書」 有時間會把買的書啃完了寫寫心得體會,或者看到精彩的地方會分享出來,當然不至於技術一類。為能看到這兒的自己點個贊吧,加油!