這一定是全網寫JVM最好的文章之一—JVM運行時數據區

2020-12-24 一枚勤懇的程式設計師

JVM基礎知識

一個Java程序到底是如何運行的?

一個Java程序,首先要經過javac編譯成.class文件,.class文件是給JVM進行識別的,JVM將.class文件加載到方法區,執行引擎會執行這些字節碼,執行時,會翻譯成作業系統相關的函數。

過程如下:Java文件->編譯器->字節碼->JVM->機器碼

JVM,JRE,JDK的關係

首先看一下這個圖,最表象的,就是JDK>JRE>JVM,也就是JDK包含JRE,JRE包含JVM,那麼這三個到底有什麼區別呢?先從最小的開始說

JVM:JVM具體可以理解成就是一個平臺,一個虛擬機,可以把class翻譯成機器識別的代碼。但是需要注意,JVM不會自己生成代碼,需要大家編寫代碼,同時需要很多依賴庫,這個就需要用到JRE

JRE:JRE除了包含JVM之外,還提供了很多的類庫,也就說我們說的jar包(比如:讀取和操作文件,連接網絡,IO等等),這些東西都是JRE提供的基礎類庫。JVM標準加上實現了一大堆類庫,就組成了Java的運行時環境,也就是我們常說的JRE

JDK:玩過Java的小夥伴應該都用過java -jar,javac等命令吧,如果只有jvm,jre,我們代碼是寫完了,但是怎麼編譯呢?或者代碼出了問題怎麼調試呢?這些都是JDK提供的,所以,jdk其實就是給我們提供了一些工具,一些命令,讓我們完成編譯代碼,調試代碼,反編譯代碼等操作。

為什麼說Java是一次編譯到處運行

不管是windows,mac,還是linux,unix,oracle官網上都提供了下載對應的jdk版本,我們只需要編寫java代碼,因為jvm不同作業系統上下載的不同版本的,所以不需要我們管各種作業系統之間的區別,jvm只識別字節碼,所以jvm其實跟語言是解耦的,也沒有直接關聯。

Java內存區域

根據上圖可以知道,方法區和堆是一個顏色,虛擬機棧和本地方法棧和程序計數器是一個顏色 所以,方法區和堆是線程共享的,虛擬機棧和本地方法棧和程序計數器是線程私有的

通過上圖,可知Java內存區域包括運行時數據區和執行引擎和本地庫接口和本地方法庫

運行時數據區

運行時數據區包括:方法區,虛擬機棧,本地方法棧,堆和程序計數器

虛擬機棧

虛擬機棧是線程私有的,他的生命周期與線程是一樣的。虛擬機棧描述的是Java方法執行的線程內存模型:每個方法執行的時候都會創建一個棧楨用於存放局部變量表,操作數棧,動態連結,方法出口等信息。

看這個方法,main方法中調用a方法,a方法調用b方法,以此調用。這時候我們運行代碼,線程就會對應有一個虛擬機棧,同時,執行某個方法的時候,就會產生一個棧楨

首先是執行main方法,就會有一個棧楨,main調用a方法,就會有一個a方法對應的棧楨,以此類推,但是,執行完的時候,是後進先出,棧的數據結構就是這樣,最後是調用的c方法,c方法執行完畢之後,c的棧楨會先出去,然後繼續b出棧,以此類推

什麼是棧楨

棧楨是用於支持虛擬機進行方法調用和方法執行的數據結構,棧楨存儲了方法的局部變量表,操作數棧,動態連結和方法返回地址等信息。每一個方法從調用到執行完成的過程,都對應一個棧楨在虛擬機裡從入棧到出站的過程。

局部變量表

局部變量表是一組變量值存儲空間,用於存放方法參數和方法內定義的局部變量。包括八種基本數據類型,對象引用(reference類型)和returnAddress類型(指向一條字節碼指令的地址)。

其中64位長度的long和double類型的數據會佔用2個局部變量空間,其餘的數據類型只佔用1個。

操作數棧

操作數棧也稱為操作棧,是一個後入先出棧(LIFO)。隨著方法執行和字節碼指令的執行,會從局部變量表或對象實例的欄位中複製或變量寫入到操作數棧,再隨著計算進行將棧中的元素出棧到局部變量表或者返回給方法調用者,也就是出棧/入棧操作。

這裡說明一下,操作數棧,實際上就是緩存,在計算機中,內存是用來存放數據的,cpu是用來計算的,但是在,cpu和內存之間,作業系統增加了一個三級緩存,用來解決,內存和cpu之間速度差距太大,這裡操作數棧就相當於是三級緩存,堆和棧(局部變量表)相當於內存,執行引擎相當於cpu

