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