JVM虛擬機筆記

1.執行java文件

zhangjg@linux:/deve/workspace/HelloJava/src$ javac HelloWorld.java   
zhangjg@linux:/deve/workspace/HelloJava/src$ ls  
HelloWorld.class  HelloWorld.java  

HelloWorld.class並不能直接被系統識別執行,需要java虛擬機來執行。

每啓動一個JAVA程序,必然會先創建一個JVM進程,由JVM加載class文件,轉成字節碼執行程序。

JVM是進程,執行的只有是線程,可以是一個主線程和多個其他線程

(取自 https://www.cnblogs.com/yixianyixian/p/7680321.html

java命令首先啓動虛擬機進程,虛擬機進程成功啓動後,讀取參數“HelloWorld”,把他作爲初始類加載到內存,對這個類進行初始化和動態鏈接(關於類的初始化和動態鏈接會在後面的博客中介紹),然後從這個類的main方法開始執行。也就是說我們的.class文件不是直接被系統加載後直接在cpu上執行的,而是被一個叫做虛擬機的進程託管的。首先必須虛擬機進程啓動就緒,然後由虛擬機中的類加載器加載必要的class文件,包括jdk中的基礎類(如String和Object等),然後由虛擬機進程解釋class字節碼指令,把這些字節碼指令翻譯成本機cpu能夠識別的指令,才能在cpu上運行。

 

從這個層面上來看,在執行一個所謂的java程序的時候,真真正正在執行的是一個叫做Java虛擬機的進程,而不是我們寫的一個個的class文件。這個叫做虛擬機的進程處理一些底層的操作,比如內存的分配和釋放等等。我們編寫的class文件只是虛擬機進程執行時需要的“原料”。這些“原料”在運行時被加載到虛擬機中,被虛擬機解釋執行,以控制虛擬機實現我們java代碼中所定義的一些相對高層的操作,比如創建一個文件等,可以將class文件中的信息看做對虛擬機的控制信息,也就是一種虛擬指令。

2.JVM體系結構

JVM三大子系統

                                                  

類加載器:用於加載.class文件,但並不是在開始運行時加載所有類,而是當程序需要某個類時,纔會加載。

執行引擎:由虛擬機加載的類,被加載到Java虛擬機內存中之後,虛擬機會讀取並執行它裏面存在的字節碼指令。虛擬機中執行字節碼指令的部分叫做執行引擎。就像一個人,不是把飯吃下去就完事了,還要進行消化,執行引擎就相當於人的腸胃系統。在執行的過程中還會把各個class文件動態的連接起來。

垃圾收集子系統:Java虛擬機會進行自動內存管理。具體說來就是自動釋放沒有用的對象,而不需要程序員編寫代碼來釋放分配的內存。這部分工作由垃圾收集子系統負責。

虛擬機內存區

                                           

堆(HEAP):存儲對象和數組,如new Person(),new String[]。是最耗內存的區域,被所有線程共享,又稱GC堆

程序計數器:又稱寄存器,記錄當前線程執行到哪個位置。一個CPU的內核只會執行一條線程中的指令,因此,爲了能夠使得每個線程都在線程切換後能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,否則就會影響到程序的正常執行次序。因此,可以這麼說,程序計數器是每個線程所私有的。不會發生內存泄漏

Java棧(STACK)線程私有,每一個棧幀對應一個被調用的方法,記錄了該方法的

  -- 局部變量:方法中的局部變量,基本類型直接存儲,對象則存儲的引用

  -- 操作數棧:表達式求值

  -- 指向常量池的引用:即調用類中的常量

  -- 方法返回地址:方法執行完後會返回到調用該方法的地址

                                                      

本地方法棧:線程私有,執行JVM中native method

方法區線程共享,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量常量以及編譯器編譯後的代碼等。不同的虛擬機實現方法區的方式也不相同。對於HotPot而言,jdk1.7版本前稱方法區爲永久代(PermGen Space),由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代內存溢出, 拋出“java.lang.OutOfMemoryError: PermGen space “異常,常用-XX:PermSize  -XX:MaxPermSize來增大方法區內存大小避免內存溢出。但jdk1.8之後,用meta space元空間取代了PermGen Space,元空間受限於本地內存,PermGen則受限於虛擬機

常量池:可以簡單理解爲方法區的一部分,包含字面量和符號引用兩大類。作用是存儲共享的常量,這樣就避免了堆上創建重複對象額外的開銷。8大基本類型全存儲在常量池中,但所有封裝類中只有Float和Double不存在常量池中。

字符串對象本身是不可變的(final),但它的引用是可以改變的,所以a="1",a="2",並沒有將1這個對象改成2,而是將a的引用地址從1改爲了2,“1”如果後面沒有用到會被垃圾回收期回收;

final String a="1"  和 String a="1"  含義不一樣,前者引用已經固定死不能修改,後者還能修改引用。所以在編譯期間,String b=a+"3"  前者相當於b="1"+"3"直接在字符串常量池中創建“13”這個字符串,後者還是得在運行期間StringBuffer.append("3")來拼接一個新的字符串對象,存放於堆中。

靜態常量池在編譯期間把所有常量保存起來,運行常量池在運行期間先把靜態常量池裏所有東西拿過來,同時在運行過程中往常量池裏添加新內容,如String.intern()。

2.1 類加載器

類加載器只分兩種,bootstrap加載器和其他加載器

注意:classLoader有屬性 private final ClassLoader parent

自定義的類加載器parent是AppClassLoader,AppClassLoader的parent是ExtClassLoader,但ExtClassLoader是沒有父級的!!!

BootStrapClassLoader是C++編寫的,並不是ExtClassLoader的父級!!!

雙親委派模型(https://blog.csdn.net/u011080472/article/details/51332866

加載器加載文件執行classLoader類中loadClass方法。

1.先判斷是否已加載過該文件    findLoadedClass

2.如果該classLoader有父級,則執行父級的loadClass   super.loadClass

3.如果沒有父級了,則啓用bootstrap加載器作爲父級,執行本地方法   findBootstrapClass

4.如果所有父級都無法加載class文件,則用本classLoader中的findClass方法

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

雙親委派的好處是,例如java.lang.Object類,永遠只會被BootStrap ClassLoader執行,Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程序將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行。


2.2 類初始化

https://www.cnblogs.com/chenyangyao/p/5296807.html

                                                                  

2.3 類加載過程

                               è¿éåå¾çæè¿°

加載(裝載):獲得類的二進制流,存入方法區運行時數據,生成Java.lang.Class作爲各種數據訪問接口

驗證:class甚至可以由自己創建16進制的文件獲得,但要滿足一定條件,如文件格式:開頭幾個字是CA FE BA BY,後面是主版本號次版本號,常量池等等;元數據驗證:類是否有父類,是否繼承了final類,抽象類的實現類是否實現了所有方法;字節碼驗證:類型轉換;符號引用驗證;

準備:爲類變量分配內存並設置類變量初始值,都在方法區內執行。只會分配類變量(static修飾),實例變量是在對象實例化時一起分配在堆中。Private static int value=123    存的是初始值0,value=123是在初始化階段執行的。Private static final int value=123 ,因爲是final修飾,存的是123。

解析:將常量池中的符號引用替換爲直接引用的過程

初始化:<clinit>類構造器執行的方法,即初始化靜態變量和靜態代碼塊(準備階段是設置類變量初始值)。<clinit>方法僅在類有類變量賦值或者靜態代碼塊時才執行,對於類會默認先執行父類中的<clinit>方法,接口裏的變量默認是static final類型,所以不會執行父類的<clinit>。如果多線程同時去初始化一個類,只會有一個執行,其他阻塞,且同一個classLoader只會初始化一次同一個類。

public class DeadLoopClass {

    static{
        if(true){
            System.out.println(Thread.currentThread()+" 單線程初始化DeadLoopClass類,其餘線程等待");
            // while(true)測阻塞,屏蔽掉測classLoader只初始化一次類
            while(true){}
        }
    }
}

public class TestInitClass {
    public static void main(String[] args) {
        Runnable script = new Runnable() {
            @Override
            public void run() {
                // 只會初始化一次DeadLoopClass
                System.out.println("thread start");
                DeadLoopClass dd = new DeadLoopClass();
                System.out.println("初始化完成");
            }
        };
        Thread t1= new Thread(script);
        Thread t2 = new Thread(script);

        t1.start();
        t2.start();

    }
}

3 垃圾收集器

3.1 判斷對象是否已死

引用計數法:引用計數器,每當有一個地方引用到該對象,則+1,引用失效則-1。但無法解決objA.instance=objB,objB.instance=objA這樣的循環引用。

可達性分析:GC ROOTS(棧中引用對象,方法區類靜態屬性引用對象,方法區常量引用對象,native方法引用對象)作爲起始點,向下搜索引用鏈,如果對象和GC ROOTS間沒有引用鏈,則不可達,可以回收。

           

3.2 引用類型

https://blog.csdn.net/u011179993/article/details/54564380

強引用:類似Object A= new Object();

軟引用(SoftReference):SoftRefernce<Object> soft = new SoftReference(new Object()); soft.get()軟引用對象只有在內存不足的情況下會被回收。

/**
 * @Auther: ycig
 * @Date: 2018/11/9 09:16
 * @Description: java -Xms10m -Xmx10m SoftReferenceTest  軟引用只有內存溢出了纔會被回收
 */
public class SoftReferenceTest {

    static class HeapObject {
        byte[] bs = new byte[1024 * 1024];
    }

    public static void main(String[] args) {
        SoftReference<HeapObject> softReference = new SoftReference<>(new HeapObject());

        List<HeapObject> list = new ArrayList<>();

        while (true) {
            if (softReference.get() != null) {
                list.add(new HeapObject());
                System.out.println("list.add");
            } else {
                System.out.println("---------軟引用已被回收---------");
                break;
            }
            System.gc();
        }
    }
}

弱引用():無論內存是否足夠,只要開始gc,就會被清除掉

public class WeakReferenceTest {
    static class TestObject{

    }

    public static void main(String[] args) throws InterruptedException {
        WeakReference<TestObject> weakReference=new WeakReference<>(new TestObject());

        System.out.println(weakReference.get() == null);//false

        System.gc();
        TimeUnit.SECONDS.sleep(1);//暫停一秒鐘

        System.out.println(weakReference.get() == null);//true
    }
}

虛引用(PhantomReference):不能獲得對象實例,也不會對生存時間產生影響。唯一作用就是被收集器回收時能收到一個系統通知。

3.3 兩次執刑

對於可達性分析而言,對象銷燬至少需要經過兩次gc,第一次發現沒有和GC roots的引用鏈,會被標記。如果對象沒有重寫finalize()方法或是finalize()方法已執行了一次,則直接判定死刑。如果對象有重寫finalize()方法,且是第一次執行,則會放入F-QUEUE隊列中,執行一遍finalize(),如果在方法中有與引用鏈中任意對象關聯,則將會逃出死刑。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK=null;

    /**
     * 當虛擬機認爲該對象沒有再被引用,會執行finalize方法
     * 如果沒有重寫該方法或者已經被虛擬機調用過一次,則不會執行該方法
     * 只有第一次gc執行,且重寫了finalize,纔會執行
     * 推薦用try finally方法替代
     * @throws Throwable
     */
    @Override
    public void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize 開始執行");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws  Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        SAVE_HOOK = null;
        // 第一次執行gc,可以拯救回來
        System.gc();

        Thread.sleep(500);

        if(SAVE_HOOK !=null){
            System.out.println("SAVE_HOOK IS ALIVE");
        }else{
            System.out.println("SAVE_HOOK IS DEAD");
        }

        // 加了這句話 SAVE_HOOK = new FinalizeEscapeGC();   第二次執行gc還是會拯救回來
        SAVE_HOOK = null;
        // 第二次執行gc,不會進入finalize()方法,拯救不回來了
        System.gc();

        Thread.sleep(500);

        if(SAVE_HOOK !=null){
            System.out.println("SAVE_HOOK IS ALIVE");
        }else{
            System.out.println("SAVE_HOOK IS DEAD");
        }

    }


}

