JVM內(nèi)存結(jié)構(gòu):程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧
一、JVM 入門(mén)介紹
JVM 定義
Java Virtual Machine,JAVA程序的運(yùn)行環(huán)境(JAVA二進(jìn)制字節(jié)碼的運(yùn)行環(huán)境)
JVM 優(yōu)勢(shì)
- 一次編寫(xiě),到處運(yùn)行
- 自動(dòng)內(nèi)存管理,垃圾回收機(jī)制
- 數(shù)組下標(biāo)越界檢查 常見(jiàn)的JVM
注:我們筆記所使用的的是HotSpot 版本
JVM JRE JDK的比較
JVM JRE JDK的區(qū)別:
學(xué)習(xí)步驟
學(xué)習(xí)順序如下圖:(由簡(jiǎn)到難)
二、內(nèi)存結(jié)構(gòu)
整體架構(gòu)
1、程序計(jì)數(shù)器(寄存器)
Program Counter Register
1.1 作用
程序計(jì)數(shù)器用于保存JVM中下一條所要執(zhí)行的指令的地址
0:getstatic #20 // PrintStream out = System.out; 1:astore_1 // -- 2:aload_1 // out.println(1); 3:iconst_1 // -- 4:invokevirtual #26 // -- 5:aload_1 // out.println(2); 6:iconst_2 // -- 7:invokevirtual #26 // -- 8:aload_1 // out.println(3); 9:iconst_3 // -- 10:invokevirtual #26 // -- 11:aload_1 // out.println(4); 12:iconst_4 // -- 13:invokevirtual #26 // -- 14:aload_1 // out.println(5); 15:iconst_5 // -- 16:invokevirtual #26 // -- return
Java指令執(zhí)行流程:
- 每一條二進(jìn)制字節(jié)碼(JVM指令) 通過(guò) 解釋器 轉(zhuǎn)換成 機(jī)器碼 然后 就可以被 CPU 執(zhí)行了!
- 當(dāng) 解釋器 將一條jvm 指令轉(zhuǎn)換成 機(jī)器碼后 其會(huì) 向程序計(jì)數(shù)器 遞交 下一條 jvm 指令的執(zhí)行地址!
- 程序計(jì)數(shù)器在硬件層面 其實(shí)是通過(guò) 寄存器 實(shí)現(xiàn)的!
- 所以程序計(jì)數(shù)器的作用就是:用于保存JVM中下一條所要執(zhí)行的指令的地址!
1.2 特點(diǎn)
- 線(xiàn)程私有
- CPU會(huì)為每個(gè)線(xiàn)程分配時(shí)間片,當(dāng)當(dāng) 前線(xiàn)程的時(shí)間片使用完以后,CPU就會(huì)去執(zhí)行另一個(gè)線(xiàn)程中的代碼
- 程序計(jì)數(shù)器是每個(gè)線(xiàn)程所私有的,當(dāng)另一個(gè)線(xiàn)程的時(shí)間片用完,又返回來(lái)執(zhí)行當(dāng)前線(xiàn)程的代碼時(shí),通過(guò)程序計(jì)數(shù)器可以知道應(yīng)該執(zhí)行哪一句指令
- 不會(huì)存在內(nèi)存溢出
2、虛擬機(jī)棧
Java Virtual Machine Stacks
2.1 定義
- 每個(gè)線(xiàn)程運(yùn)行需要的內(nèi)存空間,這一空間被稱(chēng)為虛擬機(jī)棧(Frames)
- 每個(gè)棧由多個(gè)棧幀(Frame) 組成,對(duì)應(yīng)著每個(gè)方法運(yùn)行時(shí)所占用的內(nèi)存
- 每個(gè)線(xiàn)程只能有一個(gè)活動(dòng)棧幀,對(duì)應(yīng)著當(dāng)前正在執(zhí)行的方法,當(dāng)方法執(zhí)行時(shí)壓入棧,方法執(zhí)行完畢后 彈出棧
2.2 演示
代碼
/** * @Auther: csp1999 * @Date: 2020/11/10/11:36 * @Description: 演示棧幀 */ public class Demo01 { public static void main(String[] args) { methodA(); } private static void methodA() { methodB(1, 2); } private static int methodB(int a, int b) { int c = a + b; return c; } }
我們打斷點(diǎn)來(lái)Debug 一下看一下方法執(zhí)行的流程:
接這往下走,使方法B執(zhí)行完畢:
然后方法A執(zhí)行完畢,其對(duì)應(yīng)的棧幀出棧,main方法對(duì)應(yīng)的棧幀為活動(dòng)棧幀;最后main執(zhí)行完畢 棧幀出棧,虛擬機(jī)棧為空,代碼運(yùn)行結(jié)束!
2.3 面試問(wèn)題辨析
- 1.垃圾回收是否涉及棧內(nèi)存?
- 不需要。因?yàn)樘摂M機(jī)棧中是由一個(gè)個(gè)棧幀組成的,在方法執(zhí)行完畢后,對(duì)應(yīng)的棧幀就會(huì)被彈出棧。所以無(wú)需通過(guò)垃圾回收機(jī)制去回收內(nèi)存。
- 2.棧內(nèi)存的分配越大越好嗎?
- 不是。因?yàn)?strong>物理內(nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線(xiàn)程數(shù)就會(huì)越少。
- 舉例:如果物理內(nèi)存是500M(假設(shè)),如果一個(gè)線(xiàn)程所能分配的棧內(nèi)存為2M的話(huà),那么可以有250個(gè)線(xiàn)程。而如果一個(gè)線(xiàn)程分配棧內(nèi)存占5M的話(huà),那么最多只能有100 個(gè)線(xiàn)程同時(shí)執(zhí)行!
3.方法內(nèi)的局部變量是否是線(xiàn)程安全的?
從圖中得出:局部變量如果是靜態(tài)的可以被多個(gè)線(xiàn)程共享,那么就存在線(xiàn)程安全問(wèn)題。如果是非靜態(tài)的只存在于某個(gè)方法作用范圍內(nèi),被線(xiàn)程私有,那么就是線(xiàn)程安全的!
看一個(gè)案例:
/** * 局部變量的線(xiàn)程安全問(wèn)題 */ public class Demo02 { public static void main(String[] args) {// main 函數(shù)主線(xiàn)程 StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(() -> {// Thread新創(chuàng)建的線(xiàn)程 m2(sb); }).start(); } public static void m1() { // sb 作為方法m1()內(nèi)部的局部變量,是線(xiàn)程私有的 ---> 線(xiàn)程安全 StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { // sb 作為方法m2()外部的傳遞來(lái)的參數(shù),sb 不在方法m2()的作用范圍內(nèi) // 不是線(xiàn)程私有的 ---> 非線(xiàn)程安全 sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { // sb 作為方法m3()內(nèi)部的局部變量,是線(xiàn)程私有的 StringBuilder sb = new StringBuilder();// sb 為引用類(lèi)型的變量 sb.append(1); sb.append(2); sb.append(3); return sb;// 然而方法m3()將sb返回,sb逃離了方法m3()的作用范圍,且sb是引用類(lèi)型的變量 // 其他線(xiàn)程也可以拿到該變量的 ---> 非線(xiàn)程安全 // 如果sb是非引用類(lèi)型,即基本類(lèi)型(int/char/float...)變量的話(huà),逃離m3()作用范圍后,則不會(huì)存在線(xiàn)程安全 } }
該面試題答案:
如果方法內(nèi)局部變量沒(méi)有逃離方法的作用范圍,則是線(xiàn)程安全的
如果局部變量引用了對(duì)象,并逃離了方法的作用范圍,則需要考慮線(xiàn)程安全問(wèn)題
2.4 內(nèi)存溢出
Java.lang.stackOverflowError 棧內(nèi)存溢出
發(fā)生原因
- 1.虛擬機(jī)棧中,棧幀過(guò)多(無(wú)限遞歸),這種情況比較常見(jiàn)!
- 2.每個(gè)棧幀所占用內(nèi)存過(guò)大(某個(gè)/某幾個(gè)棧幀內(nèi)存直接超過(guò)虛擬機(jī)棧最大內(nèi)存),這種情況比較少見(jiàn)!
舉2個(gè)案例:
案例1:
/** * 演示棧內(nèi)存溢出 java.lang.StackOverflowError * -Xss256k 可以通過(guò)棧內(nèi)存參數(shù) 設(shè)置棧內(nèi)存大小 */ public class Demo03 { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++;// 統(tǒng)計(jì)棧幀個(gè)數(shù) method1();// 方法無(wú)限遞歸,不斷產(chǎn)生棧幀 到虛擬機(jī)棧 } } 最后輸出結(jié)果: java.lang.StackOverflowError at com.haust.jvm_study.demo.Demo03.method1(Demo03.java:21) ... ... 39317// 棧幀個(gè)數(shù),不同的虛擬機(jī)大小能存放的棧幀數(shù)量不一樣
我們可以通過(guò)修改參數(shù)來(lái)指定虛擬機(jī)棧內(nèi)存大小
當(dāng)我們將虛擬機(jī)棧內(nèi)存縮小到指定的256k的時(shí)候再運(yùn)行Demo03后,會(huì)得到其棧內(nèi)最大棧幀數(shù)為:3816 遠(yuǎn)小于原來(lái)的39317!
案例2:
/** * 兩個(gè)類(lèi)之間的循環(huán)引用問(wèn)題,導(dǎo)致的棧溢出 * * 解決方案:打斷循環(huán),即在員工emp 中忽略其dept屬性,放置遞歸互相調(diào)用 */ public class Demo04 { public static void main(String[] args) throws JsonProcessingException { Dept d = new Dept(); d.setName("Market"); Emp e1 = new Emp(); e1.setName("csp"); e1.setDept(d); Emp e2 = new Emp(); e2.setName("hzw"); e2.setDept(d); d.setEmps(Arrays.asList(e1, e2)); // 輸出結(jié)果:{"name":"Market","emps":[{"name":"csp"},{"name":"hzw"}]} ObjectMapper mapper = new ObjectMapper();// 要導(dǎo)入jackson包 System.out.println(mapper.writeValueAsString(d)); } } /** * 員工 */ class Emp { private String name; @JsonIgnore// 忽略該屬性:為啥呢?我們來(lái)分析一下! /** * 如果我們不忽略掉員工對(duì)象中的部門(mén)屬性 * System.out.println(mapper.writeValueAsString(d)); * 會(huì)出現(xiàn)下面的結(jié)果: * { * "name":"Market","emps": * [c * {"name":"csp",dept:{name:'xxx',emps:'...'}}, * ... * ] * } * 也就是說(shuō),輸出結(jié)果中,部門(mén)對(duì)象dept的json串中包含員工對(duì)象emp, * 而員工對(duì)象emp 中又包含dept,這樣互相包含就無(wú)線(xiàn)遞歸下去,json串越來(lái)越長(zhǎng)... * 直到棧溢出! */ private Dept dept; public String getName() { return name; } public void setName(String name) { this.name = name; } public Dept getDept() { return dept; } public void setDept(Dept dept) { this.dept = dept; } } /** * 部門(mén) */ class Dept { private String name; private List<Emp> emps; public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Emp> getEmps() { return emps; } public void setEmps(List<Emp> emps) { this.emps = emps; } }
2.5 線(xiàn)程運(yùn)行診斷
案例1:CPU占用過(guò)高
- Linux環(huán)境下運(yùn)行某些程序的時(shí)候,可能導(dǎo)致CPU的占用過(guò)高,這時(shí)需要定位占用CPU過(guò)高的線(xiàn)程
- top命令,查看是哪個(gè)進(jìn)程占用CPU過(guò)高
- ps H -eo pid, tid(線(xiàn)程id), %cpu | grep 剛才通過(guò)top查到的進(jìn)程號(hào) 通過(guò)ps命令進(jìn)一步查看具體是哪個(gè)線(xiàn)程占用CPU過(guò)高!
- jstack 進(jìn)程id 通過(guò)查看進(jìn)程中的線(xiàn)程的nid,剛才通過(guò)ps命令看到的tid來(lái)對(duì)比定位,注意jstack查找出的線(xiàn)程id是16進(jìn)制的,需要轉(zhuǎn)換
- 可以通過(guò)線(xiàn)程id,找到有問(wèn)題的線(xiàn)程,進(jìn)一步定位到問(wèn)題代碼的源碼行數(shù)!
我們可以看到上圖中的thread1 線(xiàn)程一直在運(yùn)行(runnable)中,說(shuō)明就是它占用了較高的CPU內(nèi)存;
3、本地方法棧
一些帶有native 關(guān)鍵字的方法就是需要JAVA去調(diào)用本地的C或者C++方法,因?yàn)镴AVA有時(shí)候沒(méi)法直接和操作系統(tǒng)底層交互,所以需要用到本地方法!
如圖:
- 本地接口的作用是融合不同的編程語(yǔ)言為Java所用,它的初衷是融合C/C++程序,Java誕生的時(shí)候是C/C++橫行的時(shí)候,要想立足,必須由調(diào)用C/C++程序,于是就在內(nèi)存中專(zhuān)門(mén)開(kāi)辟了一塊區(qū)域處理標(biāo)記為native的代碼,它的具體做法是Native Method Stack中登記native方法,在Execution Engine執(zhí)行時(shí)加載native libraies
- 目前該方法的使用的越來(lái)越少了,除非是與硬件有關(guān)的應(yīng)用,比如通過(guò)Java程序驅(qū)動(dòng)打印機(jī)或者Java系統(tǒng)管理生產(chǎn)設(shè)備,在企業(yè)級(jí)應(yīng)用中已經(jīng)比較少見(jiàn)。因?yàn)楝F(xiàn)在的異構(gòu)領(lǐng)域間的通信很發(fā)達(dá),比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹
- 本地方法棧(Native Method Stack):(它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執(zhí)行時(shí)加載本地方法庫(kù))
- native方法的舉例: Object類(lèi)中的clone wait notify hashCode 等 Unsafe類(lèi)都是native方法
4、總結(jié)
這篇文章的內(nèi)容就到這了,希望大家多多關(guān)注腳本之家的其他內(nèi)容!
相關(guān)文章
Flink流處理引擎零基礎(chǔ)速通之?dāng)?shù)據(jù)的抽取篇
今天不分享基礎(chǔ)概念知識(shí)了,來(lái)分享一個(gè)馬上工作需要的場(chǎng)景,要做數(shù)據(jù)的抽取,不用kettle,想用flink。實(shí)際就是flink的sql、table層級(jí)的api2022-05-05Servlet從入門(mén)到精通(超級(jí)詳細(xì)!)
在JavaWeb項(xiàng)目中,處理請(qǐng)求和發(fā)送響應(yīng)的過(guò)程是由一種叫做Servlet 的程序來(lái)完成的,并且 Servlet 是為了解決實(shí)現(xiàn)動(dòng)態(tài)頁(yè)面而衍生的東西,下面這篇文章主要給大家介紹了關(guān)于Servlet從入門(mén)到精通的相關(guān)資料,需要的朋友可以參考下2022-03-03Java結(jié)合redistemplate使用分布式鎖案例講解
在Java中使用RedisTemplate結(jié)合Redis來(lái)實(shí)現(xiàn)分布式鎖是一種常見(jiàn)的做法,特別適用于微服務(wù)架構(gòu)或多實(shí)例部署的應(yīng)用程序中,以確保數(shù)據(jù)的一致性和避免競(jìng)態(tài)條件,下面給大家分享使用Spring Boot和RedisTemplate實(shí)現(xiàn)分布式鎖的案例,感興趣的朋友一起看看吧2024-08-08java ThreadPool線(xiàn)程池的使用,線(xiàn)程池工具類(lèi)用法說(shuō)明
這篇文章主要介紹了java ThreadPool線(xiàn)程池的使用,線(xiàn)程池工具類(lèi)用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10idea普通javaweb項(xiàng)目如何部署到tomcat(讀取web.xml文件)
這篇文章主要介紹了idea普通javaweb項(xiàng)目如何部署到tomcat(讀取web.xml文件),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08springboot實(shí)現(xiàn)maven多模塊和打包部署
本文主要介紹了springboot實(shí)現(xiàn)maven多模塊和打包部署,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04java多線(xiàn)程實(shí)現(xiàn)有序輸出ABC
這篇文章主要為大家詳細(xì)介紹了java多線(xiàn)程實(shí)現(xiàn)有序輸出ABC,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08JVM內(nèi)存結(jié)構(gòu)相關(guān)知識(shí)解析
這篇文章主要介紹了JVM內(nèi)存結(jié)構(gòu)相關(guān)知識(shí)解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Java反射中java.beans包學(xué)習(xí)總結(jié)
本篇文章通過(guò)學(xué)習(xí)Java反射中java.beans包,吧知識(shí)點(diǎn)做了總結(jié),并把相關(guān)內(nèi)容做了關(guān)聯(lián),對(duì)此有需要的朋友可以學(xué)習(xí)參考下。2018-02-02java 利用反射獲取內(nèi)部類(lèi)靜態(tài)成員變量的值操作
這篇文章主要介紹了java 利用反射獲取內(nèi)部類(lèi)靜態(tài)成員變量的值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12