囉裏吧嗦jvm

一.爲何要了解jvmhtml

有次作項目的時候,程序run起來的時候,老是報OutOfMemoryError,有老司機教咱們用jconsole.exe看內存溢出問題java

就是這貨
啓動jconsole後,發現一個是進程是它本身,一個是個人eclipse,哈哈,如今還不習慣idea, 還一個就是咱們項目啓動的進程了,項目我用的maven搭建的,引入了tomcat插件,linux

jconsole能夠看不少東西, 須要你們慢慢摸索,包括能夠檢測死鎖,當時的項目啓動程序,就看堆內存佔用的空間不斷上升,最後報錯,c++

當時銀行的jvm內存設置已經比較大了,最後發現啓動時日誌打印不合理,打印的東西太多,佔用內存空間太大算法

瞭解jvm可讓咱們排查問題多出一個角度,而不是每次都是重啓解決問題,固然通常狀況下都是邏輯代碼寫的很差,產生了大量的內存,致使堆不夠用發生windows

 

jvm內存分爲 堆 和 非堆
堆又分爲:

1.老年代
Old Gen:年輕代的對象若是可以挺過數次收集,就會進入老人區

2.新生代
Eden Space(伊甸園):一個Eden,全部新建對象都會存在於該區
Survivor Space(倖存者區):兩個Survivor區,用來實施複製算法

3.持久代 
permanent:裝載Class信息等基礎數據,默認64M,通常狀況下,持久代是不會進行GC的
-XX:MaxPermSize=
  
-Xms 是堆的初始內存
-Xmx 是堆的最大內存
-Xmn 年輕代大小
-Xss 每一個線程的堆棧大小

經過java -X 命令可查看

java –Xms128m –Xmx128m 

 
 

非堆分爲數組

Metaspacetomcat

Code Cache多線程

Compressed Class Spaceeclipse

 

 

二.當你輸入java Hello時發生了什麼

 

如今咱們固然使用了各類集成的開發工具,好比eclipse,

最原始的執行java程序,

首先你得先按照好jdk,而後配置好環境變量, 讓系統能在任意目錄下都能找到java命令 並執行

其次寫一個java文件,符合規範,好比類名和文件名一致,類要是public的,javac會對其進行校驗

經過cmd打開windows的命令窗口, 使用javac xxx.java 命令將其編譯成字節碼文件, java虛擬機只認字節碼文件,不管你是經過什麼途徑獲得它的,只要符合字節碼文件規範,就能在不一樣系統上的jvm運行,咱們開發一般在windows環境, 而後部署項目在linux環境,只要一次編譯,處處運行

 

執行java xxx 命令,這裏jvm究竟作了什麼呢

     String sourcePath = "C:/Users/"+System.getProperties().getProperty("user.name")+"/Desktop";
        
        JavaFile javaFile = JavaFile.builder("proxy", typeSpecBuilder.build()).build();
        // 爲了看的更清楚,我將源碼文件生成到桌面
       
        javaFile.writeTo(new File(sourcePath));
 
        // 編譯
        JavaCompiler.compile(new File(sourcePath + "/proxy/Proxy.java"));
 
        // 使用反射load到內存
        URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL("file:C:\\Users\\"+System.getProperties().getProperty("user.name")+"\\Desktop\\") });

        Object obj = null;
        
        //Classloader:類加載器,你可使用自定義的類加載器,咱們的實現版本爲了簡化,直接在代碼中寫死了Classloader。
        
        Class clazz1 = classLoader.loadClass("proxy.Proxy");
        
        System.out.println(clazz1);
        System.out.println(clazz1.getDeclaredConstructors().getClass());
        
        //將生成的TimeProxy編譯成class 使用類加載器加載進內存中 再經過反射或者該類的構造器 
        //再經過構造器將其代理類 TimeProxy 構造出來
        
        //NoSuchException  打印classz信息 發現 剛開始建立類 沒有使用public 
        Constructor constructor = clazz1.getConstructor(InvocationHandler.class);
        System.out.println("constructor" + constructor);
        obj = constructor.newInstance(handler);

 報錯那就是少引了一個jar包

<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.8.0</version>
</dependency>

 

首先根據內存配置,爲jvm申請內存空間,並按照 jvm的 【內存模型】  劃分好 區域

建立一個引導類加載器實例,初步加載系統類到內存方法區區域中;

建立JVM 啓動器實例Launcher

並取得類加載器ClassLoader

使用上述獲取的ClassLoader實例加載咱們定義的類