3.4 垃圾收集算法

標記-清除:效率一般,容易產生空間碎片

複製:內存分爲大小相等兩塊,先用一塊,快用完了就把還存活的對象複製到另一塊,把原先的一塊清除掉。這樣來回,適用於對象存活時間短的地方,如新生代。其實現在是用一個Eden區(80%)和兩個survivor區(10%+10%)來複制對象,每次使用eden區和一塊Survivor0區(90%),用完了後複製存活對象到另一個Survivor1區(10%),清空eden和survivor0,接着再用eden和survivor1進行內存管理,如此反覆。默認只有10%不到的對象存活了下來,如果超過10%,就得向其他區借內存。

標記整理:先標記,後讓存活對象都向一端移動,然後清理掉端邊界以外的內存。

分代收集:當代虛擬機都採用這種方法,根據對象生存週期將內存分爲幾部分。一般把JAVA堆分爲新生代和老生代,新生代採用複製算法,因爲每次回收過程中都有大批對象死去。老年代對象存活率高,採用標記-清除或者標記-整理方法。

現我們知道了不同區域採用不同的收集方法,可達性分析需要找到GC ROOTS和引用鏈,程序一直在併發運行,GC ROOTS也是時刻變換的,只有在某一時刻STOP THE WORLD,才能通過OopMap找到這一時刻的GC ROOTS。STW需要在安全點執行,安全點位置一般在

  • 1、循環的末尾
  • 2、方法臨返回前 / 調用方法的call指令後
  • 3、可能拋異常的位置

