JVM基本概念
JVM是可運行java代碼的假想計算機巨朦,運行在操作系統(tǒng)之上,與硬件沒有直接交互跌榔。JVM的基本組成:
- 類加載器
- 運行時數(shù)據(jù)區(qū)
- 本地方法庫
- 執(zhí)行引擎
運行過程
java源文件通過編譯器生成對應的.class文件(字節(jié)碼文件)折剃,字節(jié)碼文件通過jvm中的解釋器镶奉,編譯成特定機器上的機器碼测蹲。
1莹捡、java源文件-->編譯器-->字節(jié)碼文件
2、字節(jié)碼文件-->JVM-->機器碼
每一種平臺的解釋器是不同的扣甲,但實現(xiàn)的虛擬機是相同的(跨平臺的原因)篮赢。當一個程序開始運行,虛擬機就開始實例化文捶,多個程序啟動就會存在多個虛擬機實例荷逞。程序退出或關(guān)閉媒咳,則虛擬機實例銷毀粹排,多個虛擬機實例之間數(shù)據(jù)不能共享。
線程
線程指程序執(zhí)行過程中的一個線程實體涩澡。JVM允許一個應用并發(fā)執(zhí)行多個線程顽耳。
- 虛擬機線程
- 周期性任務線程
- GC線程
- 編譯器線程
- 信號分發(fā)線程
JVM內(nèi)存區(qū)域
線程私有數(shù)據(jù)區(qū)域的生命周期與線程相同,依賴用戶線程的啟動/結(jié)束而創(chuàng)建/銷毀VM內(nèi)妙同,每個線程都與操作系統(tǒng)本地線程直接映射射富,因此部分內(nèi)存區(qū)域的存/否跟隨本地線程的生/死對應。
線程共享區(qū)域隨虛擬機的啟動/關(guān)閉而創(chuàng)建/銷毀粥帚。
線程私有:程序計數(shù)器胰耗、虛擬機棧、本地方法區(qū)
線程共享:JAVA堆芒涡、方法區(qū)
一柴灯、程序計數(shù)器
一塊較小的內(nèi)存空間, 是當前線程所執(zhí)行的字節(jié)碼的行號指示器卖漫,每條線程都要有一個獨立的程序計數(shù)器,這類內(nèi)存也稱為“線程私有”的內(nèi)存赠群。
正在執(zhí)行 java 方法的話羊始,計數(shù)器記錄的是虛擬機字節(jié)碼指令的地址(當前指令的地址)。如果還是 Native 方法查描,則為空突委。
這個內(nèi)存區(qū)域是唯一一個在虛擬機中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域
二、虛擬機棧
描述java方法執(zhí)行的內(nèi)存模型冬三,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表匀油、操作數(shù)棧、動態(tài)鏈接长豁、方法出口等信息钧唐。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程匠襟。
棧幀( Frame)是用來存儲數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu)钝侠,同時也被用來處理動態(tài)鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)酸舍。棧幀隨著方法調(diào)用而創(chuàng)建帅韧,隨著方法結(jié)束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。
三啃勉、本地方法棧
本地方法區(qū)和 Java Stack 作用類似, 區(qū)別是虛擬機棧為執(zhí)行 Java 方法服務, 而本地方法棧則為Native 方法服務, 如果一個 VM 實現(xiàn)使用 C-linkage 模型來支持 Native 調(diào)用, 那么該棧將會是一個C 棧忽舟,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二為一。
四淮阐、堆
是被線程共享的一塊內(nèi)存區(qū)域叮阅,創(chuàng)建的對象和數(shù)組都保存在 Java 堆內(nèi)存中,也是垃圾收集器進行垃圾收集的最重要的內(nèi)存區(qū)域泣特。由于現(xiàn)代 VM 采用分代收集算法, 因此 Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區(qū)浩姥、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。
五状您、方法區(qū)/永久代
即我們常說的永久代(Permanent Generation), 用于存儲被 JVM 加載的類信息勒叠、常量、靜態(tài)變量膏孟、即時編譯器編譯后的代碼等數(shù)據(jù). HotSpot VM把GC分代收集擴展至方法區(qū), 即使用Java堆的永久代來實現(xiàn)方法區(qū), 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內(nèi)存, 而不必為方法區(qū)開發(fā)專門的內(nèi)存管理器(永久帶的內(nèi)存回收的主要目標是針對常量池的回收和類型
的卸載, 因此收益一般很小)眯分。
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本柒桑、字段弊决、方法、接口等描述等信息外魁淳,還有一項信息是常量池(Constant Pool Table)飘诗,用于存放編譯期生成的各種字面量和符號引用傅联,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。 Java 虛擬機對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規(guī)定疚察,每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求蒸走,這樣才會被虛擬機認可、裝載和執(zhí)行貌嫡。
運行時內(nèi)存
Java從GC角度可以細分為新生代比驻、老年代和永久代
新生代
是用來存放新生的對象。一般占據(jù)堆的 1/3 空間岛抄。由于頻繁創(chuàng)建對象别惦,所以新生代會頻繁觸發(fā)MinorGC 進行垃圾回收。新生代又分為Eden 區(qū)夫椭、ServivorFrom掸掸、ServivorTo 三個區(qū)。
MinorGC 的過程(復制->清空->互換)
1)Eden區(qū)
Java 新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大蹭秋,則直接分配到老年代)扰付。當 Eden 區(qū)內(nèi)存不夠的時候就會觸發(fā) MinorGC,對新生代區(qū)進行一次垃圾回收仁讨。
2)servivorFrom
上一次 GC 的幸存者羽莺,作為這一次 GC 的被掃描者。
3)servivorTo
保留了一次 MinorGC 過程中的幸存者洞豁。
老年代
主要存放應用程序中生命周期長的內(nèi)存對象盐固。
老年代的對象比較穩(wěn)定,所以 MajorGC 不會頻繁執(zhí)行丈挟。在進行 MajorGC 前一般都先進行了一次 MinorGC刁卜,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發(fā)曙咽。當無法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對象時也會提前觸發(fā)一次 MajorGC 進行垃圾回收騰出空間蛔趴。
MajorGC 采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象桐绒,然后回收沒有標記的對象夺脾。MajorGC 的耗時比較長之拨,因為要掃描再回收茉继。MajorGC 會產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗蚀乔,我們一般需要進行合并或者標記出來方便下次直接分配烁竭。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常吉挣。
永久代
指內(nèi)存的永久保存區(qū)域派撕,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時候被放入永久區(qū)域婉弹,它和和存放實例的區(qū)域不同,GC 不會在主程序運行期對永久區(qū)域進行清理。所以這也導致了永久代的區(qū)域會隨著加載的 Class 的增多而脹滿终吼,最終拋出 OOM 異常镀赌。
JAVA8與元數(shù)據(jù)
在 Java8 中,永久代已經(jīng)被移除际跪,被一個稱為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代商佛。元空間的本質(zhì)和永久代類似,元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機中姆打,而是使用本地內(nèi)存良姆。因此,默認情況下幔戏,元空間的大小僅受本地內(nèi)存限制玛追。類的元數(shù)據(jù)放入 native memory, 字符串池和類的靜態(tài)變量放入 java 堆中,這樣可以加載多少類的元數(shù)據(jù)就不再由MaxPermSize 控制, 而由系統(tǒng)的實際可用空間來控制闲延。
垃圾回收
如何判斷垃圾
1)引用計數(shù)法
當對象被引用是計數(shù)+1(存在循環(huán)引用問題)
2)可達性分析
為了解決引用計數(shù)法的循環(huán)引用問題痊剖,Java 使用了可達性分析的方法。通過一系列的“GC roots”對象作為起點搜索垒玲。如果在“GC roots”和一個對象之間沒有可達路徑邢笙,則稱該對象是不可達的。
要注意的是侍匙,不可達對象不等價于可回收對象氮惯,不可達對象變?yōu)榭苫厥諏ο笾辽僖?jīng)過兩次標記過程。兩次標記后仍然是可回收對象想暗,則將面臨回收妇汗。
垃圾回收算法
1)標記清除
最基礎(chǔ)的垃圾回收算法,分為標記和清除兩個階段说莫。標記階段標記出所有需要回收的對象杨箭,清除階段回收被標記的對象所占用的空間。
ps:該算法會導致內(nèi)存碎片化储狭,可能發(fā)生大對象不能找到可利用的空間
2)復制
為了解決標記清除算法導致的內(nèi)存碎片化而被提出的算法互婿。按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊。每次只使用其中一塊辽狈,當這一塊內(nèi)存滿后將尚存活的對象復制到另一塊上去慈参,把已使用的內(nèi)存清掉。
ps:該算法最大的問題是可用內(nèi)存被壓縮到了原本的一半刮萌。且存活對象增多的話驮配,Copying 算法的效率會大大降低。
3)標記整理
結(jié)合了以上兩個算法,為了避免缺陷而提出壮锻。標記階段和 Mark-Sweep 算法相同琐旁,標記后不是清理對象,而是將存活對象移向內(nèi)存的一端猜绣。然后清除端邊界外的對象灰殴。
4)分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根據(jù)對象存活的不同生命周期將內(nèi)存劃分為不同的域掰邢,一般情況下將 GC 堆劃分為老年代(Tenured/Old Generation)和新生代(Young Generation)验懊。老年代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收尸变,因此可以根據(jù)不同區(qū)域選擇不同的算法
四種引用
1)強引用
在 Java 中最常見的就是強引用义图,把一個對象賦給一個引用變量,這個引用變量就是一個強引用召烂。當一個對象被強引用變量引用時碱工,它處于可達狀態(tài),它是不可能被垃圾回收機制回收的奏夫,即使該對象以后永遠都不會被用到 JVM 也不會回收怕篷。因此強引用是造成 Java 內(nèi)存泄漏的主要原因之一。
2)軟引用
軟引用需要用 SoftReference 類來實現(xiàn)酗昼,對于只有軟引用的對象來說廊谓,當系統(tǒng)內(nèi)存足夠時它不會被回收,當系統(tǒng)內(nèi)存空間不足時它會被回收麻削。軟引用通常用在對內(nèi)存敏感的程序中蒸痹。
3)弱引用
弱引用需要用 WeakReference 類來實現(xiàn),它比軟引用的生存期更短呛哟,對于只有弱引用的對象來說叠荠,只要垃圾回收機制一運行,不管 JVM 的內(nèi)存空間是否足夠扫责,總會回收該對象占用的內(nèi)存榛鼎。
4)虛引用
虛引用需要 PhantomReference 類來實現(xiàn),它不能單獨使用鳖孤,必須和引用隊列聯(lián)合使用者娱。虛引用的主要作用是跟蹤對象被垃圾回收的狀態(tài)。
垃圾回收器
新生代收集器
1)serial(單線程復制苏揣、串行)
2)parNew(serial+多線程)
3)parallel scavenge(多線程復制黄鳍、吞吐量優(yōu)先)
老年代收集器
1)serial Old(單線程標記整理)
- JDK1.5之前與新生代的parallel Scavenge配合使用
- CMS收集器的備用方案,在CMS并發(fā)失敗時會使用
2)parallel Old(多線程標記整理)
- JDK1.6后提供腿准,ParallelScavenge的老年代版本际起,注重吞吐量
3)CMS(多線程標記清除)
- 響應時間優(yōu)先,獲取最短垃圾回收停頓時間
- 初始標記:標記GC Roots直接關(guān)聯(lián)對象吐葱,速度快街望,仍需暫停所有工作線程
- 并發(fā)標記:進行GC Roots跟蹤過程,不需要暫停工作線程
- 重新標記:為了修正并發(fā)標記時期工作線程運行導致標記對象發(fā)生的變動弟跑,需暫停所有工作線程
- 并發(fā)清除:清除GC Roots不可達對象灾前,不需要暫停工作線程
整堆收集器(G1)
1、基于標記-整理算法孟辑,不產(chǎn)生內(nèi)存碎片
2哎甲、可以非常精確控制停頓時間,在不犧牲吞吐量前提下饲嗽,實現(xiàn)低停頓垃圾回收炭玫。
G1 收集器避免全區(qū)域垃圾收集,它把堆內(nèi)存劃分為大小固定的幾個獨立區(qū)域貌虾,并且跟蹤這些區(qū)域的垃圾收集進度吞加,同時在后臺維護一個優(yōu)先級列表,每次根據(jù)所允許的收集時間尽狠,優(yōu)先回收垃圾最多的區(qū)域衔憨。區(qū)域劃分和優(yōu)先級區(qū)域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率袄膏。
GC調(diào)優(yōu)
- 內(nèi)存
- 鎖競爭
- cpu占用
- IO
類加載機制
類加載流程:加載-->驗證-->準備-->解析-->初始化
加載
這個階段會在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象践图,作為方法區(qū)這個類的各種數(shù)據(jù)的入口。
驗證
驗證Class 文件的字節(jié)流中包含的信息是否符合當前虛擬機的要求沉馆。
準備
對類變量分配內(nèi)存并設(shè)置類變量的默認值码党,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間,常量池斥黑、靜態(tài)變量在JDK8之前存儲在方法區(qū)中闽瓢,JDK8之后存儲在堆中。
分配內(nèi)存空間和賦默認值是兩個步驟心赶,分配空間在準備階段扣讼,賦值在初始化階段,但靜態(tài)變量如果是final的基本類型或字符串常量缨叫,賦值則會在準備階段完成椭符。
解析
虛擬機將常量池中的符號引用替換為直接引用的過程
初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后耻姥,除了在加載階段可以自定義類加載器以外销钝,其它操作都由 JVM 主導。到了初始階段琐簇,才開始真正執(zhí)行類中定義的 Java 程序代碼蒸健。
- 初始化的情況
1)main方法所在的類會被初始化
2)首次訪問這個類的靜態(tài)變量或靜態(tài)方法
3)子類初始化時座享,若父類還沒初始化,會引發(fā)父類初始化
4)子類訪問父類的靜態(tài)變量似忧,會觸發(fā)父類的初始化
5)new 會導致初始化
6)Class.forName - 不會初始化的情況
1)訪問類的static final靜態(tài)常量(基本類型和字符串)
2)類對象.class不會觸發(fā)初始化
3)創(chuàng)建該類的數(shù)組不會觸發(fā)初始化
4)loadclass方法不會觸發(fā)初始化
5)class.forName 的參數(shù)2為false時不會觸發(fā)初始化
類加載器
啟動類加載器(BootStrapClassLoader)
加載$JAVA_HOME/jre/lib目錄渣叛,無法直接訪問
擴展類加載器(ExtensionClassLoader)
加載$JAVA_HOME/jre/lib/ext目錄,上級為啟動類加載器
應用程序類加載器(ApplicationClassLoader)
加載用戶路徑(classpath)上的類庫盯捌,上級為擴展類加載器
自定義類加載器(CustomClassLoader)
通過繼承 java.lang.ClassLoader實現(xiàn)自定義的類加載器
雙親委派模型
1.當Application ClassLoader 收到一個類加載請求時淳衙,他首先不會自己去嘗試加載這個類,而是將這個請求委派給父類加載器Extension ClassLoader去完成饺著。
2.當Extension ClassLoader收到一個類加載請求時箫攀,他首先也不會自己去嘗試加載這個類,而是將請求委派給父類加載器Bootstrap
ClassLoader去完成幼衰。
3.如果Bootstrap ClassLoader加載失敗(在<JAVA_HOME>\lib中未找到所需類)靴跛,就會讓Extension ClassLoader嘗試加載。
4.如果Extension ClassLoader也加載失敗渡嚣,就會使用Application ClassLoader加載汤求。
5.如果Application ClassLoader也加載失敗,就會使用自定義加載器去嘗試加載严拒。
優(yōu)點:避免重復加載 + 避免核心類篡改
缺點:父類加載器無法委托子類加載器
打破雙親委派模型
重寫classloader()方法
例子:
1扬绪、JDBC加載mysql驅(qū)動
DriverManager 類中要加載各個實現(xiàn)了Driver接口的類,然后進行管理裤唠,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包挤牛,由BootStrap類加載器加載,而其Driver接口的實現(xiàn)類是位于服務商提供的 Jar 包种蘸,根據(jù)類加載機制墓赴,當被裝載的類引用了另外一個類的時候,虛擬機就會使用裝載第一個類的類裝載器裝載被引用的類航瞭。也就是說BootStrap類加載器還要去加載jar包中的Driver接口的實現(xiàn)類诫硕,即打破雙親委派模型。
2刊侯、tomcat
- 對于各個 webapp中的 class和 lib章办,需要相互隔離,不能出現(xiàn)一個應用中加載的類庫會影響另一個應用的情況滨彻,而對于許多應用藕届,需要有共享的lib以便不浪費資源。
- 與 jvm一樣的安全性問題亭饵。使用單獨的 classloader去裝載 tomcat自身的類庫休偶,以免其他惡意或無意的破壞;
- 熱部署辜羊。
3踏兜、OSGI
IO流
阻塞IO模型(BIO)
最傳統(tǒng)的一種IO模型词顾,即在讀寫數(shù)據(jù)過程中會發(fā)生阻塞現(xiàn)象。以流的方式處理數(shù)據(jù)碱妆。
適用于連接數(shù)較小且固定的架構(gòu)肉盹。
多路復用IO模型(NIO)
多路復用IO中會有一個線程不斷去輪詢多個socket的狀態(tài),只有當socket有真正的讀寫事件時山橄,才會調(diào)用實際的IO讀寫操作垮媒。以塊(緩沖區(qū))的方式處理數(shù)據(jù)舍悯。
適用于連接數(shù)多且連接比較短(輕操作)的架構(gòu)航棱,如:聊天服務器、彈幕系統(tǒng)等萌衬,JDK1.4開始支持
Channel(通道):支持非阻塞讀寫饮醇,也可以讀寫緩沖區(qū),也支持異步讀寫秕豫。
Buffer(緩沖區(qū)):本質(zhì)就是一塊可以讀寫的內(nèi)存朴艰,這塊內(nèi)存被包裝成NIO Buffer對象,并提供了一組方法來訪問該內(nèi)存混移。
Selector(選擇器):NIO組件祠墅,可以檢查一個或多個通道,并確定哪些通道已經(jīng)準備好進行讀寫恶复。
異步非阻塞IO模型(AIO)
IO 操作的兩個階段都不會阻塞用戶線程亚再,這兩個階段都是由內(nèi)核自動完成瓜挽,然后發(fā)送一個信號告知用戶線程操作已完成。
適用于連接數(shù)多且連接比較長(重操作)的架構(gòu)狗准,如:相冊服務器等,JDK1.7開始支持茵肃。