加載完成時候JVM會執行Main類的main方法入口,執行Main類的main方法結束,java程序運行結束,JVM銷燬

上面的看看就好了,說的很籠統,基本流程知道就ok,就是jvm裏面也是各類代碼實現的,有用到c++,下圖是看程序jvm內存大小

 

 

 

個人電腦 默認是128M 和 1.75G

 

三.jvm內存模型

 

 3.1 運行時區域

xxx.java文件經過 JavaCompiler java編譯器 編譯成xxx.class文件----

執行java命令其實就啓動了java.exe,啓動一個Java程序時,一個JVM實例就產生了

JVM實例對應了一個獨立運行的java程序它是進程級別------

jvm實例建立啓動器得到類加載器

classLoader,加載.class文件進內存

 

由誰來執行,由執行引擎來執行, 執行引擎能夠經過解釋器 或者 即時編譯器 去執行指令

反彙編命令

javap -c xxx Java 字節碼的指令

 

在JVM實現中,線程爲Execution Engine的一個實例,main函數是JVM指令執行的起點

JVM會建立main線程來執行main函數,以觸發JVM一系列指令的執行

classLoader,加載.class文件進內存能夠細分爲3步驟

3.1.1 加載 xxx.class是一個二進制字節碼文件, 加載到method are方法區

3.1.2 連接 能夠分爲3步

3.1.2.1 驗證 驗證.class文件在結構上知足JVM規範的要求,驗證工做可能會引發其餘類的加載但不進行驗證和準備

3.1.2.2 準備 正式爲類變量分配內存並設置類變量初始值的階段,,「一般狀況」下是數據類型的零值,特殊狀況就是你定義了是static final ,那麼jvm會識別爲常量,爲該變量設置一個ConstantValue屬性,準備階段就初始化

 

3.1.2.3 解析 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

3.1.2.4 初始化 簡單來講真正開始賦值, 收集類變量 , 靜態塊, 執行類構造器,虛擬機也規定了5種狀況須要對類作個初始化, 例如初始化子類發現父類還沒初始化過, 那麼先初始化父類, 每一個類在初始化前 ,前面的加載連接都是必須執行的,每一個類只被初始化一次

 

初始化還有不少規則,好比使用數組定義來引用類, 不會觸發該類的初始化, DemoTest[] a = new DemoTest[10],假如DemoTest裏有類變量,靜態塊,則不會初始化

public class ClassLoadTest {
    
    public static void main(String[] args) {
        SuperCla[]  a = new SuperCla[11];
     }
}

class SuperCla{
static {
        
        System.out.println("xxxxx");
    }
}
執行結果是不輸出任何結果
例如子類引用定義在父類的靜態變量,則不會觸發子類的初始化
例如編譯階段就有常量屬性的,就是那種final static ,就不會觸發類的初始化, 由於直接從常量池中取

一個經典的例子

public class ClassLoadTest {
    
    public static void main(String[] args) {
    
     }
    static {
        
        System.out.println("1");
    }
    static ClassLoadTest a = new ClassLoadTest();
    
    static {
        
        System.out.println("2");
    }
    
    {
        System.out.println("6");
    }
    
    ClassLoadTest(){
        System.out.println("3");
        System.out.println("b=" + b + ",c =" + c + ",d = " + d+ ",e=" + e);
    }
    int b = 100;
    
    static int c = 200;
    
    static final int d = 300;
    
    static int e = 400;
    
    static void func() {
        System.out.println("第二次c=" + c );
        System.out.println("4");
    }
    
}

1
6
3
b=100,c =0,d = 300,e=0
2

 

 

 

 

因此歸到該類上就是 java ClassLoadTest 命令

 

jvm 加載 ClassLoadTest 類

 

|

驗證 沒毛病

|

準備 類變量要分配內存 並設置初始值了 裏面有3個類變量 static ClassLoadTest a = new ClassLoadTest();

static int c

static final int d

a呢 就初始值就是null c內初始值是0 ,由於這個一般狀況下是數據類型的零值 意外狀況就是加了final關鍵字 因此會爲d生成一個ConstantValue屬性,該屬性值是300

 

|

原本準備完了是要繼續初始化的 ,可是因爲 類變量的初始化是它自身 實例的初始化 有new 關鍵字

因此 就先初始化 該類的實例了,

先執行非靜態塊 ,再執行構造方法

 

類的初始化就直接中斷掉了....

因此先打印出來的 c 是0

 

(若是你在func裏 打印c的值 會發現c仍是200)