這裡有個類:

public class Person { public int work(){ int x = 1; int y = 2; int z = (x+y)*10; return z; } public static void main(String[] args){ Person person = new Person(); person.work(); }}複製代碼這裡,我們可以用javap -c xxx.class命令來進行反彙編

javap -c Person.classCompiled from "Person.java"public class com.zzz.Person { public com.zzz.Person(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int work(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn public static void main(java.lang.String[]); Code: 0: new #2 // class com/zzz/Person 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method work:()I 12: pop 13: return}複製代碼這裡顯示的東西就有點像類了,這裡我就不用javap命令了,安裝一個idea插件jclass Bytecode Viewer 然後單擊class文件,點view中的show bytecode with jclasslib

點擊方法裡面的work的code就可以看到和上面work裡面一樣的信息了

這裡執行work方法的時候,work方法壓入棧,開始執行第一行代碼int i=1;這段代碼在反編譯中對應著兩行,

,這個插件很人性的就是,可以點擊這個iconst之類的,就會跳轉到官網對應的說明

這裡官網給的解釋,將int常數i壓入操作數棧中。

接著執行istroe_1 再來看一下官網這個意思

這裡說明的意思是,將int類型的i從操作數棧彈出,存放在i處的局部變量表中

這裡大家也發現了把,操作數棧其實真的很像cpu上的三級緩存,局部變量表相當於內存

之後iconst_2和istore_2和之前對應的也一樣,壓入操作數棧,從操作數棧彈出,存放到局部變量表

再往後就是執行iload_1和iload_2

這裡的意思是將局部變量表中i的位置的int類型的數據壓入到操作數棧中,所以這裡就是把1,2位置的數據壓入操作數棧

之後執行iadd

iadd的意思是把1,2這兩個數從操作數棧彈出,進行相加,結果是3,再把結果進行入棧

這裡,執行引擎運算完畢之後,需要入棧,因為執行引擎類似於cpu是不用來存儲數據的,所以需要操作數棧來保存數據

之後執行bipush 10

這裡意思是將這個value=10的int類型的數壓入到操作數棧中

之後執行imul

從操作數棧彈出兩個值,然後進行乘法運算,之後把結果壓入操作數棧

之後執行istore_3,就是把30出棧,放入局部變量表3的位置

之後執行iload_3,上面說了,也就是把局部變量表3的位置壓入操作數棧,也就是30

之後執行ireturn

因為我們方法需要返回的,看這裡解釋也可以解釋的通為什麼需要iload了,這個ireturn是需要從操作數棧的棧頂進行返回,所以需要把30壓入操作數棧的棧頂。把30從操作數棧彈出,將它壓入調用程序框架的操作數棧,當前方法的操作數棧中的其他值都丟棄

動態連接

Java虛擬機中,每個棧楨都包含一個指向運行時常量池中該棧所屬的方法的符號引用,持有這個引用的目的是為了支持方法調用過程中的動態連接

動態連接的作用:將符號引用轉換成直接引用(這裡後面會講到)複製代碼動態連接後面會講

方法返回地址

方法返回地址存放調用該方法的PC寄存器的值。一個方法的結束,有兩種方法:正常的執行完成,出現未處理的異常非正常的退出。無論通過哪種方法退出,在方法退出後都返回該方法被調用的位置。方法正常退出時,調用者的PC計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧楨中一般不會保存這部分信息。

本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機棧是虛擬機執行Java方法,而本地方法棧則是虛擬機使用本地方法(也就是Native方法)服務。

特點:

本地方法棧加載native方法,native的存在是用來補全Java中缺陷(早期Java不完善,有些地方需要調用C++)虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則是虛擬機使用native方法服務。是線程私有的,它的生命周期與線程相同,每個線程都有一個。在Java虛擬機規範中,對本地方法棧這塊區域,與Java虛擬機棧一樣,規定了兩種類型的異常:1. StackOverFlowError:線程請求的棧深度>所允許的深度2. OutOfMemoryError:本地方法棧擴展時無法申請到足夠的內存複製代碼為什麼一定需要本地方法棧

因為虛擬機中,虛擬機棧是用來處理內部的一些操作,但是調用native方法和java中方法是不一樣的,所以為了規範,就出現了本地方法棧

PC程序計數器

程序計數器也叫PC寄存器,是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型中,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個計數器來完成。

為什麼要有程序計數器

作業系統中,CPU的邏輯處理器的核心數是有限的。為什麼可以同時跑100個線程呢? CPU用了一種優化手段叫做時間片

什麼是CPU時間片輪轉

時間片輪轉法(Round-Robin,RR)主要用於分時系統中的進程調度。為了實現輪轉調度,系統把所有就緒進程按先入先出的原則排成一個隊列。新來的進程加到就緒隊列末尾。每當執行進程調度時,進程調度程序總是選出就緒隊列的隊首進程,讓它在CPU上運行一個時間片的時間。時間片是一個小的時間單位,通常為10-100ms數量級。當進程用完分給他們的時間片後,系統的計時器發出時鐘中斷,調度程序便停止該進程的運行,把它放入就緒隊列的末尾,然後把cpu分給就緒隊列的隊首進程,同樣也讓他運行一個時間片,如此往復

通俗點來說,就是,假如你上班摸魚,不好好寫碼,你又打遊戲又逛淘寶的,假如你淘寶相中一個東西,遊戲是打吃雞,老闆過來了,你淘寶和遊戲就逛不了了,趕緊切屏到idea中敲代碼,老闆走了,你打開遊戲,找個草叢趴著確保不會有人看到你打死你,然後切到淘寶聊天頁跟賣家諮詢商品,然後老闆又過來了,你又切屏到idea了,老闆走了,你又切到淘寶或者吃雞了,如此反覆,老闆固定時間路過,遊戲,寫碼,逛淘寶,時間是有限的,需要分配

所以在JVM中,就需要這麼一個程序計數器,用來記錄代碼執行到哪裡了,否則時間片執行完,或者方法內部調用方法執行完畢之後,你都不知道你執行到哪裡了

PC計數器的特點

區別於計算機硬體的PC寄存器,兩者略有不同。計算機用PC寄存器來存放"偽指令"或地址,而相對於虛擬機,PC寄存器它表現為一塊內存,虛擬機的PC寄存器的功能也是存放偽指令,更確切的說存放的是將要執行指令的地址當虛擬機正在執行的方法是一個本地方法的時候,JVM的PC寄存器存儲的值是undefined。程序計數器是線程私有的,它的生命周期與線程一樣,每個線程都有一個此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域

對於Java應用程式來說,Java堆是虛擬機管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,Java世界裡"幾乎"所有的對象實例都在這裡分配內存。"幾乎"是指從實現角度來看,隨著Java語言的發展,現在已經能看到些許跡象表明日後出現值類型的支持,即使只考慮現在,由於即時編譯技術的進步,尤其是逃逸分析技術的日漸強大,棧上分配,標量替換優化手段已經導致一些微妙的變化悄然發生,所以說Java對象實例都分配在堆上也漸漸變得不那麼絕對了。

你一定要知道的JVM逃逸分析:https://juejin.cn/post/6905629726573133837 如果想了解棧上分配,請看我掘金上發的這篇文章,通俗易懂

堆的特點

是Java虛擬機所管理的內存中最大的一塊堆是JVM所有線程共享的堆中也包含私有的線程緩衝區 Thread Local Allocation Buffer(TLAB)複製代碼在虛擬機啟動的時候創建唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都要在這裡分配內存Java堆是垃圾收集器管理的主要區域因此很多時候Java堆也被稱為"GC堆"。從內存回收的角度來看,由於現在收集器基本採用分代收集算法,所以Java堆還可以細分為:新生代,老年代;新生代又可以分為Eden空間,From Survivor空間,To Survivor空間Java堆是計算機物理存儲上不連續的,邏輯上是連續的,也是大小可調節的(通過-Xms和-Xmx控制)。方法結束後,堆中對象不會馬上移除僅僅在垃圾回收的時候才移除。如果在堆中沒有內存完成實例的分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。堆的分類

現在垃圾回收器都使用分代理論,堆空間也分類如下:

在Java7 Hotspot虛擬機中將Java堆內存分為3個部分:

年輕代老年代永久代

在Java8之後,由於方法區的內存不在分配在Java堆上,而是存儲於本地內存元空間Metaspace中,所以永久代就不存在了

年輕代和老年代

年輕代(Young Gen):年輕代主要存放新創建的對象,內存大小相對會比較小,垃圾回收會比較頻繁。年輕代分成1個Eden Space和2個Suvivor Space(from和to)老年代(Tenrued Gen):老年代主要存放JVM認為生命周期比較長的對象(經過幾次Young Gen的垃圾回收後仍然存在),內存大小相對會比較大,垃圾回收也相對沒有那麼頻繁。

新生代和老年代堆結構佔比

默認 -XX:NewRatio=2,標識新生代佔1,老年代佔2,新生代佔整個堆的1/3

Eden空間和另外兩個Survivor空間佔比分別是8:1:1

可以通過操作選項-XX:SurvivorRatio調整這個空間比例。比如:-XX:SurvivorRatio=8

幾乎所有的java對象都在Eden區創建,但80%的對象生命周期都很短,創建出來就會銷毀

從圖中可以看出:堆大小=新生代+老年代。其中,堆的大小可以通過參數-Xms,-Xmx來指定

默認的,新生代(Young)與老年代(Old)的比例的值為1:2(該值可以通過參數-XX:NewRatio來指定),即,新生代(Young)=1/3的堆空間大小。老年代(Old)=2/3的堆空間大小。其中,新生代(Young)被細分為Eden和兩個Survivor區域,這兩個Survivor區域分別被命名為from和to,以示 區分。默認的,Eden:from:to=8:1:1(可以通過JVM每次只會使用Eden和其中一塊Survivor區域來為對象服務,所以無論什麼時候,總是有一塊Survivor區域是空閒的。因此,新生代市實際可用的內存空間為9/10(即90%)的新生代空間。

對象分配過程

JVM設計者不僅需要考慮內存如何分配,在哪裡分配等問題,並且由於內存分配算法與內存回收算法密切相關,因此還需要考慮GC執行完內存回收後是否存在空間中間產生內存碎片。

分配過程:

new的對象先放到伊甸園區。該區域大小限制當伊甸園區填滿時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中不再被其他對象引用的對象進行銷毀,再加載新的對象放到伊甸園區然後再將伊甸園區的剩餘對象移動到Survivor0區如果再次觸發垃圾回收,此時上次倖存下來的放在Survivor0區的,如果沒有回收,就放在Survivor1區如果再次經歷垃圾回收,此時會重新返回Survivor0區,接著再去Survivor1區。如果累計次數到達默認的15次,就會進入老年代老年代內存不足時,會再次觸發GC:Major GC進行老年代的內存清理如果老年代執行Major GC後仍然沒有辦法進行對象的保存,就會報OOM異常

分配對象的流程:

堆GC

Java中的堆也是GC垃圾收集的主要區域,GC分為兩種:一種是部分收集器(Partial GC),另一種是整堆收集器(Full GC)

部分收集器:不是完整收集Java堆的收集器,它又分為:

新生代收集(Minor GC/Young GC):只是新生代的垃圾收集老年代收集(Major GC/Old GC):只是老年代的垃圾收集(CMS GC單獨回收老年代)混合收集(Mixed GC):收集整個新生代以及老年代的垃圾收集(G1 GC會混合回收,region區域回收)整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集器

年輕代GC觸發條件:

年輕代空間不足,就會觸發Minor GC, 這裡年輕代指的是Eden代滿,Survivor不滿不會引發GCMinor GC會引發STW(stop the world) ,暫停其他用戶的線程,等垃圾回收接收,用戶的線程才恢復老年代GC (Major GC)觸發機制

老年代空間不足時,會嘗試觸發MinorGC. 如果空間還是不足,則觸發Major GC如果Major GC , 內存仍然不足,則報錯OOMMajor GC的速度比Minor GC慢10倍以上.FullGC 觸發機制:

調用System.gc() , 系統會執行Full GC ,不是立即執行. 老年代空間不足方法區空間不足通過Minor GC進入老年代平均大小大於老年代可用內存通過列印GC信息來看新生代老年代情況和是否GC

在idea VM options中配置-XX:+PrintGCDetails用來列印GC

public class GCTest { public static void main(String[] args) { //byte[] allocation1 = new byte[60000*1024]; //byte[] allocation2 = new byte[10000*1024]; //byte[] allocation3 = new byte[10000*1024]; //byte[] allocation4 = new byte[10000*1024]; // byte[] allocation5 = new byte[10000*1024]; }}複製代碼執行上面代碼,結果:

可以看到eden佔了8%,from和to佔0%,老年代也是0%,這裡eden為什麼執行就佔8%相信大家應該也知道,我就不用多說了,繼續

public class GCTest { public static void main(String[] args) { byte[] allocation1 = new byte[60000*1024]; //byte[] allocation2 = new byte[10000*1024]; //byte[] allocation3 = new byte[10000*1024]; //byte[] allocation4 = new byte[10000*1024]; // byte[] allocation5 = new byte[10000*1024]; }}複製代碼

這裡看的話,eden區直接從8%升到99%,那麼我們繼續把6改成7

public class GCTest { public static void main(String[] args) { byte[] allocation1 = new byte[70000*1024]; //byte[] allocation2 = new byte[10000*1024]; //byte[] allocation3 = new byte[10000*1024]; //byte[] allocation4 = new byte[10000*1024]; // byte[] allocation5 = new byte[10000*1024]; }}複製代碼

這裡就是大對象,直接進入老年代了,新生代還是和main方法中沒代碼運行的時候一樣了,都是0%

這個時候,我們把7改回6,6000*1024的時候是eden區馬上滿的時候,我們把allocation2解開注釋,來看一下列印信息

public class GCTest { public static void main(String[] args) { byte[] allocation1 = new byte[60000*1024]; byte[] allocation2 = new byte[10000*1024]; //byte[] allocation3 = new byte[10000*1024]; //byte[] allocation4 = new byte[10000*1024]; // byte[] allocation5 = new byte[10000*1024]; }}複製代碼

這裡發生Young GC是因為給allocation2分配內存的時候,eden區內存幾乎被分配完了,上面也說到了Eden沒有足夠的空間進行分配的時候,虛擬機就會發起一次Young GC(Minor GC),GC期間虛擬機又發現allocation1無法存入Survior空間,所以只能把新生代的對象提前轉交給老年代中去,老年代的空間足夠存放allocation1,所以不會出現Full GC

什麼樣子的對象直接進入老年代

大對象直接進入老年代

大對象就是需要大量連續空間的對象(比如:字符串,數組)。JVM參-XX:PretenureSizeThreshold 可以設置大對象的大小,如果對象超過設置大小會直接進入老年代,不會進入年輕代,這個參數只在 Serial 和ParNew兩個收集器下有效。

比如設置JVM參數:-XX:PretenureSizeThreshold=1000000 (單位是字節) -XX:+UseSerialGC ,再執行下上面的第一個程序會發現大對象直接進了老年代

為什麼要這樣?

為了避免為大對象分配內存時的複製操作而降低效率

長期存活的對象進入老年代

既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。

如果對象在 Eden 出生並經過第一次 Minor GC 後仍然能夠存活,並且能被 Survivor 容納的話,將被移動到 Survivor空間中,並將對象年齡設為1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,CMS收集器默認6歲,不同的垃圾收集器會略微有點不同),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold 來設置。

對象動態年齡判斷

當前放對象的Survivor區域裡(其中一塊區域,放對象的那塊s區),一批對象的總大小大於這塊Survivor區域內存大小的50%(-XX:TargetSurvivorRatio可以指定),那麼此時大於等於這批對象年齡最大值的對象,就可以直接進入老年代了, 例如Survivor區域裡現在有一批對象,年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會 把年齡n(含)以上的對象都放入老年代。這個規則其實是希望那些可能是長期存活的對象,儘早進入老年代。對象動態年 齡判斷機制一般是在minor gc之後觸發的。

老年代空間分配擔保機制

年輕代每次minor gc之前JVM都會計算下老年代剩餘可用空間 如果這個可用空間小於年輕代裡現有的所有對象大小之和(包括垃圾對象) 就會看一個「-XX:-HandlePromotionFailure」(jdk1.8默認就設置了)的參數是否設置了 如果有這個參數,就會看看老年代的可用內存大小,是否大於之前每一次minor gc後進入老年代的對象的平均大小。 如果上一步結果是小於或者之前說的參數沒有設置,那麼就會觸發一次Full gc,對老年代和年輕代一起回收一次垃圾, 如果回收完還是沒有足夠空間存放新的對象就會發生"OOM" 當然,如果minor gc之後剩餘存活的需要挪動到老年代的對象大小還是大於老年代可用空間,那麼也會觸發full gc,full gc完之後如果還是沒有空間放minor gc之後的存活對象,則也會發生「OOM」

方法區

方法區和堆一樣,是線程共享的內存區域,它用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼緩存等數據。

元空間,永久代是方法區具體的落地實現,方法區看作是一塊獨立於Java堆的內存空間,它主要是用來存儲所加載的類信息的

方法區是對JVM的"邏輯劃分",在jdk1.7之前很多開發者習慣將方法區稱為"永久代",是因為在HotSpot虛擬機中,設計人員使用永久代來實現JVM規範的方法區。在jdk1.8之後使用了元空間來實現方法區

JVM在執行某個類的時候,必須先加載。在加載類(加載,驗證,準備,解析,初始化)的時候,JVM會先創建class文件,而在class文件中除了有類的版本,欄位,方法和接口等描述信息外,還有一項信息是常量池(final修飾的變量),符號引用則包括類和方法的全限定名(例如String這個類,它的全限定名就是Java/lang/String),欄位的名稱和描述符以及方法的名稱和描述符

創建對象各數據區域的聲明:

方法區的特點:

方法區與堆一樣是各個線程共享的內存區域方法區在JVM啟動的時候就會被創建並且它實例的物理內存空間和Java堆一樣都可以不連續方法區的大小跟堆空間一樣可以選擇固定大小或者動態變化方法區的對象決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出虛擬機同樣會跑出(OOM)異常關閉JVM就會釋放這個區域的內存方法區內部結構:

類加載器將Class文件加載到內存之後,將類的信息存儲到方法區中。

方法區中存儲的內容:

類型信息(域信息,方法信息)運行時常量池和常量池

什麼是常量池

這裡我們用javap -v xxx.class

javap -v Person.class Classfile /Users/xxx/Desktop/coupon/target/classes/com/zzz/Person.class Last modified 2020-12-17; size 572 bytes MD5 checksum c295a3afb5d289652e0225d17f86483f Compiled from "Person.java"public class com.zzz.Person minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #5.#26 // java/lang/Object."<init>":()V #2 = Class #27 // com/zzz/Person #3 = Methodref #2.#26 // com/zzz/Person."<init>":()V #4 = Methodref #2.#28 // com/zzz/Person.work:()I #5 = Class #29 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 LocalVariableTable #11 = Utf8 this #12 = Utf8 Lcom/zzz/Person; #13 = Utf8 work #14 = Utf8 ()I #15 = Utf8 x #16 = Utf8 I #17 = Utf8 y #18 = Utf8 z #19 = Utf8 main #20 = Utf8 ([Ljava/lang/String;)V #21 = Utf8 args #22 = Utf8 [Ljava/lang/String; #23 = Utf8 person #24 = Utf8 SourceFile #25 = Utf8 Person.java #26 = NameAndType #6:#7 // "<init>":()V #27 = Utf8 com/zzz/Person #28 = NameAndType #13:#14 // work:()I #29 = Utf8 java/lang/Object{ public com.zzz.Person(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/zzz/Person; public int work(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn LineNumberTable: line 6: 0 line 7: 2 line 8: 4 line 9: 11 LocalVariableTable: Start Length Slot Name Signature 0 13 0 this Lcom/zzz/Person; 2 11 1 x I 4 9 2 y I 11 2 3 z I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class com/zzz/Person 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method work:()I 12: pop 13: return LineNumberTable: line 13: 0 line 14: 8 line 15: 13 LocalVariableTable: Start Length Slot Name Signature 0 14 0 args [Ljava/lang/String; 8 6 1 person Lcom/zzz/Person;}SourceFile: "Person.java"複製代碼看列印的這些信息,裡面有一段開頭是Constant pool,這個就是常量池也可以叫做靜態常量池

常量池可以看做是一張表,虛擬機指令根據這張表找到要執行的類名,方法名,參數類型,字面量等類型。

運行時常量池

常量池是存放編譯期間生成的各種字面量與符號引用

運行時常量池:常量池在運行時的表現形式,編譯後的字節碼文件中包含了類型信息,域信息,方法信息等,通過ClassLoader將字節碼文件的常量池中的信息加載到內存中,存儲在了方法區的運行時常量池當中。 運行時常量池存放的是運行時一些直接引用。

運行時常量池在類加載完成之後,將靜態常量池中的符號引用值轉換成運行時常量池中,類在解析之後,將符號引用替換成直接引用。

運行時常量池在jdk1.7版本之後,就移到堆內存中了,這裡指的是物理空間,而邏輯空間還是屬於方法區(方法區是邏輯分區)。

public class Person { public int work(){ int x = 1; int y = 2; int z = (x+y)*10; return z; } public static void main(String[] args){ Person person = new Person(); person.work(); }}複製代碼還是這段代碼為例,其實上面的work()這個是個方法,也可以當做是一個符號,work是符合,()是符號,public修飾符也可以當成符號

運行main方法,person調用work()方法,在JVM眼中,這個work()就是一個符號

元空間

方法區與堆空間相似,也是一個共享內存區,所以方法區是線程共享的。假如兩個線程都試圖訪問方法區中的同一個類信息,而這個類還沒有裝入JVM,那麼此時只允許一個線程去加載它,另一個線程必須等待

在HotSpot虛擬機,Java7版本已經將永久代的靜態變量和運行時常量池轉移到了堆中,其餘部分則存儲在JVM的非堆內存中,而Java8版本已經將方法區實現的永久代去掉了,並用元空間代替了之前的永久代,並且在元空間的存儲位置是本地內存。

元空間大小參數:

jdk1.7 及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;jdk1.8 以後(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSizejdk1.8 以後大小就只受本機總內存的限制(如果不設置參數的話)Java8為什麼使用元空間代替永久代,這樣做有什麼好處?

官方給出的解釋是:

移除永久代是為了融合 HotSpot JVM 與 JRockit VM 而做出的努力,因為 JRockit 沒有永久代,所以不需要配置永久代。

永久代內存經常不夠用或發生內存溢出,拋出異常 java.lang.OutOfMemoryError: PermGen。這是因為在 JDK1.7 版本中,指定的 PermGen 區大小為 8M,由於 PermGen 中類的元數據信息在每次 FullGC 的時候都可能被收集,回收率都偏低,成績很難令人滿意;還有為 PermGen 分配多大的空間很難 確定,PermSize 的大小依賴於很多因素,比如,JVM 加載的 class 總數、常量池的大小和方法的大小等。

作者:Five在努力連結:https://juejin.cn/post/6907841173356806152

相關焦點

  • 想理解JVM看了這篇文章,就知道了!
    2|0JVM介紹2|1什麼是JVM作為java工程師,對於jvm肯定不陌生。JVM是Java Virtual Machine的縮寫,通俗來說也就是運行java代碼的容器。當項目啟動時,會根據jvm相關配置參數,在計算機的內存中開啟一片空間用於運行JVM。
  • 【JVM系統學習之路】運行時數據區概述和程序計數器
    JVM系統學習之路系列演示代碼地址:https://github.com/mtcarpenter/JavaTutorial本篇將 運行時數據區概述及線程 和 程序計數器 的知識點由於不是很多所以就一起梳理,也是為後續學習的知識點做一個鋪墊。
  • JDK、JRE、JVM,是什麼關係?
    一、前言截至到這已經寫了22篇面經手冊,你看了多少?其實小傅哥就是借著面經的幌子在講 Java 核心技術,探索這些核心知識點面試的背後到底在問什麼。想問一些面試官,是因為大家都在問所以你問,還是你想從這裡問出什麼? 其實可能很多面試官如果不了解這些技術,往往會被求職者的答案擊碎內心,哈哈哈哈哈哈。
  • 面經手冊 · 第23篇《JDK、JRE、JVM,是什麼關係?》
    「面試官」:謝飛機寫過 Java 嗎?「謝飛機」:那當然寫過,寫了3年多了!include:Java 和 JVM 交互的頭文件,例如我們 JVMTI 寫的 C++ 工程時,就需要把這個 include 包引入進去jvmti.h。
  • Java虛擬機系列一:一文搞懂 JVM 架構和運行時數據區
    Java 虛擬機架構圖對 JVM 還不太了解的同學第一次看到這張花裡胡哨的圖肯定會一臉懵逼,不用怕,其實我們只需要重點理解並掌握其中一部分 (同時也是面試重點) 就好了,比如運行時數據區、垃圾收集器、內存分配策略和類加載機制等,類文件結構也可以學習一下,其他的稍作了解即可。
  • JVM 解剖公園:JNI 臨界區與 GC Locker
    寫在前面「[JVM 解剖公園][1]」是一個持續更新的系列迷你博客,閱讀每篇文章一般需要5到10分鐘。限於篇幅,僅對某個主題按照問題、測試、基準程序、觀察結果深入講解。因此,這裡的數據和討論可以當軼事看,不做寫作風格、句法和語義錯誤、重複或一致性檢查。如果選擇採信文中內容,風險自負。
  • JVM-概述和內存區域
    舉個例子將groovy編譯之後的class文件用jvm運行1、先配置好groovy環境方法區(永久代)在jdk8中又叫做元空間Metaspace運行時數據區概述堆內存:保存所有引用數據的真實信息;棧內存:基本類型、運算、指向堆內存的指針;方法區:所以定義的方法的信息都保存方法區中,屬於共享區
  • 程式設計師每日一題-jvm裡方法和方法區、棧區的二三事
    答案是D解析又是JVM相關的,那就先上一個JAVA虛擬機運行時數據區的邏輯圖JAVA虛擬機運行時數據區的邏輯圖好,對照上圖,逐項解釋A:堆區是JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。
  • JVM 面試基礎準備篇(一)
    常量池主要存儲兩方面內容:字面量(Literal)和符號引用(Symbolic References)字面量:文本字符串,final修飾等符號引用:類和接口的全限定名、欄位名稱和描述符、方法名稱和描述符2.1.3.1  javap 驗證javap -v -p Person.class > vp.txt 反編譯驗證字節碼和指令信息
  • 聊到JVM(還怕面試官問JVM嗎?)
    3、JVM體系結構 ‍類裝載器子系統運行時數據區執行引擎本地方法接口垃圾收集模塊    發現報錯了,這就是雙親委派機制起的作用,當類加載器委託到根加載器的時候,String類已經被根加載器加載過一遍了,所以不會再加載,從一定程度上防止了危險代碼的植入!!1.  防止重複加載同一個.class。
  • JVM中的五大內存區域劃分詳解及快速掃盲
    簡單用一張圖來理解這三個的關係:3. jvm的組成成分不了解jvm的同學看到這張圖後可能會有點懵逼,不過沒關係,放這張圖只是想讓你了解jvm中有三塊內容非常重要,1.java代碼如何執行?運行java文件的大概流程想要運行java的源文件,必須要經過javac編譯器編譯成.class文件,也就是字節碼文件。然後通過jvm中的解釋器,解釋成特定機器上的機器碼。每種機器上的解釋器是不一樣的,我們經常用的也就是windows和linux系統,這也是為什麼java能夠跨平臺的原因。
  • JVM之jvisualvm的簡單使用
    JVM之jvisualvm的簡單使用閱讀本篇教程之前,必須先閱讀JVM之jconsole的簡單使用這篇教程。jvisualvm比jconsole稍微好用了那麼一點點,其實就是圖形化界面做的比jconsole好看了一點。還有一點就是jvisualvm支持插件安裝。
  • JVM超神之路:年後跳槽需要的JVM知識點,周末給你整理了一份!!!
    另外,OOM 時自動 Dump 堆棧,我一般也會進行配置。2、常用的調優工具有哪些?JDK內置的命令行:jps(查看jvm進程信息)、jstat(監視jvm運行狀態的,比如gc情況、jvm內存情況、類加載情況等)、jinfo(查看jvm參數的,也可動態調整)、jmap(生成dump文件的,在dump的時候會影響線上服務)、jhat(分析dump的,但是一般都將dump導出放到mat上分析)、jstack(查看線程的)。
  • JVM性能調優實踐
    ③GC日誌:程序啟動時用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序運行時把gc的詳細過程記錄下來,或者直接配置「-verbose:gc」參數把gc日誌列印到控制臺,通過記錄的gc日誌可以分析每塊內存區域gc的頻率、時間等,從而發現問題,進行有針對性的優化。
  • JavaScript引擎實現JVM 支持運行Java
    而使用JVM是實現這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入JVM後,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用JVM屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。JVM在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。
  • JVM菜鳥進階高手之路十四:分析篇
    題目回顧JVM菜鳥進階高手之路十三,問題現象就是相同的代碼,jvm參數不一樣,表現的現象不一樣。缺點:對空間有一定浪費,所以複製空間一般不會特別大。-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 還有這2個參數關於cms的,-XX:+UseCMSInitiatingOccupancyOnly表示JVM不基於運行時收集的數據來啟動CMS垃圾收集周期通過CMSInitiatingOccupancyFraction
  • JVM源碼分析之jstat工具原理完全解讀
    hsperfdata_<user>/<pid>這個文件PerfData文件文件創建這個文件是否存在取決於兩個參數,一個UsePerfData,另一個是PerfDisableSharedMem,如果設置了-XX:+PerfDisableSharedMem或者-XX:-UsePerfData,那這個文件是不會存在的,默認情況下PerfDisableSharedMem
  • 我們寫的Java代碼是怎麼運行起來的?
    在進行「我們寫的Java代碼是怎麼運行起來的?」這個話題之前,我們先來了解一個概念:JVM。引入Java語言虛擬機後,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。
  • jvm面試系列一:java內存模型你掌握了多少?
    如果你應聘的職位涉及系統調優,如堆大小的分配、垃圾回收機制的選擇、處理內存溢出、線程死鎖等,對JVM這一塊知識就有更高的要求。說明:因為知識點較多,擴展開來篇幅太長,jvm系列面試題將會分解開來從內存模型,垃圾回收,類加載機制,參數調優等多個角度整理,方便大家閱讀。
  • java線程前傳——jvm內存結構、內存模型和cpu結構
    當代計算機體系結構1、當代計算CPU體系大體上如下每個CPU有一個共享緩存,名為三級緩存L3每個CPU有多個核心組成每個核心有兩級緩存,分別叫一級緩存L1,二級緩存L2一級緩存分兩種,分別叫數據緩存(L1 D),指令緩存(L1 i)CPU肯定是需要和內存交互的,這個過程是少不了的一個線程肯定是要運行在一個核上的