3.5 收集器

新生代收集器和老年代收集器搭配方式

                                      

新生代收集器

                                  è¿éåå¾çæè¿°

老年代收集器

serial old   Parallel Old  CMS

CMS:Concurrent Mark Sweep,希望系統停頓時間最短。標記-清除分爲四個步驟 初始標記、併發標記、重新標記、併發清除。

初始標記需要stw,僅僅是標記GC ROOTS直接關聯的對象(藍色部分),因爲有Oop Map的存在,所以該步驟很快。

併發標記不需要stw,從上一步被標記的對象進行可達性分析,組成關係網,與主線程共同執行。因爲是併發的,所以有可能會標記一些本不該回收的對象。

重新標記,再次stw,僅針對上一步標記的對象,所以時間相當短。

併發清除,清除掉那些不在關係網上的對象,無需stw。由於是併發清除,所以可能會在清除過程中又產生些新的垃圾,又稱“浮動垃圾”

優點是停頓時間短,缺點是耗CPU,且標記-清除方法容易產生碎片,導致內存空間不連續。

è¿éåå¾çæè¿°

G1 收集器

回收範圍是整個堆。將整個堆分爲多個region,每個region依然存在年輕代和年老代的概念。每個region都有remembered set,記錄了相關引用信息,垃圾收集掃描就無需全堆掃描了,只用掃描remembered set。年輕代採用複製,年老代採用標記-整理。也分爲四步驟:初始標記、併發標記、最終標記、篩選回收,其中篩選回收指每個region回收的空間各不相同,對各個region的回收價值和成本進行排序,用戶可通過-XX:MaxGCPauseMills=50毫秒設置有限的收集時間。 

è¿éåå¾çæè¿°

4. 內存模型

因爲CPU處理速度特別快,內存跟不上,所以需要告訴緩存來作爲內存和處理器間的緩衝。將運算所需的數據從內存複製到高速緩存區,運算完畢後再同步回內存中。

內存模型中,每一個工作內存對應一個線程,裏面用到的公共數據來源於主內存(局部變量在方法中屬於線程私有)。多個線程共用同一個類變量會引發線程安全問題。

                        

                                               

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章