|

 

當前面的加載、連接、初始化都結束後,JVM 會調用 ClassLoadTest 的 main 方法。被調用的 main 必須被修飾爲 public , static, void

 

|

按照源碼的順序 執行靜態代碼塊 和 類變量 ---&gt; 先打印1 ---&gt;而後就跑偏了 靜態變量new了本身的一個實例 因而去初始化本身的實例---&gt;

 

先執行 非靜態塊 打印6-----&gt; 再執行構造方法----&gt;打印3----&gt;打印bcd變量的值 此時 b是實例變量,成語變量 new ClassLoadTest()進行初始化,因此是100

 

c 是類變量,因爲前面準備階段---先去執行了 ClassLoadTest a = new ClassLoadTest(); c仍是零值 ,d因爲是final static, 準備階段就默認給個值

a= null b=100 c=0 d=300---------------

在按照源碼順序執行 第二個靜態代碼塊 打印--2

 


 

具體的類加載流程咱們知道了, 如今來看看類加載到jvm內存後, 把 哪些東西 放在內存模型的 哪一個位置

 

首先來看線程共享的區域: 方法區 和 堆

 1.先看方法區 method area , 存儲了每一個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量、(即時)編譯器編譯後的代碼,

Class文件中還有常量池,存儲編譯期間生成的字面量和符號引用,

而方法區裏面的【運行時常量池】就是該類或接口的運行時表示

在這裏要介紹下String 的 intern方法, 該方法在運行期間也可將新的常量 放進常量池, 大大減少堆的空間

new String(「abc」).intern()

abc 會出如今堆裏是一個新的對象, 調用intern方法後, 會把abc 放在常量池中

並返回指向該常量的引用

intern用來返回常量池中的某字符串,若是常量池中已經存在該字符串,則直接返回常量池中該對象的引用。
不然,在常量池中加入該對象,而後 返回引用。
原文舉例:
Now lets understand how Java handles these strings. When you create two string literals:

String name1 = "Ram"; 

String name2 = "Ram";

In this case, JVM searches String constant pool for value "Ram", and if it does not find it there 
then it allocates a new memory space and store value "Ram" and return its reference to name1. 
Similarly, for name2 it checks String constant pool for value "Ram" but this time it find "Ram" 
there so it does nothing simply return the reference to name2 variable. 
The way how java handles only one copy of distinct string is called String interning.


舉例
String s1 = new String("aaa").intern();
String s2 = "aaa";
System.out.println(s1 == s2);    // true

2.堆: 存儲對象及數組自己, 而非引用,是垃圾收集器管理的主要區域

3.接下來就是每一個線程獨有的了,本地方法棧,jvm棧, 在HotSpot中,沒有JVM Stacks和Native Method Stacks之分,功能上已經合併,因此放在一塊兒介紹,原理都相似

JVM 棧中存放的是一個個的棧幀,每一個棧幀對應一個被調用的方法,因此很容易想到局部變量確定在棧幀中,由於方法裏定義的是局部變量,還有些其餘的看圖

 

棧的使用是好比 線程執行test方法,就會建立test的棧幀,並將此棧幀壓棧,發現inner()方法,又建立一個棧幀,壓棧, 等inner()
方法執行完畢,則將inner()出棧,在將test()出棧
public class A {
        void test() {
                inner();
        }
        
        void inner(){
        }

