1. 面試必問的 JVM 運行時數(shù)據(jù)區(qū),你懂了嗎?

        共 5850字,需瀏覽 12分鐘

         ·

        2021-07-18 19:22


         前言

        Java 虛擬機的運行時數(shù)據(jù)區(qū)經(jīng)常在面試中被拿來提問,很多概念在市面上有各種各樣的說法,搞的不少同學應該是懵逼的。

        當我們陷入不知道哪個說法是正確的情況時,最好的參考就是源碼和規(guī)范。

        在面試中,當面試官反問你:為什么某某是這樣?的時候,如果你回答:因為規(guī)范是這么寫的、因為源碼是這么寫的。

        這個回答是非常有說服力的。

        因此,本文在描述一些有爭議的問題上,優(yōu)先以《Java 虛擬機規(guī)范》的說法為準。


        正文

        1、運行時數(shù)據(jù)區(qū)(Run-Time Data Areas)

        Java 虛擬機定義了若干種在程序執(zhí)行期間會使用到的運行時數(shù)據(jù)區(qū)域。

        其中一些數(shù)據(jù)區(qū)域在 Java 虛擬機啟動時被創(chuàng)建,隨著虛擬機退出而銷毀。也就是線程間共享的區(qū)域:堆、方法區(qū)、運行時常量池。

        另外一些數(shù)據(jù)區(qū)域是按線程劃分的,這些數(shù)據(jù)區(qū)域在線程創(chuàng)建時創(chuàng)建,在線程退出時銷毀。也就是線程間隔離的區(qū)域:程序計數(shù)器、Java虛擬機棧、本地方法棧。


        1)程序計數(shù)器(Program Counter Register)

        Java 虛擬機可以支持多個線程同時執(zhí)行,每個線程都有自己的程序計數(shù)器。在任何時刻,每個線程都只會執(zhí)行一個方法的代碼,這個方法稱為該線程的當前方法(current method)。

        如果線程正在執(zhí)行的是 Java 方法(不是 native 的),則程序計數(shù)器記錄的是正在執(zhí)行的 Java 虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的是本地(native)方法,那么計數(shù)器的值是空的(undefined)。


        2)Java虛擬機棧(Java Virtual Machine Stacks)

        每個 Java 虛擬機線程都有自己私有的 Java 虛擬機棧,它與線程同時創(chuàng)建,用于存儲棧幀。

        Java 虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。

        每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。


        3)本地方法棧(Native Method Stacks)

        本地方法棧與 Java 虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是 Java 虛擬機棧為虛擬機執(zhí)行 Java方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的本地(Native)方法服務。


        4)堆(Heap)

        堆是被各個線程共享的運行時內(nèi)存區(qū)域,也是供所有類實例和數(shù)組對象分配內(nèi)存的區(qū)域。

        堆在虛擬機啟動時創(chuàng)建,堆存儲的對象不會被顯示釋放,而是由垃圾收集器進行統(tǒng)一管理和回收。


        5)方法區(qū)(Method Area)

        方法區(qū)是被各個線程共享的運行時內(nèi)存區(qū)域。方法區(qū)類似于傳統(tǒng)語言的編譯代碼的存儲區(qū)。它存儲了每一個類的結構信息,例如:運行時常量池、字段和方法數(shù)據(jù),構造函數(shù)和普通方法的字節(jié)碼內(nèi)容,還包括一些用于類、實例、接口初始化用到的特殊方法。


        6)運行時常量池(Run-Time Constant Pool)

        運行時常量池是 class 文件中每一個類或接口的常量池表(constant_pool table)的運行時表示形式。

        它包含了若干種常量,從編譯時已知的數(shù)值字面量到必須在運行時解析后才能獲得的方法和字段引用。運行時常量池的功能類似于傳統(tǒng)編程語言的符號表(symbol table),不過它包含的數(shù)據(jù)范圍比通常意義上的符號表要更為廣泛。


        2、Java 中有哪幾種常量池?

        現(xiàn)在我們經(jīng)常提到的常量池主要有三種:class 文件常量池、運行時常量池、字符串常量池。


        3、class 文件常量池

        class 文件常量池(class constant pool)屬于 class 文件的其中一項,class 類文件包含:魔數(shù)、類的版本、常量池、訪問標志、字段表集合、方發(fā)表等信息。

        常量池用于存放編譯期間生成的各種字面量(Literal)和符號引用(Symbolic References)。

        字面量比較接近于Java語言層面的常量概念,如文本字符串、聲明為 final 的常量值等。

        符號引用則屬于編譯原理方面的概念。符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區(qū)分,直接引用一般是指向方法區(qū)的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。符號引用主要包括下面幾類常量:

        • 被模塊導出或開放的包(Package)

        • 類和接口的全限定名(Fully Qualified Name)

        • 字段的名稱和描述符(Descriptor)

        常量池中每一項常量都是一個表,截至JDK 13,常量表中分別有17種不同類型的常量。17種常量類型所代表的具體含義如圖所示。


        關于 class 文件常量池的更多內(nèi)容可以閱讀周志明的《深入理解Java虛擬機》6.3.2 章節(jié)。


        4、運行時常量池

        class 文件常量池是在類被編譯成 class 文件時生成的。而當類被加載到內(nèi)存中后,JVM 就會將 class 文件常量池中的內(nèi)容存放到運行時常量池中。

        Java 虛擬機規(guī)范中對運行時常量池的定義如下:

        A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file.

        運行時常量池是 class 文件中每一個類或接口的常量池表(constant_pool table)的運行時表示形式。

        因此,根據(jù)規(guī)范定義,可以說運行時常量池是 class 文件常量池的運行時表示,每個類在運行時都有自己的一個獨立的運行時常量池。


        5、字符串常量池

        簡單來說,HotSpot VM 里的字符串常量池(StringTable)是個哈希表,全局只有一份,被所有的類共享。

        StringTable 具體存儲的是 String 對象的引用,而不是 String 對象實例自身。String 對象實例在 JDK 6 及之前是在永久代里,從JDK 7 開始放在堆里。

        根據(jù) Java 虛擬機規(guī)范的定義,堆是存儲 Java 對象的地方,其他地方是不會有 Java 對象實體的,如果有的話,根據(jù)規(guī)范定義,這些地方也要算堆的一部分。


        6、字符串常量池是否屬于方法區(qū)?

        我認為是不屬于的。

        在讀本文之前,我相信很多同學會有如下觀點:因為運行時常量池屬于方法區(qū),所以很多同學認為字符串常量池也應該屬于方法區(qū)。

        但是相信看了上面的內(nèi)容后,會開始意識到,運行時常量池和字符串常量池其實是不同的兩個東西,當然它們在字符串解析時會有關聯(lián)。

        Java 虛擬機規(guī)范中對方法區(qū)的定義如下:

        The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization

        在 Java 虛擬機中,方法區(qū)是被各個線程共享的運行時內(nèi)存區(qū)域。方法區(qū)類似于傳統(tǒng)語言的編譯代碼的存儲區(qū),或者類似于操作系統(tǒng)進程中的文本段。它存儲了每一個類的結構信息,例如:運行時常量池、字段和方法數(shù)據(jù),構造函數(shù)和普通方法的字節(jié)碼內(nèi)容,還包括一些用于類、實例、接口初始化用到的特殊方法。

        這邊的關鍵在于 “它存儲了每一個類的結構信息”,而字符串常量池并不屬于某個類,字符串常量是全局共享的,因此,根據(jù)規(guī)范定義,我們可以說字符串常量池不屬于方法區(qū)。

        那字符串常量池(StringTable)究竟存在哪里了? 

        StringTable 本體是存儲在 native memory(本地內(nèi)存)里,不是在永久代里,不是在方法區(qū)里,當然,更不是在堆里。


        7、運行時常量池和字符串常量池的關聯(lián)? 

        上面說了,運行時常量池和字符串常量池在字符串解析時會有關聯(lián),具體如下。

        類的運行時常量池中有 CONSTANT_String_info(見題3表格)類型的常量,CONSTANT_String_info 類型的常量的解析(resolve)過程如下:

        首先到字符串常量池(StringTable)中查找是否已經(jīng)有了該字符串的引用,如果有,則直接返回字符串常量池的引用;如果沒有,則在堆中創(chuàng)建 String 對象,并在字符串常量池駐留其引用,然后返回該引用。

        也就說,運行時常量池里的 CONSTANT_String_info 類型的常量,經(jīng)過解析(resolve)之后,同樣存的是字符串的引用,并且和 StringTable 駐留的引用的是一致的。


        8、String#intern 方法

        在 JDK 7 及之后的版本中,該方法的作用如下:如果字符串常量池中已經(jīng)有這個字符串,則直接返回常量池中的引用;如果沒有,則將這個字符串的引用保存一份到字符串常量池,然后返回這個引用。

        下面的例子可以進行簡單的驗證:

        public static void main(String args[]) {
        // 創(chuàng)建2個對象,str持有的是new創(chuàng)建的對象引用 // 1)駐留(intern)在字符串常量池中的對象 // 2)new創(chuàng)建的對象 String str = new String("joonwhee"); // 字符串常量池中已經(jīng)有了,返回字符串常量池中的引用 String str2 = "joonwhee"; // false,str為new創(chuàng)建的對象引用,str2為字符創(chuàng)常量池中的引用 System.out.println(str == str2); // str修改為字符串常量池的引用,所以下面為true str = str.intern(); // true System.out.println(str == str2);}


        9、永久代(PermGen)

        永久代在 Java 8 被移除。根據(jù)官方提案的描述,移除的主要動機是:要將 JRockit 和 Hotspot 進行融合,而 JRockit 并沒有永久代。

        而據(jù)我們所了解的,還有另外一個重要原因是永久代本身也存在較多的問題,經(jīng)常出現(xiàn)OOM,還出過不少bug。

        根據(jù)官方提案的描述,永久代主要存儲了三種數(shù)據(jù): 

        1)Class metadata(類元數(shù)據(jù)),也就是方法區(qū)中包含的數(shù)據(jù),除了編譯生成的字節(jié)碼被放在 native memory(本地內(nèi)存)。

        2)interned Strings,也就是字符串常量池中駐留引用的字符串對象,字符串常量池只駐留引用,而實際對象是在永久代中。

        3)class static variables,類靜態(tài)變量。

        移除永久代后,interned Strings 和 class static variables 被移動了堆中,Class metadata 被移動到了后來的元空間。


        10、永久代和方法區(qū)的關系?

        方法區(qū)是 Java 虛擬機規(guī)范中定義的一種邏輯概念,而永久代是對方法區(qū)的實現(xiàn)。但是永久代并不等同于方法區(qū),方法區(qū)也不等同于永久代。

        永久代中的 interned Strings 并不屬于方法區(qū),按規(guī)范:堆是存儲 Java 對象的地方 ,這部分應該屬于堆,因此永久代并不是只用于實現(xiàn)方法區(qū)。

        方法區(qū)中 JIT 編譯生成的代碼并不是存放在永久代,而是在 native memory 中,因此可以說方法區(qū)也并不只是由永久代來實現(xiàn)。


        11、元空間(metaspace)

        元空間在 Java 8 移除永久代后被引入,用來代替永久代,本質(zhì)和永久代類似,都是對方法區(qū)的實現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存(native memory)。

        元空間主要用于存儲 Class metadata(類元數(shù)據(jù)),根據(jù)其命名其實也看得出來。

        可以通過 -XX:MaxMetaspaceSize 參數(shù)來限制元空間的大小,如果沒有設置該參數(shù),則元空間默認限制為機器內(nèi)存。


        12、為什么引入元空間?

        在 Java 8 之前,Java 虛擬機使用永久代來存放類元信息,通過-XX:PermSize、-XX:MaxPermSize 來控制這塊內(nèi)存的大小,隨著動態(tài)類加載的情況越來越多,這塊內(nèi)存變得不太可控,到底設置多大合適是每個開發(fā)者要考慮的問題。

        如果設置小了,容易出現(xiàn)內(nèi)存溢出;如果設置大了,又有點浪費,盡管不會實質(zhì)分配這么大的物理內(nèi)存。

        而元空間可以較好的解決內(nèi)存設置多大的問題:當我們沒有指定 -XX:MaxMetaspaceSize 時,元空間可以動態(tài)的調(diào)整使用的內(nèi)存大小,以容納不斷增加的類。


        13、元空間能徹底解決內(nèi)存溢出(Out Of Memory)問題嗎?

        很遺憾,答案是不行的。

        元空間無法徹底解決內(nèi)存溢出的問題,只能說是有所緩解。當內(nèi)存使用完畢后,元空間一樣會出現(xiàn)內(nèi)存溢出的情況,最典型的場景就是出現(xiàn)了內(nèi)存泄漏時。


        最后

        我是囧輝,一個堅持分享原創(chuàng)技術干貨的程序員,我的目標是幫助大家拿到心儀的大廠 Offer。

        瀏覽 38
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 青娱乐在线免费观看 | 免费专区 - 色哟哟 | 区二区三区毛片乱码免费 | 黄色影像影片. | 俺去啦国产欧美 |