JVM學習思考

  畢業以來技術上一直沒有太大進步,仔細一想多是沒有作技術分享,我喜歡把學習總結記錄在印象筆記中,那麼理解的是對是錯也就沒人能評判一下。爲了技術進步,接下來將陸續把一些學習總結遷移到博客園中,歡迎你們多多指正!html

JVM的定義

Jvmjava

Java虛擬機。一次編譯,處處運行的前提算法

Jrespring

JVM+核心類庫數組

Jdk多線程

JVM+核心類庫+擴展類庫jvm

JMMide

Java內存模型。主要用於多線程共享數據oop

子模塊學習

自動內存管理(分配、回收內存)、虛擬機執行子系統(類加載機制、虛擬機字節碼執行引擎)

 

JVM運行時數據區

 

JDK8之後:

(圖摘自java3y)

程序計數器:

當前線程所執行的字節碼的行號指示器。

Java虛擬機棧:

Java方法執行的內存模型:每一個方法執行時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態鏈接、方法出口等。

本地方法棧:

爲虛擬機執行的native方法服務。

Java堆:

存放對象實例。

常量池:常量池記錄的是代碼出現過的常量、類名、成員變量等以及符號引用(類引用、方法引用,成員變量引用等)。

方法區(元空間):

存儲已被虛擬機加載的類元數據信息。

 

內存溢出:

內存不夠用。OutOfMemoryError: Java heap space、StackOverFlowError、OutOfMemoryError: Metaspace

內存泄漏:

無用內存未及時回收。最終可能致使內存溢出。

 

示例:

一、各類內存溢出。二、建立String對象時的內存分配

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * jvm內存溢出示例
 * -Xms 堆初始值
 * -Xmx 堆最大值
 * -Xmn 新生代大小
 * -Xss 每一個線程的棧大小
 * -server -Xmx20m -Xms20m -Xmn10m -Xss1m -XX:+HeapDumpOnOutOfMemoryError
 * -server -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails  -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
 */
public class MemErrorDemo {
    int depth = 0;

    /**
     * 內存溢出錯誤(OOM)
     */
    public void OOMError() {
        List<byte[]> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(new byte[5 * 1024 * 1024]);
            System.out.println("loop count: " + (++i));
        }
    }

    /**
     * 棧溢出錯誤(StackOverFlowError)
     */
    public void SOFError() {
        try {
            depth++;
            SOFError();
        } finally {
            System.out.println("遞歸count: " + depth);
        }
    }

    /**
     * 元空間錯誤
     * 使用cglib生成動態代理類
     */
    public void MetaSpaceError() {
        int i = 0;
        try {
            while (true) {
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        return proxy.invokeSuper(obj, args);
                    }
                });
                enhancer.create();
            }
        } catch (Exception e) {
            System.out.println("generate class count" + i);
            e.printStackTrace();
        }
    }

    static class OOMObject {

    }

    public static void main(String args[]) {
        MemErrorDemo memErrorDemo = new MemErrorDemo();
        //memErrorDemo.OOMError();
        //memErrorDemo.SOFError();
        memErrorDemo.MetaSpaceError();
    }
}
/**
 * 字符串對象建立時內存分配
 * 字符串常量池(內容在編譯期肯定)、堆
 * 編譯期、運行期
 */
public class StringObjDemo {
    public static void main(String args[]) {
        String a = "he";
        String b = "llo";

        String c = "hello";
        String d = "he" + "llo";
        String e = new String("hello");
        String f = a + "llo";
        String g = a + b;
        String h = e.intern();

        System.out.println(c == d);
        System.out.println(c == e);
        System.out.println(c == f);
        System.out.println(c == g);
        System.out.println(f == g);
        System.out.println(c == h);
    }
}

 

JVM垃圾回收

回收的內容和區域

內容:廢棄的對象、常量和無用的類;區域:堆、元空間

 

判斷對象是否生存

引用記數法:

難解決兩個或多個對象的循環引用情況。

可達性分析算法:

可做爲GCRoots的對象包括:

一、 虛擬機棧中(棧幀中本地變量表)中引用的對象。

二、 元空間中靜態屬性引用的對象

三、 常量池中常量引用的對象

四、 本地方法棧中native方法引用的對象。

 

強引用:平時使用最多的的引用,若對象有強引用,而且從GCRoots到其可達(可達性分析),則不會被GC回收。

軟引用:在堆內存未發生溢出時不會回收有軟引用的對象,在內存溢出將要發生前,先對軟引用關聯的對象進行回收,回收後仍溢出,則拋OOM異常。

弱引用:對象只有弱引用,會在下一次GC進行回收時被回收

虛引用:不能經過虛引用獲取對象實例,不影響對象生存時間,惟一做用是在對象回收時收到一個系統通知

 

垃圾收集算法

標記-清除算法:標記出須要回收的對象,而後清楚有標記的對象。

標記-整理算法:標記出須要回收的對象,而後移動到一端,直接清理邊界外的內存。

複製算法:將內存容量分兩部分,保持在某一時刻始終有一部分是空的,將仍然存活的對象複製到此區域。

分代收集算法:

新生代:複製算法。朝生夕死,對象存活率低,將區域分3份,eden:survivor0:survivor1=8:1:1

老年代:標記-清楚/標記-整理算法。

 

回收事件:

新生代回收事件:minor gc

老年代回收事件:major gc

所有回收(包括MetaSpace):full gc(Stop the world),默認堆空間使用到達80%(可調整)的時候會觸發fgc

 

內存分配與回收策略

一、 對象優先在Eden分配

二、 大對象直接進入老年代

三、 長期存活的對象進入老年代。每熬過一次minor gc年齡增長1歲,默認15歲進入老年代。