 4.最後一個 程序計數器

在JVM中,多線程是經過線程輪流切換來得到CPU的執行時間, 就是說在微觀上,線程都是走走停停, 在某一個時刻一個cpu的內核只會執行 某一條線程的指令, 那麼須要一個工具記錄以前線程執行到哪兒了,只是打個比方 A線程執行到3這個指令的位置停住了, 把cpu讓給線程B, 線程B執行完畢後, A線程怎麼知道剛纔執行到哪兒了呢

那麼只能是用一個 每一個線程獨有的 程序計算器 來記住A線程---位置3 ,這個也很好理解這個佔用內存大小是固定的
public void test();
    Code:
       0: new           #17                 // class java/lang/String
       3: dup
       4: ldc           #19                 // String 1
       6: invokespecial #21                 // Method java/lang/String."&lt;init&gt;":
       (Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: invokevirtual #24  

 

 

 

四.GC垃圾回收

首先咱們要肯定GC垃圾回收主要做用的區域, 因爲 jvm棧,本地方法棧,程序計數器都是隨線程生滅的,因此gc主要是對方法區和堆裏面的內容進行回收

而堆又是佔內存最大的一塊區域,存放對象實例,因此也是回收重點。

 

這些知識有點抽象,因此咱們但願很直觀的看到, 用 cmd 命令行執行class文件的時候 指定參數也能夠看,不過咱們如今都是用eclipse

選擇xx.java --右擊---Run as -&gt; Run configurations

- java應用名 -arguments -VM arguments,加入jvm參數

-Xms20m --jvm堆的最小值  
-Xmx20m --jvm堆的最大值  
-XX:+PrintGCTimeStamps -- 打印出GC的時間信息  
-XX:+PrintGCDetails  --打印出GC的詳細信息  
-verbose:gc --開啓gc日誌  
-Xloggc:d:/gc.log -- gc日誌的存放位置  
-Xmn10M -- 新生代內存區域的大小  

-XX:SurvivorRatio=8 --新生代內存區域中Eden和Survivor的比例

確保設置的是本身要測試的類, 不是的話先執行一遍

那具體採起什麼措施去回收這些對象呢,針對方法區的變量,類信息回收, 某些虛擬機的永久代, 條件比較苛刻, 並且佔用空間小, 並且能夠經過配置決定,在這裏不過多討論

 


 

回收的斷定-對象死不死, 回收的時機--死了什麼時候處理, 回收的方法策略--火化仍是土葬

1.回收的斷定: 針對堆的對象實例進行回收,先判斷這些對象有沒有用, 能不能被回收

有兩種算法

Java不使用的引用計數算法
:當對象有個地方引用它,計數器+1, 引用失效計數器-1, 計數器爲0則斷定爲不可以使用對象

 缺點:沒法解決對象互相 循環引用

public class Test {
    public Test test; 
    
    public static void main(String[] args){
        Test t1 = new Test();
        Test t2 = new Test();
        t1.test = t2;
        t2.test = t1;
        
        System.gc();
    }
    
}
2.可達性分析算法(根搜索算法):

這個算法的基本思想是經過一系列稱爲「GC Roots」的對象做爲起始點,

 
 

從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,

 
 

當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證實此對象是不可用的。

在Java語言中,能夠做爲GCRoots的對象包括下面幾種:

(1). 虛擬機棧(棧幀中的局部變量區,也叫作局部變量表)中引用的對象。

(2). 方法區中的類靜態屬性引用的對象。

(3). 方法區中常量引用的對象。

(4). 本地方法棧中JNI(Native方法)引用的對象。

 

 

 8,9,10會被回收

2. 回收的時機:

即便是被判斷不可達的對象,也要再進行篩選,當對象沒有覆蓋finalize()方法,

或者finalize方法已經被虛擬機調用過,則沒有必要執行;

若是有必要執行——放置在F-Queue的隊列中——Finalizer線程執行。

注意:對象能夠在被GC時能夠自我拯救(this),機會只有一次,由於任何一個對象的finalize()方法都只會被系統自動調用一次

。並不建議使用,應該避免。使用try_finaly或者其餘方式。

這個只是判斷哪些對象存活,哪些對象死亡,並不真正進行回收, 並且即便被斷定爲不可達對象, 回不回收仍是要通過兩次標記(瞭解)

1. 若是對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,

(1)該對象被第一次標記並進行一次篩選

篩選條件爲是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了

,則均視做沒必要要執行該對象的finalize方法,即該對象將會被回收。

 

反之,若對象覆蓋了finalize方法而且該finalize方法並無被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,

以後會由虛擬機自動創建的、優先級低的Finalizer線程去執行,而虛擬機沒必要要等待該線程執行結束,即虛擬機只負責創建線程,

其餘的事情交給此線程去處理。

 

(2).對F-Queue中對象進行第二次標記,若是對象在finalize方法中拯救了本身,即關聯上了GCRoots引用鏈,

如把this關鍵字賦值給其餘變量,那麼在第二次標記的時候該對象將從「即將回收」的集合中移除,若是對象仍是沒有拯救本身,

那就會被回收。以下代碼演示了一個對象如何在finalize方法中拯救了本身,然而,它只能拯救本身一次,第二次就被回收了。

package testGc;

/*
 * 此代碼演示了兩點:
 * 1.對象能夠再被GC時自我拯救
 * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
 * */
public class FinalizeEscapeGC {
    
    public String name;
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public FinalizeEscapeGC(String name) {
        this.name = name;
    }

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    
    
    //若是判斷爲該對象不可達 
    
    //第一次標記 
    
    //對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了
    //則均視做沒必要要執行該對象的finalize方法,即該對象將會被回收
    
    //此FinalizeEscapeGC對象覆蓋了finalize()  且該finalize方法並無被執行過
    //這個對象會被放置在一個叫F-Queue的隊列中 
    //以後會由虛擬機自動創建的、優先級低的Finalizer線程去執行
    
    //第二次標記
    
    //關聯上了GCRoots引用鏈則該對象將從「即將回收」的集合中移除
    //不然會被回收
    
    //只能第一次執行的時候能夠拯救本身一次 不被回收
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        System.out.println(this);
        FinalizeEscapeGC.SAVE_HOOK = this;
        //關聯上了GCRoots引用鏈
        //把this關鍵字賦值給其餘變量
        //方法區中的類靜態屬性引用的對象
    }

    @Override
    public String toString() {
        return name;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC("leesf");
        System.out.println(SAVE_HOOK);
        // 對象第一次拯救本身
        SAVE_HOOK = null;
        System.out.println(SAVE_HOOK);
        
        // 兩次標記  1.重寫了finalize方法且沒有執行過 放隊列  
        // 2.隊列中堅持是否是有 關聯上引用鏈  有便可以拯救本身一次 
        System.gc();
        
        // 由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }

        // 下面這段代碼與上面的徹底相同,可是這一次自救卻失敗了
        // 一個對象的finalize方法只會被調用一次
        SAVE_HOOK = null;
        System.gc();
        // 由於finalize方法優先級很低,因此暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
    }
}

重點就是finalize方法只會執行一次,只有第一次能夠拯救本身

 

3.而是經過垃圾收集算法和垃圾收集器來具體執行回收,

垃圾收集器是 垃圾收集算法的具體實現

一、標記-清除(Mark-Sweep)算法
首先標記出全部須要回收的對象,標記完成後統一回收全部被標記的對象

效率低

2、複製(Copying)算法
它將可用的內存分爲兩塊,每次只用其中一塊,當這一塊內存用完了
就將還存活着的對象複製到另一塊上面,而後再把已經使用過的內存空間一次性清理掉

分代收集算法就把內存劃分爲幾塊, 一般在新生代用複製算法
【新生代
Eden Space(伊甸園):一個Eden,全部新建對象都會存在於該區
Survivor Space(倖存者區):兩個Survivor區,用來實施複製算法】

若是採起1:1, 那很是不划算,意味着可用內存只能用一半,一般新生代的內存 Eden + Survivor 佔90%, Survivor空閒佔10%,
回收就 先複製  eden和survivor活着的對象到  Survivor, 再清理調eden 和 survivor

  
(HotSpot虛擬機默認Eden區和Survivor區的比例爲8:1)

三、標記-整理(Mark-Compact)算法

分代收集算法就把內存劃分爲幾塊, 一般在老年代用標記整理法
由於老年代一般都是不易被回收的對象, 採用複製算法很是不划算, 存活90%, 複製後仍是90%

讓全部存活對象都向一端移動,而後直接清理掉邊界之外的內存

4、分代收集算法
以上內容的結合, 根據對象的生命週期,內存劃分 新生代, 老年代, 
大批對象死去、少許對象存活的(新生代),使用複製算法
對象存活率高、沒有額外空間進行分配擔保的(老年代)採用其餘兩個方法</pre><figure><hr/></figure><h2>五.JVM性能調優</h2><pre class="prism-token token language-js">經過上面的一些理論, 具體的實踐仍是在項目中就具體狀況進行分析, 固然99%的oom 都是代碼問題致使的,
好比打印日誌佔用的內存空間不足,
好比某個線程產生大量對象缺沒有被釋放

少許是設置緣由,好比最大堆內存設置的過小,
好比卡頓是由於頻繁發生FUll GC, 那麼如何調整老年代和新生代的大小

 

 

五.JVM性能調優

 經過上面的一些理論, 具體的實踐仍是在項目中就具體狀況進行分析, 固然99%的oom 都是代碼問題致使的,\n好比打印日誌佔用的內存空間不足,\n好比某個線程產生大量對象缺沒有被釋放\n\n少許是設置緣由,好比最大堆內存設置的過小,\n好比卡頓是由於頻繁發生FUll GC, 那麼如何調整老年代和新生代的大小

六.守護線程

任何非守護線程還在運行,程序就不會終止, 在全部用戶線程都終止了, 那麼守護線程也終止,虛擬機退出

最典型的應用就是GC(垃圾回收器)

相關文章
相關標籤/搜索