面試官問我平時(shí)寫的Bug的存儲位置
本文轉(zhuǎn)載自微信公眾號「大魚仙人」,作者大魚 。轉(zhuǎn)載本文請聯(lián)系大魚仙人公眾號。
說到寫bug,我們每天都在用Java實(shí)現(xiàn)著各種需求,我們實(shí)現(xiàn)的Java程序每天都運(yùn)行在每個(gè)機(jī)器的虛擬機(jī)上,但是你了解你寫的代碼的具體存儲位置嗎
說實(shí)話,這個(gè)東西,在我剛開始學(xué)Java的時(shí)候,我聽到JVM虛擬機(jī)這個(gè)名詞的時(shí)候,我的感覺是這個(gè)樣子的(慚愧
你們肯定也會有些疑問吧,平時(shí)寫的代碼每一部分都是存儲在哪里的?是的,沒錯,我的內(nèi)心就像拖著下巴的那位,除了,模樣,emmm...
雖然現(xiàn)在也不是多么的精通,但是比之前好太多了,不是涉及很底層的東西也算是了解一些,當(dāng)然真要是問我各種涉及細(xì)節(jié),毫不謙虛的說,以我的水平,我可能只會阿巴阿巴(逃
如果大家對更深入的JVM感興趣,可以和JVM大神R大這種多去溝通溝通
是的,沒錯,其實(shí)我這個(gè)文章算是掃盲文章,但是在掃盲文章的基礎(chǔ)上說的更細(xì)一點(diǎn),更多一點(diǎn),我也會給大家拋出一些面試官愛問的問題,并且?guī)痛蠹医獯穑源蠹艺埍M情讀下去,肯定會讓你有所收獲
大家覺得不錯的點(diǎn)個(gè)關(guān)注,大家一起探討、一起學(xué)習(xí)、一起進(jìn)步
JVM內(nèi)存結(jié)構(gòu)
JVM內(nèi)存布局,先給大家上個(gè)圖
如果你是讀過JVM文章的養(yǎng)魚仔的話,那你肯定看過上面類似的圖,我在給大家放一張,大家在熟悉一遍,看過的回一下,沒看過的混個(gè)臉熟
JVM內(nèi)存主要分為堆、虛擬機(jī)棧、本地方法棧、方法區(qū)、程序計(jì)數(shù)器等,堆是虛擬機(jī)內(nèi)存占據(jù)最大的一部分,堆的目的就是盛放大量的對象實(shí)例的;虛擬機(jī)棧對應(yīng)的是方法的執(zhí)行過程,本地方法棧是用來調(diào)用本地方法的執(zhí)行過程;方法區(qū)就是用來存儲存儲類信息、常量、靜態(tài)變量的數(shù)據(jù),是線程共享的數(shù)據(jù);程序計(jì)數(shù)器,就是存儲著線程下一條將要執(zhí)行的指令
每個(gè)區(qū)域都有其特定的功能,就像是一個(gè)企業(yè),一個(gè)工作室,每個(gè)人發(fā)揮著自己的長處,各司其職
走著吧,各位養(yǎng)魚仔(我是魚),一起來瞧瞧每一部分的具體的細(xì)節(jié)以及面試官愛問的問題
虛擬機(jī)堆
Java堆是垃圾收集器管理的主要地方,因此很多的時(shí)候也被稱為GC堆,Java堆還可以分為年輕代和老年代,年輕代又可以分為Eden空間、From Survivor空間、To Survivor空間,默認(rèn)是8:1:1的比例
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣
在實(shí)現(xiàn)時(shí),既可以實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過-Xmx和-Xms控制);如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會拋出OutOfMemoryError異常。
打斷一下,Java堆的區(qū)域都是線程共享的嗎?
當(dāng)你聽到這個(gè)問題的時(shí)候,你首先想到的是什么呢?
let me tell you,面試官其實(shí)問這個(gè)的時(shí)候就是在看你對堆的了解程度,你只知道是用來放對象實(shí)例的,那面試官對你表現(xiàn)覺得不算非常滿意;但是如果你知道TLAB,并且知道它的原理和問題,那面試官就會覺得:這小伙子不一般,我得再多深入了解了解,可以考慮當(dāng)我的好助手
首先,你得肯定回答,沒錯,堆是全局共享的,但是會存在一些問題,那就是多個(gè)線程在堆上同時(shí)申請空間,如果在并發(fā)的場景中,兩個(gè)線程先后把對象引用指向了同一個(gè)內(nèi)存區(qū)域,那可能就會出現(xiàn)問題;為了解決這個(gè)問題呢,就得進(jìn)行同步控制,說到同步控制,就會影響到效率
就拿Hotspot來舉例子,它的解決方案是每個(gè)線程在堆中都預(yù)先分配一小塊內(nèi)存,然后再給對象分配內(nèi)存的時(shí)候,先在這塊“私有內(nèi)存”進(jìn)行分配,這塊用完之后再去分配新的“私有內(nèi)存”,這就是TLAB分配
你也看到了,我加引號了,它并不是真正意義上的私有,而是表面上的私有;它是從堆內(nèi)存劃分出來的,有了TLAB技術(shù),堆內(nèi)存并不是完完全全的線程共享,每個(gè)線程在初始化的時(shí)候都會去內(nèi)存中申請一塊TLAB
切記:并不是TLAB區(qū)域的內(nèi)存其它線程完全無法訪問,其它線程也是可以讀取的,只不過無法在這個(gè)區(qū)域分配內(nèi)存而已
說到這的時(shí)候,也給面試官一個(gè)眼神,說明我的干貨還沒完,我還能繼續(xù)吹
難道TLAB很完美嗎?所謂,金無足赤人無完人,肯定有他的問題所在
我們知道TLAB是線程特有的,它的內(nèi)存區(qū)域不是很大,所以會出現(xiàn)一些不夠用的情況,比如一個(gè)線程的TLAB的空間有100KB,其中已經(jīng)使用了80KB,如果還需要再分配一個(gè)30KB的對象,則無法直接在TLAB上分配了,這種情況有兩種解決辦法
- 直接在堆中分配
- 廢棄當(dāng)前TLAB,重新申請TLAB空間再次進(jìn)行內(nèi)存分配
其實(shí)兩種各有利弊,第一種的缺點(diǎn)就是存在一種極端情況,TLAB只剩下1KB,就會導(dǎo)致后續(xù)的分配可能大多數(shù)對象都需要直接在堆中分配;第二種的就是可能會出現(xiàn)頻繁的廢棄TLAB、頻繁申請TLAB的情況
為了解決這兩個(gè)方案存在的問題,虛擬機(jī)定義了一個(gè)refill_waste的值,這個(gè)值可以翻譯為“最大浪費(fèi)空間”。當(dāng)請求分配的內(nèi)存大于refill_waste的時(shí)候,會選擇在堆內(nèi)存中分配。若小于refill_waste值,則會廢棄當(dāng)前TLAB,重新創(chuàng)建TLAB進(jìn)行對象內(nèi)存分配
那你剛剛說的,幾乎所有對象實(shí)例都存儲在這里,是還有例外嗎?能詳細(xì)解釋下嗎?
是的,親愛的面試官,Java對象實(shí)例和數(shù)組元素不一定都是在堆上分配內(nèi)存,滿足特定的條件的時(shí)候,它們可以在棧上分配內(nèi)存
面試官微微一笑,什么情況呢?
親愛的面試官,是這樣子的,JVM中的Java JIT編譯器有兩個(gè)優(yōu)化,叫做逃逸分析和標(biāo)量替換;
逃逸分析,聽著有點(diǎn)意思,逃,誰逃,什么時(shí)候逃,往哪里逃?
中文維基上對逃逸分析的描述挺準(zhǔn)確的,摘錄如下:
在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動態(tài)范圍的方法——分析在程序的哪些地方可以訪問到指針。當(dāng)一個(gè)變量(或?qū)ο?在子程序中被分配時(shí),一個(gè)指向變量的指針可能逃逸到其它執(zhí)行線程中,或是返回到調(diào)用者子程序。
大魚白話文版本:
一個(gè)子程序分配了一個(gè)對象并且返回了該對象的指針,那么這個(gè)對象在整個(gè)程序中被訪問的地方無法確定,任何調(diào)用這個(gè)子程序的都可以拿到這個(gè)對象的位置,并且調(diào)用這個(gè)對象,遂,對象逃之;
若指針存儲在全局變量或者其它數(shù)據(jù)結(jié)構(gòu)中,全局變量也可以在子程序之外被訪問到,遂,對象逃之;
若未逃之,則可將方法變量和對象分配到棧上,方法執(zhí)行完之后自動銷毀,不需要垃圾回收的介入,提高系統(tǒng)的性能
簡潔版:
逃逸分析通過分析對象引用的作用域,來決定對象的分配地方(堆 or 棧)
我們一起來看個(gè)例子
- public StringBuilder getBuilder1(String a, String b) {
- StringBuilder builder = new StringBuilder(a);
- builder.append(b);
- // builder通過方法返回值逃逸到外部
- return builder;
- }
- public String getBuilder2(String a, String b) {
- StringBuilder builder = new StringBuilder(a);
- builder.append(b);
- // builder范圍維持在方法內(nèi)部,未逃逸
- return builder.toString();
getBuilder1中的builder對象會通過方法返回值逃逸到方法的外部,而反觀getBuilder2中的builder對象則不會溢出去,作用域只會在方法內(nèi)部,toString方法會new一個(gè)String用來返回,所以沒有逃逸
如果把堆內(nèi)存限制得小一點(diǎn)(比如加上-Xms10m -Xmx10m),關(guān)閉逃逸分析還會造成頻繁的GC,開啟逃逸分析就沒有這種情況,說明逃逸分析確實(shí)降低了堆內(nèi)存的壓力
逃逸分析了之后,就可以直接降低堆內(nèi)存的壓力嗎?(你剛剛說的那個(gè)標(biāo)量替換是什么)
但是,逃逸分析只是棧上內(nèi)存分配的前提,接下來還需要進(jìn)行標(biāo)量替換才能真正實(shí)現(xiàn)。標(biāo)量替換用話不太好說明,直接來看例子吧,形象生動
- public static void main(String[] args) throws Exception {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 10000; i++) {
- allocate();
- }
- System.out.println((System.currentTimeMillis() - start) + " ms");
- Thread.sleep(10000);
- }
- public static void allocate() {
- MyObject myObject = new MyObject(2019, 2019.0);
- }
- public static class MyObject {
- int a;
- double b;
- MyObject(int a, double b) {
- this.a = a;
- this.b = b;
- }
- }
標(biāo)量,就是指JVM中無法再細(xì)分的數(shù)據(jù),比如int、long、reference等。相對地,能夠再細(xì)分的數(shù)據(jù)叫做聚合量
Java虛擬機(jī)中的原始數(shù)據(jù)類型(int,long等數(shù)值類型以及reference類型等)都不能再進(jìn)一步分解,它們就可以稱為標(biāo)量。相對的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它稱為聚合量,Java中最典型的聚合量是對象
如果逃逸分析證明一個(gè)對象不會被外部訪問,并且這個(gè)對象是可分解的,那程序真正執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來代替。拆散后的變量便可以被單獨(dú)分析與優(yōu)化,可以各自分別在棧幀或寄存器上分配空間,原本的對象就無需整體分配空間了
仍然考慮上面的例子,MyObject就是一個(gè)聚合量,因?yàn)樗蓛蓚€(gè)標(biāo)量a、b組成。通過逃逸分析,JVM會發(fā)現(xiàn)myObject沒有逃逸出allocate()方法的作用域,標(biāo)量替換過程就會將myObject直接拆解成a和b,也就是變成了:
- static void allocate() {
- int a = 2019;
- double b = 2019.0;
- }
可見,對象的分配完全被消滅了,而int、double都是基本數(shù)據(jù)類型,直接在棧上分配就可以了。所以,在對象不逃逸出作用域并且能夠分解為純標(biāo)量表示時(shí),對象就可以在棧上分配
除了這些之后,你還知道哪些優(yōu)化嗎?
emmm,先思索一下(即使知道,也要稍加思考!
除此之外,JVM還有一個(gè)同步消除(鎖消除):鎖消除是Java虛擬機(jī)在JIT編譯是,通過對運(yùn)行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節(jié)省毫無意義的請求鎖時(shí)間。
鎖消除基于分析逃逸基礎(chǔ)之上,開啟鎖消除必須開啟逃逸分析
線程同步本身比較耗,如果確定一個(gè)對象不會逃逸出線程,無法被其它線程訪問到,那該對象的讀寫就不會存在競爭,對這個(gè)變量的同步措施就可以消除掉。單線程中是沒有鎖競爭。(鎖和鎖塊內(nèi)的對象不會逃逸出線程就可以把這個(gè)同步塊取消)
- public synchronized String append(String str1, String str2) {
- StringBuffer sBuf = new StringBuffer();
- // append方法是同步操作
- sBuf.append(str1);
- sBuf.append(str2);
- return sBuf.toString();
- }
從源碼中可以看出,append方法用了synchronized關(guān)鍵詞,它是線程安全的。但我們可能僅在線程內(nèi)部把StringBuffer當(dāng)作局部變量使用
這時(shí)我們可以通過編譯器將其優(yōu)化,將鎖消除,前提是java必須運(yùn)行在server模式,server模式會比client模式作更多的優(yōu)化,同時(shí)必須開啟逃逸分析
說一說剛剛說的這些的參數(shù)嗎
我個(gè)乖乖兔,這我哪記得,不過得虧我昨天剛讀了大魚的文章,順便學(xué)習(xí)了下
逃逸分析:-XX:+DoEscapeAnalysis開啟逃逸分析(jdk1.8默認(rèn)開啟,其它版本未測試);-XX:-DoEscapeAnalysis 關(guān)閉逃逸分析
同步消除:-XX:+EliminateLocks開啟鎖消除(jdk1.8默認(rèn)開啟,其它版本未測試);-XX:-EliminateLocks 關(guān)閉鎖消除
標(biāo)量替換:-XX:+EliminateAllocations開啟標(biāo)量替換(jdk1.8默認(rèn)開啟,其它版本未測試);-XX:-EliminateAllocations 關(guān)閉標(biāo)量替換
那你平時(shí)是用哪些參數(shù)優(yōu)化內(nèi)存的?
一般我個(gè)人接觸到的有兩類參數(shù):內(nèi)存調(diào)整參數(shù)、垃圾收集器調(diào)整參數(shù)
內(nèi)存調(diào)整參數(shù):-Xmx堆內(nèi)存最大值;-Xms堆內(nèi)存最小值;-Xmn堆新生代的大小;-Xss設(shè)置線程棧的大小;-XX:NewRatio指定堆中的老年代和新生代的大小比例, 不過使用CMS收集器的時(shí)候這個(gè)參數(shù)會失效
關(guān)于方法區(qū)的參數(shù),在JDK8之前,用-XX:PermSize和-XX:MaxPermSize來分別設(shè)置方法區(qū)的最小值和最大值;JDK8以及之后不再使用這個(gè)參數(shù)來設(shè)置方法區(qū)了,改為-XX:MeatspaceSize和-XX:MaxMetaspaceSize來設(shè)置方法區(qū)的大小了,Max參數(shù)主要就是防止某些情況導(dǎo)致Metaspace無限的使用本地內(nèi)存,若超過設(shè)定值就會觸發(fā)Full GC,所以需要根據(jù)系統(tǒng)內(nèi)存大小來動態(tài)的改變此值
垃圾收集器的調(diào)整參數(shù)我就不舉例子了,垃圾收集器調(diào)整參數(shù)就是設(shè)置JVM的垃圾收集器或者調(diào)整收集器的一些優(yōu)化參數(shù),說實(shí)話大魚也不沒那么了解,這種參數(shù)我一般都是用到的時(shí)候去查資料,也沒啥必要了解那么細(xì),專業(yè)人員除外
你剛剛說了堆內(nèi)存中有個(gè)8:1:1,出于什么考慮這樣設(shè)計(jì)的呢
有的對象朝生夕死,有的對象可能會活很久很久,有的對象很小,有的對象可能會很大,每個(gè)對象的特點(diǎn)不一樣,分配的堆內(nèi)存地方不一樣,也就對應(yīng)著不同的回收策略以及垃圾回收器,年輕代就是存放那種使用完就立馬回收的對象,而老年代則用來存放那些長期駐留在內(nèi)存中的對象
其實(shí)說白了,就是根據(jù)多種對象的特點(diǎn)來設(shè)計(jì)出多種了回收策略,而對于整塊內(nèi)存使用一種回收策略是不友好的,所以根據(jù)對象的特點(diǎn)來將堆內(nèi)存拆分開,然后對于每塊內(nèi)存采用不同的回收策略
虛擬機(jī)棧和本地內(nèi)存棧
Java虛擬機(jī)棧屬于線程私有的,生命周期和線程相同;虛擬機(jī)棧是Java方法執(zhí)行的內(nèi)存模型,描述的方法的執(zhí)行過程;每個(gè)方法被執(zhí)行的時(shí)候都會同時(shí)創(chuàng)建一個(gè)棧幀結(jié)構(gòu),用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息,棧里面會包含很多的
棧幀,可以認(rèn)為每一個(gè)方法的調(diào)用直到執(zhí)行完成對應(yīng)這一個(gè)棧幀的入棧和出棧的過程
虛擬機(jī)棧的棧幀里面都包含什么呢?
主要是包含局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口這些,接著我們來看下每一部分的作用
這些大家不需要死記硬背的哦,需要大家理解記憶,最重要的是理解每一部分的作用,下面可能第一次接觸的會比較枯燥,keep
局部變量表:存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用(reference類型,它不等同于對象本身,它可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能指向一個(gè)代表對象的句柄或者其他與此對象相關(guān)的位置)。
操作數(shù)棧:一個(gè)后進(jìn)先出的操作數(shù)棧,主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲空間。
操作數(shù)棧就是JVM執(zhí)行引擎的一個(gè)工作區(qū),當(dāng)一個(gè)方法剛開始執(zhí)行的時(shí)候,一個(gè)新的棧幀也會隨之被創(chuàng)建出來,這個(gè)方法的操作數(shù)棧是空的。每一個(gè)操作數(shù)棧都會擁有一個(gè)明確的棧深度用于存儲數(shù)值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為maxstack的值。
動態(tài)鏈接:在Java源文件被編譯到字節(jié)碼文件中時(shí),所有的變量和方法引用都作為符號引用(symbolic Reference)保存在class文件的常量池里。比如:描述一個(gè)方法調(diào)用了另外的其他方法時(shí),就是通過常量池中指向方法的符號引用來表示的,那么動態(tài)鏈接的作用就是為了將這些符號引用轉(zhuǎn)換為調(diào)用方法的直接引用。
方法出口:存放調(diào)用該方法的pc寄存器的值。一個(gè)方法的結(jié)束,有兩種方式:正常執(zhí)行完成、出現(xiàn)未處理的異常,非正常退出
無論通過哪種方式退出,在方法退出后都返回到該方法被調(diào)用的位置。方法正常退出時(shí),調(diào)用者的pc計(jì)數(shù)器的值作為返回地址,即調(diào)用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
棧的深度問題
在Java虛擬機(jī)規(guī)范中,對這個(gè)區(qū)域規(guī)定了兩種異常狀況:
如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長度的虛擬機(jī)棧),當(dāng)擴(kuò)展時(shí)無法申請到足夠的內(nèi)存時(shí)會拋出OutOfMemoryError異常。
那本地方法棧是干什么的?
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)。
虛擬機(jī)規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(譬如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
Java虛擬機(jī)棧于管理Java方法的調(diào)用,而本地方法棧(Native Method Stack)用于管理本地方法的調(diào)用。本地方法棧,也是線程私有的。
方法區(qū)
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)在原來被習(xí)慣性的稱之為永久代,但是在JDK1.8中永久代已經(jīng)不存在了,存儲的類信息、編譯之后的代碼數(shù)據(jù)都移到了元空間,而元空間并沒有在堆中,而是直接占用的本地內(nèi)存
元空間和永久代本質(zhì)是類似的,其實(shí)都是對JVM規(guī)范中的方法區(qū)的實(shí)現(xiàn),元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制
程序計(jì)數(shù)器
程序計(jì)數(shù)器啊,聽名字其實(shí)就知道了,主要作用就是計(jì)數(shù)的,但是這里的計(jì)數(shù)并不是計(jì)算數(shù)量,而是記下一條的字節(jié)碼指令
程序計(jì)數(shù)器占一小塊內(nèi)存空間,就是當(dāng)前線程的執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
那程序計(jì)數(shù)器是線程私有還是公有?
相信聰明的養(yǎng)魚仔肯定已經(jīng)猜到了,當(dāng)然是私有的嘞
Java虛擬機(jī)多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對于多核處理器來說是一個(gè)內(nèi)核)只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲,我們稱這類內(nèi)存區(qū)域?yàn)?ldquo;線程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,如果正在執(zhí)行的是Natvie方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
我愛總結(jié)
好了,今天就先聊到這了,天也不早了,你早點(diǎn)回家休息吧,好好準(zhǔn)備準(zhǔn)備明天下午來繼續(xù)下一輪面試吧
好的,尊敬的面試官(逃
回到家之后我就拿出我的小本本一頓總結(jié),跟著大魚一起來看看吧,養(yǎng)魚仔們
- 堆:線程共享,主要用于分配實(shí)例對象,但由于逃逸分析的存在也不是完全在堆上分配,可能在棧上分配;逃逸分析是個(gè)基礎(chǔ),標(biāo)量替換和鎖消除正是基礎(chǔ)逃逸分析的優(yōu)化;堆中還有個(gè)TLAB分配,屬于線程私有,但又不是完全意義上的私有
- 棧:線程私有,虛擬機(jī)棧主要是用于Java方法的執(zhí)行,每個(gè)棧幀對應(yīng)一個(gè)方法的入棧和出棧,包含局部變量、操作數(shù)棧、動態(tài)鏈接和方法出口這些;本地方法棧則是用于執(zhí)行本地方法的
- 方法區(qū):線程共享,存放加載的類信息、常量、靜態(tài)變量以及即時(shí)編譯器編譯之后的代碼
- 程序計(jì)數(shù)器:線程私有,存放每個(gè)線程接下來要執(zhí)行的指令