JVM基礎自用

1)內存區域

  • 程序計數器:可以看作是當前線程所執行的字節碼文件(class)的行號指示器,它會記錄執行痕跡,是每個線程私有的
  • 方法區:主要存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後的代碼等數據,該區域是被線程共享的,很少發生垃圾回收。包括類的靜態變量類初始化代碼定義靜態變量時的賦值語句 和 靜態初始化代碼塊)、實例變量定義實例初始化代碼定義實例變量時的賦值語句實例代碼塊構造方法)和實例方法,還有父類的類信息引用。
  • 棧:棧是運行時創建的,是線程私有的,生命週期與線程相同,存儲聲明的變量
  • 本地方法棧:爲 native 方法服務,native 方法是一種由非 java 語言實現的 java 方法,與 java 環境外交互,如可以用本地方法與操作系統交互
  • 堆:堆是所有線程共享的一塊內存,是在 java 虛擬機啓動時創建的,幾乎所有對象實例都在此創建,所以經常發生垃圾回收操作

2)JVM類加載機制

jvm的類加載機制分爲5個步驟:加載,鏈接(即爲:驗證(校驗),準備,解析),初始化(後面其實還有使用卸載(當使用完成之後,還會在方法區垃圾回收的過程中進行卸載)。)

加載:通過類的權限名獲取此類的二進制字節流;將這個類的字節流的靜態儲存結構轉化爲方法區的數據結構;在堆中生成代表這個類的Java.lang.Class對象,作爲訪問方法區數據結構的入口。字節碼來源:通過編譯,即把我們寫好的java文件,通過javac命令編譯成字節碼,也就是我們常說的.class文件。

驗證(校驗):確保class文件的字節流符合虛擬機的要求,並且不會危害虛擬機。

  1. 文件格式驗證:基於字節流驗證。
  2. 元數據驗證:基於方法區的存儲結構驗證。
  3. 字節碼驗證:基於方法區的存儲結構驗證。
  4. 符號引用驗證:基於方法區的存儲結構驗證。

準備:爲類變量分配內存,並初始化默認值,特例是 static final ,其他的static變量的值將放置在類構造器<clinit>方法之中;

解析:將類型的符號應用轉化爲直接引用

符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。

直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那麼引用的目標一定是已經存在於內存中。

舉個例子:現在調用方法hello(),這個方法的地址是12345678,那麼hello就是符號引用,12345678就是直接引用。

初始化:

初始化階段是執行類構造器<clinit>方法的過程。<clinit>方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合併而成的。虛擬機會保證<clinit>方法執行之前,父類的<clinit>方法已經執行完畢。如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不爲這個類生成<clinit>()方法。

java中,對於初始化階段,有且只有以下五種情況纔會對要求類立刻“初始化”(加載,驗證,準備,自然需要在此之前開始):

  1. 使用new關鍵字實例化對象、訪問或者設置一個類的靜態字段(被final修飾、編譯器優化時已經放入常量池的例外)、調用類方法,都會初始化該靜態字段或者靜態方法所在的類。
  2. 初始化類的時候,如果其父類沒有被初始化過,則要先觸發其父類初始化。
  3. 使用java.lang.reflect包的方法進行反射調用的時候,如果類沒有被初始化,則要先初始化。
  4. 虛擬機啓動時,用戶會先初始化要執行的主類(含有main)
  5. jdk 1.7後,如果java.lang.invoke.MethodHandle的實例最後對應的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,並且這個方法所在類沒有初始化,則先初始化。

3)類加載器

把類加載階段的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作交給虛擬機之外的類加載器來完成。

系統自帶的類加載器分爲三種:

 

1)啓動類加載器:它用來加載 Java 的核心庫,是用原生代碼來實現的

2)擴展類加載器:它用來加載 Java 的擴展庫。

3)應用類加載器:它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類

還可以創建自定義加載器:開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器。

4)雙親委派機制

如果一個類加載器收到類加載的請求,不會馬上對類進行加載,而是將請求委派給父加載器,每層的加載器都會如此,多樣所有的請求都會到達啓動了加載器,並且只有在接收到父類無法加載的反饋信息後纔會自己嘗試加載。

因此Object類在程序的各種類加載器環境中都是同一個類。如果不採用雙親委派機制,重寫一個相同的系統類,雖然可以被編譯,但是永遠無法加載。

使用雙親委託機制的好處是:能夠有效確保一個類的全局唯一性,當程序中出現多個限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類。

5)new對象過程

當虛擬機執行到new指令時,會去方法區常量池中找類的信息,即去定位該類的符號引用,如果找的說明該類已經在方法區內,則繼續執行一下操作,如若沒有則先使用Class Loader對類進行加載。

