漫畫:什么是 volatile 關鍵字?
621
2025-04-01
哈嘍!大家好,我是小奇,一位不靠譜的程序員
小奇打算以輕松幽默的對話方式來分享一些技術,如果你覺得通過小奇的文章學到了東西,那就給小奇一個贊吧
文章持續更新,可以微信搜索【小奇JAVA面試】第一時間閱讀,回復【資料】更有我為大家準備的福利喲!
前言
作為一名Java程序員,Java虛擬機是我們不必學會就可以搬磚工作的一種技能點,但是小奇為什么還要講一下呢?難道就是為了浪費大家1分鐘的寶貴時間,一個人1分鐘,50萬人就是1年,5000萬人就是100年,賺了,小奇以一己之力成功搞掛一個人(血賺)。
當然不是,并且小奇的文章也沒有那么多人看,最多也就浪費個腎吧。
學習Java虛擬機是因為面試官要問啊!,所以我們就要學,什么?不實用的你不學?那鄰居小奇可要使勁學啦,到時候面試官只要小奇不要你。
至于你問為什么面試官要問Java虛擬機呢,這個。。。我把這次機會留給你,下次你面試的時候面試官問:“講一下Java虛擬機的內存模型”。你:“面試官你好,請問為什么你要問Java虛擬機呢,你給我臺電腦,我五分鐘給你搭建好圖書管理系統他不香嗎,咱們鍵盤上見真章”。這時面試官就會告訴你答案,你就可以把答案打在評論區,讓小奇以及眾多小伙伴一起知道一下到底為什么要問?
面試
在一個晴朗的周日,我來到了一個陌生的園區(別問為什么是周日,問就是997,不過為了填飽肚子的打工人,只能明知山有虎、偏向虎山行),坐在陌生的會議室,等待HR小姐姐去叫面試官,此時我的心情和各位小伙伴一樣五味雜陳,擔心面試官問的會不會很難?問到我的知識盲區我該怎么辦?
一會自我介紹的時候要不要吹一下我和小奇的關系?
一位英俊瀟灑,眼神犀利的面試官走了進來,看到他那犀利、仿佛能看穿一切的眼神 ,我在想要不然一會就不要20k了,要8k得了,這個面試官一看就不好糊弄啊,但是我想起來我來之前剛看了小奇的趣學編程系列,我已經完全學會了小奇的精髓,我頓時就來了底氣,決定一會要30k,不給就學小奇賴著不走(哈哈)
面試官:小奇是吧,帶簡歷了嗎?
我:沒帶,現在彩印兩塊一張,我簡歷五張,每次面試都要花費十塊,我朋友說了還沒工作就先讓你掏錢的工作不要去。
面試官:。。。那你靠什么來征服我,讓我錄用你
我:氣質?
(此時面試官并沒有叫保安,而是從門后拿出了恭候我多時的棍子,我瞬間慫了)
(我只好從我的雙肩包中拿出了我從上午沒有面試通過的其他公司面試官手中要回的簡歷,上午的情形是這樣的,上午的面試官:今天的面試就到這吧,回去等通知吧!我:面試官你好,如果貴公司不打算錄取我的話,能不能把我的紙質簡歷還給我,我下午還有一家面試。上午的面試官:我說你的簡歷怎么皺皺巴巴,原來你一直在循環利用??!這個癥狀出現多久了?我:半拉月了。。。)
(當我把皺皺巴巴的簡歷交給面試官后,這場面試才得以繼續進行。。。)
Java虛擬機內存模型
面試官:我看你簡歷上寫的精通Java虛擬機?(哼,面試官輕蔑的一笑)
(此時我的內心非常緊張,緊張的并不是面試官把我問住,而是我如果虛擬機這方面回答的太專業了面試官聽不懂怎么辦,他如果不信我回答的怎么辦,此時我偷偷看了一下我藏在桌下的《深入理解Java虛擬機》,如果他不信我就拿出書來和他對峙)
我:也不算精通吧,都是同行們抬愛。
面試官:那你說一下JVM虛擬機的內存模型吧
我:JVM虛擬機中有一個運行時數據區,里面主要分為程序計數器、虛擬機棧、本地方法棧、堆、方法區
面試官:嗯。假如我們new一個對象這個時候是放在哪里?
我:堆里
面試官:嗯。假如我們int定義一個變量number放在哪里?
我:棧里
面試官:嗯。小伙子真是惜字如金啊,能不能詳細介紹一下這幾個區域都是干什么的嗎
我:那我就獻丑了。。。
1.程序計數器:簡單來說每一個線程在執行代碼的時候執行到哪一行是有一個記錄的,比如線程A執行到代碼第10行了這個時候在線程A中是有一個程序計數器來記錄10這一行。程序計數器在線程中是私有的。那么他有什么好處呢?雖然我們開發的時候可以使用多線程來開發,但是CPU在執行A線程的時候B線程就需要等待,等到CPU去執行A線程的時候B線程又需要等待了,所以說如果這個時候CPU去執行B線程,那么執行完后再回來執行A線程的時候就知道之前執行到哪一行了,可以從這一行接著執行。
2.虛擬機棧:與程序計數器一樣,虛擬機棧也是線程私有的,虛擬機是棧是存放執行方法的時候用到的一些信息,例如在執行方法的時候虛擬機就會創建一個棧幀用于存儲局部變量表(表里是局部變量)、操作數棧(如果要進行一些數的計算,那么會把數先讀取到操作數棧中進行操作最后賦值到局部變量表中)、動態鏈接、方法出口等信息。
3.堆:堆是線程共享的,堆是虛擬機所管理的內存中最大的一塊,一般優化就是優化這塊內存,比如我們Student st = new Student();那么我們新創建出來的對象就在堆內存里。
4.本地方法棧:本地方法棧是用來執行本地方法的時候所使用的,例如Java中我們會看到很多Native方法,這些方法使用例如c語言寫的,Java中只是調用。
5.方法區:方法區是線程共享的,它用于存儲已被虛擬機加載的類型信息、常量、靜態變量等。在方法區中還包含一個運行時常量池部分,這一部分用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中,所謂符號引用其實就是將一個例如main方法這個方法引用轉化為指針應用,可以更加快速的找到這個方法在磁盤中的真正位置
面試官:嗯??梢?,回答的很全面,平時都怎么學習提高呢?
我:看小奇的文章(此時真想給小奇的文章一個贊)
面試官:說一下對象創建的流程吧
我:(還想歇會喝口水呢,這么快就問下一個知識點了,我偷偷翻書看一下。。。)
當虛擬機接收到new Student()的命令后,他會先去常量池中查看這個Student()類是否有相應的符號引用,并且這個類是否被加載、解析、初始化過,如果沒有的話需要先進行類的加載、解析、初始化。
面試官:嗯。那我創建對象的時候怎么給它分配內存呢,有哪些方法?
我:可以使用指針碰撞、空閑列表等方式。
指針碰撞:假如堆空間現在沒有數據,并且堆空間是一個方形的空間,那么我們用一個指針放在起始位置,也就是緊挨著邊,這個時候有一個占用1M的對象要創建了,那么我們的指針就從初始位置開始從左向右走1M的距離,這個時候又有一個10M的對象要創建了,我們的指針從當前位置又向右走了10M的距離,這個時候有一個1G的對象來了(指針:我淦。。。),指針向右走到頭了也沒有1G的距離,這個時候就創建不了這個1G的對象了。
空閑列表:指針碰撞的方式適用于堆空間連續的這種方式,如果不連續的話就不能從左到右來分配空間了,這個時候就需要用到空閑列表了,使用一個空閑列表來記錄哪些空間是空閑的,新創建一個對象就放到那里去。
面試官:嗯。那我們剛剛創建了一個對象, 你能說一下對象里又是怎樣的一個內存布局嗎?
我:(真是往祖墳里挖啊。。。偷偷看看書,淦)
對象里可以劃分為三個部分:對象頭、實例數據、對齊填充。
面試官:額。。??梢栽敿氄归_了說一下嗎,你這樣別人也能答出來
我:(就看了一眼書,記不住那么多啊,算了硬著頭皮來吧)
對象頭:對象頭中存儲了對象自身運行時的數據,如哈希碼、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳以及指向它的類型元數據的指針(通過這個指針來確定對象是哪個類的實例)。
實例數據:這一部分是我們真正在對象中定義的信息,比如對象中的一些字段等內容,還有繼承父類的一些內容和在子類中定義的字段都在此記錄。
對齊填充:這一部分并不是每個對象都存在的,因為虛擬機要求對象的起始地址必須是8字節的整數倍,假如我們實例數據只有4字節,那么我們需要另外填充4字節的數據來保證對象的起始位置是8字節的整數倍。.
垃圾收集器與內存分配策略
面試官:嗯。說一下虛擬機怎么判斷一個對象是否是垃圾對象
我:(這么快又換下一個知識點了,啥時候能喝口水呢。。。)
可以采用可達性分析算法和引用計數算法來判斷對象是否是垃圾對象。
面試官:嗯。繼續說下去
我:
可達性分析算法:會從一個“GC Root”根開始依據引用關系向下搜索,如果不能搜索到的證明是垃圾對象。
引用計數算法:當一個對象被引用的時候就會在這個對象中的引用計數器中加1,如果引用失效時,計數器的值就會減1,當這個對象的引用計數器為0的時候就證明這個對象是垃圾對象,不過這種算法有一個缺點,就是兩個對象之間相互引用的時候就會認為兩個對象都不是垃圾對象,但是這兩個對象是因為循環依賴造成的問題,理應被清理掉,但是這種算法解決不了這種循環引用的問題。
面試官:嗯。有哪些垃圾收集算法呢?
我:(二分法、三分法、四。。。不對,怎么感覺背串了,還是不編了,偷偷看一下書吧)
標記-清除算法、標記-復制算法、標記-整理算法
面試官:嗯??梢栽僭敿毜恼f一下算法的具體內容嗎?
我:
標記-清除算法:此算法主要用于一塊內存區的垃圾收集器,在標記后直接做清除操作,不會再做后續的操作。
標記-復制算法:此算法主要用于兩塊內存區的垃圾收集器,將存活對象標記,然后將存活對象放入保留區域中,然后將之前的一塊區域全部清理掉作為下一次的保留區域。
標記-整理算法:此算法主要用于一塊內存區的垃圾收集器,他與標記清除算法的區別在于他清除后會將內存區域中存活的對象重新整理到一起,使得剩下的空間可以連續起來。
面試官:嗯??梢哉f一下都有哪些垃圾收集器嗎?
我:(三V肉、爬牛、爬V肉死砍胃汁、三V肉偶得、爬V肉偶得、CMS、G1、ZGC,我想了想還是畫出來吧,畢竟我的英語水平讀出來面試官可能會懷疑人生)
1、Serial收集器(三V肉)
Serial收集器是最基礎、歷史最悠久的收集器,這個收集器是一個單線程工作的收集器。
2、ParNew收集器(爬牛)
ParNew收集器實質上是Serial收集器的多線程并行版本,可以同時使用多條線程進行垃圾收集。
3、Parallel Scavenge 收集器(爬V肉死砍胃汁)
Parallel Scavenge收集器是一款新生代收集器,它是基于標記-復制算法實現的收集器。Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量,所謂吞吐量就是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值,即。
4、Serial Old收集器(三V肉偶得)
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器。
5、Parallel Old收集器(爬V肉偶得)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多線程并行收集,基于標記-整理算法實現。
6、CMS收集器
CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,它的運作過程分為四個步驟,包括:
1> 初始標記
2> 并發標記
3> 重新標記
4> 并發清除
初始標記:初始標記需要stw,初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。
并發標記:并發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發運行。
重新標記:重新標記階段則是為了修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短。
并發清除:這個階段清理刪除掉標記階段判斷的已經死亡的對象,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發的。
6、Garbage First 收集器(簡稱G1收集器)
G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。如圖。
G1收集器的運作過程大致可以劃分為以下四個步驟:
初始標記:僅僅只是標記一下GC Roots能直接關聯到的對象,
并發標記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發執行。
最終標記:對用戶線程做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄。
篩選回收:負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。這里的操作設計存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
面試官:嗯??梢哉f一下JVM堆內存模型嗎?
我:
堆內存模型分為年輕代和老年代,其中年輕代中又分為Eden區和Survivor區,Survivor區又分為S0和S1區。
面試官:嗯??梢哉f一下對象在堆中的流轉與回收策略嗎?
我:(這可有點多了。。。說了不知道你能不能聽得懂啊,算了,不行就拿出我藏在桌下的書給你講吧)
對象優先在Eden分配:new一個對象首先會放到Eden區中,當Eden區域放滿了后,會將Eden區域中存活的對象放入到Survivor區中的S0區域,然后將Eden區域清空,這個時候新new的對象還是放入Eden區域中,當Eden區域中再次滿了的話就將Eden區域中的存活對象和S0中的存活對象都拿出來放入到S1區域中,并將Eden區域和S0區域中都清理掉,當Eden區域再次滿了就向Eden區域中的存活對象和S1中的存活對象一起放入到S0中,也就是循環將Eden區域中的存活對象和Survivor中的其中一塊區域中的存活對象一塊拿出來放入到Survivor中的另外一塊區域中,如此循環,每循環一次對象的GC年齡加1,當GC年齡到達15的時候就會移入到老年代。
放入老年代的時候的GC我們可以稱他為輕GC,這個GC的時間比較短,當老年代滿了的時候會進行重GC,這個GC的時間比較長。
長期存活的對象進入老年代:就是對象在年輕代來回循環,到達15次(默認,這個數值可以設置),就會將對象放入老年代。
大對象直接進入老年代:當新創建的對象比較大的時候我們可以直接將他放入老年代,這樣可以避免在年輕代來回復制造成的額外開銷,具體多大的對象是大對象我們可以根據 -XX:PretenureSizeThreshold參數來設置。
動態對象年齡判定:如果在Survivor空間中低于或等于某年齡的所有對象大小的總和大于Survivor空間的一半,那么年齡大于或等于該年齡的對象就可以直接進入老年代,無需等到年齡達到15,假如現在最大的對象年齡為10,但是Survivor空間以及使用一半了,如果再往下走可能還沒有對象達到15就造成Survivor區域滿了,所以就提前將大年齡的對象放入老年代了。
空間分配擔保:在發生輕GC之前,虛擬機就會先檢查老年代可用的空間是否大于新生代所有對象的總空間,如果大于,即便新生代所有的對象都不是垃圾對象,那么老年代也放的下,如果不大于呢?虛擬機會先查看是否設置了允許擔保失敗的參數,如果允許,虛擬機會判斷老年代的剩余空間是否大于歷次從新生代到老年代里的對象的平均大小。
如果大于就會進行輕GC將新生代的存活對象放入老年代,這一次是冒險的,因為有可能這一次輕GC比之前輕GC的平均值存活的要多,這樣會造成老年代內存直接溢出。
如果小于就會先進行一次重GC將老年代的空間騰出來,保證可以將年輕代的存活對象放進去。
如果配置的參數是不允許擔保失敗,那么我們每一次到達老年代剩余的空間不夠新生代所有對象的總空間的時候我們就會進行一次重GC將老年代的空間先騰出來。
面試官:可以啊,小伙子有點東西
我:還行吧(有點東西你不給我倒點水喝。。。)
虛擬機性能監控、故障處理工具
面試官:剛才都是一些概念性的東西,現在問你點實操的,說一下有哪些虛擬機性能監控方法呢?
我:(剛想歇一會。。。早知道簡歷上寫精通Java虛擬機會被這么問我就只寫了解Java虛擬機了,哎。。。)
jps:虛擬機進程狀態工具
命令格式:jps 【options】【hostid】
jps -l命令可以查看主類全名,如果進程執行的是jar包,則輸出jar路徑
jps -v命令可以查看虛擬機進程啟動時的JVM參數
jstat:虛擬機統計信息監視工具
命令格式:jstat 【options】【hostid】【ms】【count】
參數ms和count代表查詢間隔和次數,如果省略了這個2個參數,說明只查詢一次,假設現在我們要查詢66320的垃圾收集情況,250毫秒查詢一次,一共查詢20次
jstat -gc 66320 250 20
jinfo:Java配置信息工具
jinfo的作用是實時查看和調整虛擬機各項參數。
jmap:Java內存映像工具
jmap命令用于生成堆轉儲快照。
jhat:虛擬機堆轉儲快照分析工具
jhat命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。
jstack:Java堆棧跟蹤工具
jstack命令用于生成虛擬機當前時刻的線程快照。
面試官:你這些都是命令,有哪些可視化的虛擬機故障處理工具嗎?
我:可以使用jdk自帶的VisualVM可視化工具
命令:jvisualvm
面試官:小伙子真厲害啊,我這邊沒有什么要問的了,你還有什么問題要問(面試官兩眼放光)
我:額。。。面試官這個我的紙質簡歷可以給我嗎,可以不往我的簡歷上寫寫畫畫嗎,我明天的面試還要用。
面試官:還面啥別的公司啊,就來我這吧,條件隨便開
我:那就100k吧(此時面試官又拿起了他準備好的棍子)
面試官:你要是不來就給我推薦一下,讓別人來我這面試一下
我:(此時我把我的深入理解Java虛擬機的書遞給了面試官,并告訴他)你先好好學習一下Java虛擬機吧,今天幸虧只是我來了,如果是小奇的忠實讀者來了,你將會被虐的很慘的。(我轉身只留下了帥氣的背影)
總結
Java虛擬機是即基礎又有點深奧的東西,所以大家要后認真反復的去學習,如果覺得我的文章還不錯的話就點個贊吧,另外可以微信搜索【小奇JAVA面試】閱讀更多的好文章,獲取我為大家準備的資料。
Java 虛擬化
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。