四、 動態對象年齡判斷。Survivor空間中相同年齡全部對象大小的和大於survivor空間的一半,大於或等於該年齡的對象直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

五、 空間分配擔保。新生代進行復制回收時,survivor空間不夠用,survivor沒法容納的對象進入老年代。

 

垃圾收集器

 

JVM類加載機制

類加載時機

加載、驗證、準備、初始化和卸載順序肯定,解析不必定,有時會在初始化以後——java運行時綁定(動態綁定/晚期綁定)。

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

初始化的5種狀況,若是類未初始化則馬上初始化:

一、 遇到new、getstatic、putstatic、或invokestatic4條字節碼指令。這4條指令常見場景:使用new關鍵字實例化對象、讀取一個類的靜態字段(被final修飾、已在編譯期將結果放入常量池的靜態字段除外)、調用一個類的靜態方法。

二、 使用java.lang.reflect包的方法對類反射調用。

三、 初始化一個類的時候,若是父類未初始化,則優先初始化父類(接口除外,只在使用時初始化)。

四、 虛擬機啓動時,用戶須要指定一個要執行的主類(main方法所在類),虛擬機會先初始化這個主類。

五、 使用動態語言時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,其所對應的類未初始化,則先觸發其初始化。

這5類是對一個類進行主動引用,除此以外全部引用類的方式都不會觸發初始化,稱爲被動引用。

示例:類的被動引用

 

/**
 * 類加載階段,被動引用示例
 * 一、經過子類引用父類靜態字段,不會致使子類初始化
 * 二、經過數組定義引用類不會觸發此類的初始化。
 *    數組自己不經過類加載器建立,由java虛擬機直接建立。
 * 三、常量在編譯期存入調用類的常量池,本質上沒有直接引用到定義常量的類
 */
public class ClassLoadingDemo {
    public static void main(String[] args) {
        //System.out.println(SubClass.h);
        //SubClass[] scs = new SubClass[10];
        System.out.println(SubClass.w);
    }
}

class SuperClass {
    static {
        System.out.println("SuperClass init");
    }

    public static String h = "hello";
    public static final String w = "world";
}

class SubClass extends SuperClass{
    static {
        System.out.println("SubClass init");
    }
}

 

類加載器

即便兩個類來自同一個class文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,那這兩個類一定不相等。

雙親委派模型

 

對類的加載請求,優先讓父加載器加載,只有父加載器反饋沒法加載時,子加載器纔會嘗試加載。

類加載器之間的父子關係並非經過繼承來實現的,而是經過組合關係。

 

JVM字節碼執行引擎

運行時幀棧結構——動態鏈接

Class文件的常量池中存在大量的符號引用,這些符號引用一部分會在類加載階段或者第一次使用時轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次運行期間轉化爲直接引用,稱爲動態鏈接。

方法調用

目的:惟一任務是肯定方法調用的版本即調用哪個方法。

理解概念:靜態類型、實際類型

解析調用:

靜態過程,在編譯階段就徹底肯定,在類裝載的解析階段就會把涉及的符號引用所有轉化爲直接引用,不會延遲到運行期。

在解析階段肯定惟一的版本,此類方法包括:靜態方法、私有方法、實例構造器、父類方法4類。

 

分派:

理解概念:多態

方法的宗量:方法的接收者與方法的參數

靜態分派經過靜態類型肯定方法的執行版本,發生在編譯階段。典型應用:方法的重載

示例:方法的重載

/**
 * 靜態分派示例
 * 根據靜態類型肯定方法版本
 */
public class MethodOverLoadDemo {
    public static class SuperClass {
    }

    public static class Sub1 extends SuperClass {
    }

    public static class Sub2 extends SuperClass {
    }

    public void sayHello(SuperClass s) {
        System.out.println("hello super");
    }

    public void sayHello(Sub1 s) {
        System.out.println("hello sub1");
    }

    public void sayHello(Sub2 s) {
        System.out.println("hello sub2");
    }

    public static void main(String[] args) {
        SuperClass s1 = new Sub1();
        SuperClass s2 = new Sub2();
        MethodOverLoadDemo ml= new MethodOverLoadDemo();
        ml.sayHello(s1);
        ml.sayHello(s2);
    }
}

動態分派

Java語言是一門靜態多分派,動態單分派的語言。靜態分派時經過方法接受者即靜態類型、方法參數兩個宗量肯定方法版本,動態分派時則在已有版本(靜態分派時肯定)中只經過方法接收者即實際類型肯定最終方法版本。

典型應用:方法的重寫

示例:方法的重寫

/**
 * 動態分派示例
 * 方法重寫-jvm選擇方法版本:
 * 一、在編譯期間根據靜態類型選擇一個方法版本
 * 二、在運行期間根據實際類型和編譯期間已選的版本選擇最終版本
 */
public class MethodOverrideDemo {

    public static class O {
        public void m1(O o) {
            System.out.println("O-m1");
        }
    }

    public static class A extends O {
        public void m1(A a) {
            System.out.println("A-m1");
        }
    }

    public static class B extends A {
        @Override
        public void m1(A a) {
            System.out.println("B-m1");
        }

        public void m1(B b) {
            System.out.println("B-m2");
        }

        public void m1(O b) {
            System.out.println("B-m3");
        }
    }


    public static void main(String[] args) {
        A a = new B();
        a.m1(a);

        B b = new B();
        a.m1(b);

        b.m1(b);
    }
}

 

 

參考資料:

《深刻理解Java虛擬機——JVM高級特性與最佳實踐》 周志明 著

Java3y: https://www.cnblogs.com/Java3y/p/9296496.html

相關文章
相關標籤/搜索