然後虛擬機開始爲對象分配內存,對象所需要的內存大小在類加載完成後就已經確定了。分配內存又分爲兩種,第一種,內存絕對規整,每次分配內存只需將指針後移相應的距離即可;第二種,空閒內存和非空閒內存夾雜在一起,那麼就需要用一個列表來記錄堆內存的使用情況,然後按需分配內存。java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定.

對於多線程的情況,如何確保一個線程分配了對象內存但尚未修改內存管理指針時,其他線程又分配該塊內存而覆蓋的情況?有一種方法,就是讓每一個線程在堆中先預分配一小塊內存(TLAB本地線程分配緩衝),每個線程只在自己的內存中分配內存。但對象本身按其訪問屬性是可以線程共享訪問的。

內存分配到後,虛擬機將分配的內存空間都初始化爲零值(不包括對象頭)。所以實例變量不賦初值也能使用。

如果new的對象是局部變量,new的對象變量在棧幀的局部變量表,這個對象的引用就放在棧幀。

如果new的對象是實例變量,new的對象變量在堆中,對象的引用就放在堆。

如果new的對象是靜態變量,new的對象變量在方法區,對象的引用就放在方法區。

6)堆的新生代,老年代

新生代分爲一個Eden區和兩個Survivor(比例8:1:1,可修改,而往往老年代的內存比都會遠大於新生代的內存比),通常對象主要分配到新生代,少數情況下也可能會直接分配在老年代中(如大對象的分配)。jvm每次使用Eden區和一個Survivor(我們稱爲From區),在經過一次Minor Gc,會將Eden區,和From還存活的對象複製到另外一個Survivor區(我們稱爲To區)(特別的因爲新生代往往朝生暮死,存活的時間短,所以我們只需要花費很少的開銷去對對象進行復制,及採用複製算法),最後交換From和To的身份。此時由Eden區複製過來的對象年齡爲1,From來的年齡加1;以後這些對象每在Survivor區熬過一次GC,它們的年齡就加1,當對象年齡達到某個年齡(默認值爲15)時,就會把它們移到老年代中。

在Minor Gc中有可能複製到To區的對象在To區無法放下,則這些對象將直接通過分配擔保機制進入老年代;

年老代裏存放的都是存活時間較久的,大小較大的對象,因此年老代使用標記整理算法。當年老代容量滿的時候,會觸發一次Major GC(full GC),回收年老代和年輕代中不再被使用的對象資源。

總結:

1、Minor GC是發生在新生代中的垃圾收集,採用的複製算法;

2、新生代中每次使用的空間不超過90%,主要用來存放新生的對象;

3、Minor GC每次收集後Eden區和一塊Survivor區都被清空;

4、老年代中使用Full GC,採用的標記-清除算法。

永久代(方法區)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

判斷“無用的類”:

該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。

加載該類的ClassLoader已經被回收。

該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

7)Minor GC,Major Gc,Full Gc

Minor Gc:從年輕代回收內存

1.Eden區內存滿

2、新創建的對象大小 > Eden所剩空間

Full GC:清理整個堆空間,包括年輕代和老年代(Major GC通常是跟full GC是等價的)

指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 Parallel Scavenge 收集器的收集策略裏就有直接進行 Major GC 的策略選擇過程)。

其實只有CMS的concurrent collectio會只對老年代進行GC其它能收集old gen的GC都會同時收集整個GC堆。

1.System.gc()

2.每次晉升到老年代的對象平均大小>老年代剩餘空間,即在準備觸發Minor GC,如果發現統計數據說之前的Major GC的平均晉升到老年代的大小比目前老年代的剩餘內存大,則不會觸發Major,而是轉而觸發Full GC。

Parallel Scavenge(-XX:+UseParallelGC)框架下,默認是在要觸發full GC前先執行一次young GC,並且兩次GC之間能讓應用程序稍微運行一小下,以期降低full GC的暫停時間(因爲young GC會盡量清理了young gen的死對象,減少了full GC的工作量)。

3.MinorGC後存活的對象超過了老年代剩餘空間

4.堆內存分配很大的對象

8)垃圾收集器(不全,待補)

新生代垃圾收集器

1.servial(串行,響應速度優先,Stop The World

它採用的是複製算法,它在進行垃圾收集的時候必須停止其他所有工作線程,知道垃圾收集完成(“Stop The World”)。

2.ParNew(並行,響應速度優先,Stop The World)

servial的多線程版本。

3.Parallel Scavenge(並行,吞吐量優先,)

同樣是複製算法,多線程,Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput)。

老年代垃圾收集器

1.Servial Old(串行,響應速度優先,Stop The World)

Servial 的老年代版本,但是使用的是標記整理算法

