前言
JVM可以說是下到應屆生拯腮,上到高級開發(fā)都是面試必考的知識,只是深淺的不同罷了蚁飒,但是百變不離其中动壤,了解JVM的基本原理與作用,大部分的面試題也能對答如流淮逻。為避免閱讀疲勞琼懊,該系列會將JVM分為兩個部分,第一部分主要介紹JVM的內(nèi)存劃分以及類加載過程爬早,第二部分則是主要講解GC以及JMM相關(guān)問題哼丈。
本系列章節(jié)面向的用戶群體主要是應付面試,因此本文章不會深入講解相關(guān)底層原理筛严,主要以簡單理解的方式為讀者提供應對面試的學習思路醉旦,方便讀者快速理清JVM面試相關(guān)知識,輕松走過面試第一關(guān)桨啃。
我們通過以下常見的JVM面試問題作為學習的入口:
1. JVM運行時數(shù)據(jù)區(qū)的內(nèi)存怎么進行劃分车胡?
2. JVM的類加載是怎么樣的?
3. 如何針對JVM內(nèi)存參數(shù)進行調(diào)優(yōu)优幸?
以上三個問題可以說是jvm面試的基本問題吨拍,接下來我會以總結(jié)式的方式進行問題的解答,讓讀者快速上手這類問題网杆。
1. JVM運行時數(shù)據(jù)區(qū)
其實JVM運行時數(shù)據(jù)區(qū)可以通過分類進行快速記憶羹饰,其中可以分為線程共享以及線程私有:
1. 線程共享的區(qū)域有:堆(感覺只要是學java的這個不可能答不出來)、方法區(qū)
2. 線程私有的區(qū)域有:虛擬機棧(java方法)碳却、本地方法棧(c/c++方法)队秩、程序計數(shù)器
大體可以通過下圖進行一個大概的了解:
上圖已經(jīng)很好的詮釋了相應的內(nèi)存劃分,以及某些數(shù)據(jù)具體存儲在哪昼浦,接下來我們根據(jù)這個圖進行一個簡單的總結(jié):
程序計數(shù)器是占用空間最小的內(nèi)存區(qū)域馍资,不會出現(xiàn)OOM,主要記錄當前線程正在執(zhí)行的方法的指令地址关噪,方便線程切換后能恢復到下一條指令的位置鸟蟹,如果是執(zhí)行native方法乌妙,則該計數(shù)器為空。(由于這塊老是被遺忘建钥,寫在第一處讓讀者記憶深刻點)
-
虛擬機棧是用來存放棧幀的藤韵,說白了棧幀就是方法運行時數(shù)據(jù)存儲的空間,這棧幀主要由四個部分組成:
Ⅰ. 局部變量熊经,方法體里面包含的變量泽艘,如基本變量 int , float 等以及對象的引用如
Student s
Ⅱ. 操作數(shù)棧,對數(shù)據(jù)的運算需要一個棧來進行镐依,可以理解為自己開發(fā)一個算術(shù)運算時使用的棧結(jié)構(gòu)匹涮,如果有數(shù)據(jù)進行計算的時候會將數(shù)據(jù)放入,然后計算得出的結(jié)果也會存到這里面
Ⅲ. 動態(tài)鏈接槐壳,存儲了指向class常量池的方法符號引用(一個字符串)然低,說白了這個東西就是查找方法具體位置的一種懶加載機制。在第一次運行會通過這個字符串找到相應方法所在位置务唐,之后將這個位置替換掉符號引用脚翘,變?yōu)橹苯右谩?/p>
Ⅳ. 返回地址,這個應該很好理解绍哎,我得知道方法運行完后要回去哪,這個地址便是指向進入這個方法時候的地址鞋真,方便返回
本地方法棧崇堰,與虛擬機棧差不多,只不過運行的是native(c/c++)方法
堆涩咖,存儲對象以及數(shù)組的區(qū)域海诲,基本上是jvm內(nèi)存里面最大的區(qū)域。
PS:如果方法里面的一個對象不存在被外界調(diào)用檩互,那么jvm可能會將這個對象存放在棧里面特幔,不過目前怎么判斷一個對象不怎么被外界調(diào)用(即逃逸分析)不太成熟,知道這個的人也不多(面試就遇到一回問這個)闸昨。
- 方法區(qū)蚯斯,主要存儲虛擬機加載的類信息、常量饵较、靜態(tài)變量拍嵌,例如上面說的符號引用也存在這。
PS: 讀者對于方法區(qū)最大的疑惑可能就是jdk1.8后的變化循诉,其實jdk1.8前方法區(qū)屬于堆的一個子集横辆,只不過1.8之后變成了和堆的并交(如圖中所示),其中類的元信息存儲在直接內(nèi)存茄猫,而常量池狈蚤、靜態(tài)變量的引用對象還是存在堆中困肩。
最后總結(jié)一下,JVM每遇到一個線程脆侮,就為其分配一個程序計數(shù)器锌畸,虛擬機棧和本地方法棧,棧里面指向的對象一般都在堆中他嚷,線程終止的時候便會清空這些內(nèi)存 ( 以上都是線程私有蹋绽,因此和線程共存亡 ) ,而堆中的對象則根據(jù)可達性分析算法進行生死判斷筋蓖,這個會在下一章節(jié)進行講解卸耘。
2. JVM的類加載流程
廢話不多說,大體流程看圖:
讓我們來一句話總結(jié)一下類的加載過程:將 Class 文件的字節(jié)流轉(zhuǎn)換為方法區(qū)中的 Class 對象(加載)粘咖,檢查該 Class 文件的字節(jié)流是否規(guī)范蚣抗、合法(驗證),為該 Class 的類變量( static )進行空間分配(準備)瓮下,對該 Class 中的符號引用轉(zhuǎn)換為直接引用(解析)翰铡,最后執(zhí)行用戶編寫的構(gòu)造器代碼(初始化)
PS : 其中準備階段和解析階段有些小細節(jié),準備階段如果是類常量( static final )讽坏,則會直接進行賦值操作锭魔,否則賦予初始值( int 就賦值0,對象就賦值 null 等)路呜,在初始化階段才會進行具體賦值迷捧;而解析階段則可能是一個懶加載情況,有可能是在初始化后被其他對象進行調(diào)用才會將符號引用轉(zhuǎn)為直接引用胀葱。
類加載還有一個重要的考點就是類加載機制 漠秋,也就是所謂的雙親委派機制,即從下往上拋類的信息抵屿,優(yōu)先由上層負責加載庆锦,如果上層加載失敗,則往下拋轧葛,如果最底層的加載器也無法加載,則拋出 ClassNotFoundException 異常朝群;這個機制的作用就是為了防止核心類被第三方類進行調(diào)包操作,防止多個系統(tǒng)級別的 Class 生成誉帅。
然而,這個機制也挖了不少坑蚜锨,例如A類調(diào)用B類,則B類的加載得由加載A類的類加載器完成郭膛;但是 jdbc 核心類由 bootstrap類加載器 加載氛悬,而這個加載器只會加載lib/rt.jar而無法加載第三方的jar類,便會出現(xiàn)第三方數(shù)據(jù)庫廠商jar無法被動加載的情況棍现。
所謂的主動加載其實就是我們用戶通過 Class.forName() 進行類加載镜遣,被動加載則是無需我們通過編寫這類代碼進行該類的加載
這個時候就引入SPI 和線程上下文類加載器( Thread Context ClassLoader )來加載第三方的jar,所以為什么新版的 mysql 驅(qū)動包無需我們手動進行類加載也可以自動加載(舊版的沒有用上面提到的加載器)谎僻。一般來說這個線程上下文類加載器就是 Application-ClassLoader寓辱,所以可以直接加載,但是這種用法從某種意義上屬于打破雙親委派機制的一種情況秫筏,因為需要由啟動類加載器進行加載的類,由應用程序類加載器去進行加載了。
3. JVM內(nèi)存參數(shù)調(diào)優(yōu)
JVM內(nèi)存調(diào)優(yōu)主要表現(xiàn)在對JVM內(nèi)存參數(shù)的設(shè)定上鹅颊,通過設(shè)置符合生產(chǎn)情況來穩(wěn)定項目的運行墓造,而不是提高運行性能,我們主要看設(shè)置的最頻繁的參數(shù)列表觅闽,其余參數(shù)目前來說大部分內(nèi)存參數(shù)的默認情況可以基本符合線上需求
示例參數(shù) | 描述 |
---|---|
-Xms20m | 堆初始值20M ,等同于-XX:InitalHeapSize=20m尸闸,默認為物理內(nèi)存的1/64 |
-Xmx20m | 堆最大可用值20M ,等同于-XX:MaxHeapSize=20m苞尝,默認為物理內(nèi)存的1/4 |
-Xss1m | 單個線程棧的大小宦芦,等同于-XX:ThreadStackSize,一般默認512k~1024k |
我們盡量將堆的初始值和最大值設(shè)置一樣调卑,這樣做的好處是為了防止JVM在內(nèi)存不夠的時候向OS申請內(nèi)存以及GC回收后JVM釋放部分內(nèi)存導致的性能下降恬涧,在初始化的時候限制死JVM的大小,具體設(shè)置多大看具體的項目情況气破。如果經(jīng)常出現(xiàn)OOM或FULL GC,可以考慮是否為JVM堆設(shè)置的過小低匙,或者是否發(fā)生內(nèi)存泄漏碳锈。
xss參數(shù)設(shè)置的越大,那么可創(chuàng)建的線程數(shù)量便越少强重,因為每個線程棧占用的空間越多贸人,導致可用的內(nèi)存越少;同時艺智,如果設(shè)置堆的內(nèi)存越大,可創(chuàng)建的線程也會變少十拣,因為線程不在堆內(nèi)存上創(chuàng)建,JVM只是創(chuàng)建了一個Thread對象泽西,線程在堆內(nèi)存之外的內(nèi)存上創(chuàng)建缰趋,由OS具體生成陕见,堆給的內(nèi)存越多糠溜,那么OS可使用的內(nèi)存越少。所以如果項目比較少用到遞歸或者深度較大的方法蜕着,那么這個值可以相對設(shè)置小一點红柱,以免出現(xiàn)無法創(chuàng)建線程的OOM。
總結(jié)
由于篇幅原因韧骗,本章節(jié)只是粗略概括目前JVM內(nèi)存區(qū)域劃分以及類加載的基本知識內(nèi)容以及基本的面試題零聚,如果讀者想要更進一步,可以通過閱讀相關(guān)書籍與博客來加深相應的技術(shù)深度政模。
接下來蚂会,我們回到最初的三個問題
1. JVM運行時數(shù)據(jù)區(qū)的內(nèi)存怎么進行劃分胁住?
2. JVM的類加載是怎么樣的彪见?
3. 如何針對JVM內(nèi)存參數(shù)進行調(diào)優(yōu)?
如果你們可以很流暢的回答這些問題枫慷,那么恭喜你浪规,該章節(jié)的內(nèi)容已經(jīng)全部掌握探孝,如果不行,希望可以回到對應問題講解的地方缸濒,或者對某個不了解的點進行額外的知識搜索,盡量用自己組織的語言回答這些問題斩跌。