2.Parallel Old(併發,即和用戶線程一起執行,

Parallel Old是Parallel Scavenge的老年代版本,使用的是標記整理算法

3.CMS(併發)

Concurrent Mark Sweep是一種以最短回收停頓時間爲目標的垃圾收集器,它非常符合那些集中在互聯網站或者B/S系統的服務端上的Java應用,這些應用都非常重視服務的響應速度。Mark Sweep說明它使用的是標記清除算法

G1垃圾收集器

橫跨整個堆內存面向服務端的垃圾收集器。通過把堆內存分爲大小相等的多個區域(2的冪次方),回收時計算出每個區域回收所獲得的空間以及所需時間的經驗值,根據記錄兩個值來判斷哪個區域最具有回收價值,所以叫Garbage First(垃圾優先)。

9)類成員的初始化

靜態域:靜態代碼塊、靜態成員變量 非靜態域:非靜態代碼塊、非靜態成員變量 (成員方法不包含在裏面,因爲方法只能講加載而非初始化)

1.沒有繼承父類的情況:

  初始化順序爲:靜態域 -> 非靜態域 -> 構造函數 (左邊優於右邊)。

2.繼承父類的情況:

  初始化順序爲:父類靜態域->子類靜態域->父類非靜態域->父類構造函數->子類非靜態域->子類構造函數(左邊優於右邊)。

10)類初始化觸發

主動引用

1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

2.用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要觸發初始化操作。

3.初始化一個類的時候,發現其父類還有進行過初始化,則需要觸發先其父類的初始化操作。

注意這裏和接口的初始化有點區別,,一個接口在初始化時,並不要求其父接口全部都完成了初始化,只要在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。

4.虛擬機啓動時,需要指定一個執行的主類(包含main方法的類),虛擬機會先初始化這類。

5.用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化操作。

被動引用

1.對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。(即子類直接引用從父類繼承而來的靜態變量)

2.通過數組定義來引用類,不會觸發此類的初始化

3.

    public class ConstClass {
        static {
            System.out.println("ConstClass init....");
        }
 
        public static final String MM = "hello Franco";
    }
 
    package com.xdwang.demo;
 
    public class Test3 {
        public static void main(String[] args) {
            System.out.println(ConstClass.MM);
        }
    }

運行結果:

hello Franco

並沒有ConstClass init….,這是因爲雖然Test3裏引用了ConstClass類中的常量,但其實在編譯階段通過常量傳播優化,已經將此常量存儲到Test3類的常量池中。兩個類在編譯成class之後就不存在任何聯繫了。

11)JMM(https://www.cnblogs.com/null-qige/p/9481900.html

12)可以作爲GCRoot的對象

1.虛擬機棧(棧幀的本地變量表)中應用的對象

2.方法區中靜態屬性引用的對象

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

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

當一個對象對於GCRoots不可達時,併發直接被垃圾回收。會先執行該對象的fnalize()方法,此時該對象有一次自救機會,將自己關聯到GCRoot上。如果對象在finalize()方法中將自己關聯到GCRoots上,該對象將不會被垃圾回收器回收。但是虛擬機並不保finalize()執行完畢之後才進行垃圾回收,因此finalize()方法並不能一定自救成功。並且如果一個對象被自救過一次之後,仍舊脫離GCRoot,第二次將不再執行finalize()方法。finalize()方法運行代價高昂,不穩定性高,只是JAVA誕生之初爲了讓C/C++程序員接受而做出的一種妥協,有些說法說finalize()可以用來關閉外部資源,但是try{}finally{}可以執行得更好,JAVA程序員完全可以無視finalize()的用法。

13)內存溢出 

1.內存溢出和內存泄漏的區別

內存溢出 OutOfMemory,指程序在申請內存時,沒有足夠的內存空間供其使用。

內存泄露 Memory Leak,指程序在申請內存後,無法釋放已申請的內存空間,內存泄漏最終將導致內存溢出。

2.堆溢出的原因?

堆用於存儲對象實例,只要不斷創建對象並保證 GC Roots 到對象有可達路徑避免垃圾回收,隨着對象數量的增加,總容量觸及最大堆容量後就會 OOM,例如在 while 死循環中一直 new 創建實例。

堆 OOM 是實際應用中最常見的 OOM,處理方法是通過內存映像分析工具對 Dump 出的堆轉儲快照分析,確認內存中導致 OOM 的對象是否必要,分清到底是內存泄漏還是內存溢出。

如果是內存泄漏,通過工具查看泄漏對象到 GC Roots 的引用鏈,找到泄露對象是通過怎樣的引用路徑、與哪些 GC Roots 關聯才導致無法回收,一般可以準確定位到產生內存泄漏代碼的具***置。

如果不是內存泄漏,即內存中對象都必須存活,應當檢查 JVM 堆參數,與機器內存相比是否還有向上調整的空間。再從代碼檢查是否存在某些對象生命週期過長、持有狀態時間過長、存儲結構設計不合理等情況,儘量減少程序運行期的內存消耗。

3.棧溢出的原因?

由於 HotSpot 不區分虛擬機和本地方法棧,設置本地方法棧大小的參數沒有意義,棧容量只能由 -Xss 參數來設定,存在兩種異常:

StackOverflowError: 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError,例如一個遞歸方法不斷調用自己。該異常有明確錯誤堆棧可供分析,容易定位到問題所在。

OutOfMemoryError: 如果 JVM 棧可以動態擴展,當擴展無法申請到足夠內存時會拋出 OutOfMemoryError。HotSpot 不支持虛擬機棧擴展,所以除非在創建線程申請內存時就因無法獲得足夠內存而出現 OOM,否則在線程運行時是不會因爲擴展而導致溢出的。

4.運行時常量池溢出的原因?

String 的 intern 方法是一個本地方法,作用是如果字符串常量池中已包含一個等於此 String 對象的字符串,則返回池中這個字符串的 String 對象的引用,否則將此 String 對象包含的字符串添加到常量池並返回此 String 對象的引用。

在 JDK6 及之前常量池分配在永久代,因此可以通過 -XX:PermSize-XX:MaxPermSize 限制永久代大小,間接限制常量池。在 while 死循環中調用 intern 方法導致運行時常量池溢出。在 JDK7 後不會出現該問題,因爲存放在永久代的字符串常量池已經被移至堆中。

5.方法區溢出的原因?

方法區主要存放類型信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。只要不斷在運行時產生大量類,方法區就會溢出。例如使用 JDK 反射或 CGLib 直接操作字節碼在運行時生成大量的類。很多框架如 Spring、Hibernate 等對類增強時都會使用 CGLib 這類字節碼技術,增強的類越多就需要越大的方法區保證動態生成的新類型可以載入內存,也就更容易導致方法區溢出。

JDK8 使用元空間取代永久代,HotSpot 提供了一些參數作爲元空間防禦措施,例如 -XX:MetaspaceSize 指定元空間初始大小,達到該值會觸發 GC 進行類型卸載,同時收集器會對該值進行調整,如果釋放大量空間就適當降低該值,如果釋放很少空間就適當提高。

15)Java 的引用有哪些類型?

JDK1.2 後對引用進行了擴充,按強度分爲四種:

強引用: 最常見的引用,例如 Object obj = new Object() 就屬於強引用。只要對象有強引用指向且 GC Roots 可達,在內存回收時即使瀕臨內存耗盡也不會被回收。

軟引用: 弱於強引用,描述非必需對象。在系統將發生內存溢出前,會把軟引用關聯的對象加入回收範圍以獲得更多內存空間。用來緩存服務器中間計算結果及不需要實時保存的用戶行爲等。

弱引用: 弱於軟引用,描述非必需對象。弱引用關聯的對象只能生存到下次 YGC 前,當垃圾收集器開始工作時無論當前內存是否足夠都會回收只被弱引用關聯的對象。由於 YGC 具有不確定性,因此弱引用何時被回收也不確定。

虛引用: 最弱的引用,定義完成後無法通過該引用獲取對象。唯一目的就是爲了能在對象被回收時收到一個系統通知。虛引用必須與引用隊列聯合使用,垃圾回收時如果出現虛引用,就會在回收對象前把這個虛引用加入引用隊列。

 

暫時這麼多,刷題去

請實現兩個函數,分別用來序列化和反序列化二叉樹

 

二叉樹的序列化是指:把一棵二叉樹按照某種遍歷方式的結果以某種格式保存爲字符串,從而使得內存中建立起來的二叉樹可以持久保存。序列化可以基於先序、中序、後序、層序的二叉樹遍歷方式來進行修改,序列化的結果是一個字符串,序列化時通過 某種符號表示空節點(#),以 ! 表示一個結點值的結束(value!)。

二叉樹的反序列化是指:根據某種遍歷順序得到的序列化字符串結果str,重構二叉樹。
 

例如,我們可以把一個只有根節點爲1的二叉樹序列化爲"1,",然後通過自己的函數來解析回這個二叉樹

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    //牛客的千萬別加static,牛客的java並不和c/c++一樣,類只會加載一次,所有在多樣例下加了出錯
   int num = 0;
    String Serialize(TreeNode root) {
        if (root == null) {
            return "#";
        } else {
            return root.val + "," + Serialize(root.left) + "," + Serialize(root.right);
        }
    }
    TreeNode Deserialize(String str) {
        if(num >= str.length() || '#' == str.charAt(num)) {
            num+=2;
            return null;
        }
        int sum =0;
        while(num < str.length() && str.charAt(num) != ','){
            sum = sum * 10 + (str.charAt(num) - '0');
            num++;
        }
        num++;
        TreeNode tree = new TreeNode(sum);
        tree.left = Deserialize(str);
        tree.right = Deserialize(str);
        return tree;
    }
}

 

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