【001 Java語言和JVM虛擬機】
Java語言是一種先編譯后解釋的語言。JVM是在操作系統(tǒng)之上的虛擬處理器,工作流程主要包括加載class文件、管理并分配內(nèi)存、執(zhí)行垃圾收集。
【002 JVM類加載器的執(zhí)行流程】
裝載流程包括加載、鏈接和初始化,其中類加載器只負責加載流程。
加載:取得class文件的二進制流,并在堆空間創(chuàng)建class對象的引用。
鏈接:將二進制流合并到JVM虛擬機中,通過文件驗證、準備(例如為static和final域設(shè)置初始化信息)、解析(例如將符號引用直接替換為地址引用),實現(xiàn)與平臺相關(guān)。
初始化:執(zhí)行類構(gòu)造器的過程,如果沒有通過new可以不執(zhí)行。
【003 JVM類加載器的分類】
從上到下可以分為四類:BootStrap ClassLoader、External ClassLoad、App ClassLoad和Custom ClassLoad。BootStrap ClassLoader是類加載器的頂級,主要負責對rt.jar進行加載,加載的都是JVM運行的核心類庫文件,如IO、Lang、JDBC;External ClassLoader主要是對jdk的其他擴展類庫進行加載,如Resource、Swing、Security;App ClassLoader主要是對開發(fā)者提供的類進行加載,是最常見的加載器;Custom ClassLoader是要自定義的加載器,重新實現(xiàn)類的加載方法,如Tomcat就定義了加載器以改變加載雙親規(guī)則。
【004 JVM類加載模式】
JVM默認的加載模式是雙親加載,加載class文件時會從底層加載器開始檢查,是否已用該類型加載器加載了class文件,如果已經(jīng)加載,則返回已加載的類引用,如果沒有加載,則會把加載權(quán)限交給上一級的加載器。如果在BootStrap ClassLoader中也沒有找到加載的類,則嘗試從頂層加載器開始加載該類,如加載不了,則交給下一級加載器進行加載。
雙親模式加載的好處是,避免了底層加載器修改上層加載器的加載規(guī)則,比較安全。
雙親模式加載的缺點是,子類加載器可以通過成員parent看到父類,而父類加載器看不到子類加載器的信息,如果存在例如JDBC這樣的接口定義在jt.jar父類加載器,而具體實現(xiàn)類在App ClassLoader的情況,就會加載異常。需要通過在上下文中傳入子類加載器來解決。
【005 Tomcat類加載模式】
Tomcat打破了雙親加載這一模式,自定義的WebAppClassLoader可以直接加載應(yīng)用類,而不會先交給父類加載器。這樣做的原因是因為一個JVM虛擬機里可以存在多個Web應(yīng)用,雙親加載的模式會導致父類加載的類庫會被多個應(yīng)用共享,做不到應(yīng)用隔離。同時,Tomcat的類加載器還提供了熱部署的功能,不用重啟就可以重新加載類庫。
【006 自定義類加載器】
首先,要自定義一個繼承了ClassLoader抽象類的子類,并為其指定父類加載器;其次,需要重寫findClassLoader方法指定在何處獲取已加載的類庫和loadClass方法指定如何加載類庫;最后,還需要通過應(yīng)用程序顯示制定類加載器(從這點上看,用new方法無法使用自定義的類加載器)。
【007 JVM的內(nèi)存結(jié)構(gòu)】
JVM內(nèi)存可以分為方法區(qū)、棧區(qū)、堆區(qū)和本地方法區(qū),此外還有寄存器和直接內(nèi)存區(qū)域。其中,棧區(qū)和寄存器是歸屬于線程的,每個線程都有一份獨立的空間;而其他空間是歸屬于進程的,所有線程共享。本地方法區(qū)可以直接調(diào)用內(nèi)存。
【008 棧的內(nèi)存分配】
棧區(qū)可以分為操作棧、局部變量棧等,用于保存線程運行的局部數(shù)據(jù)。另外,JVM還存在一種棧上分配的技術(shù),如果啟用DoEscapeAnalysis,那么被線程獨享的小對象,如byte數(shù)組,可以被分配到??臻g,避免垃圾回收的消耗。
【008擴展如何使一個程序的函數(shù)調(diào)用盡可能深?】
一是擴大棧的內(nèi)容空間,通過-Xss設(shè)置;二是減少一次調(diào)用使用的??臻g,方法包括減少局部變量的空間,少用long、double,減少參數(shù)個數(shù),此外還可以關(guān)閉JVM棧上分配。
【009 堆的內(nèi)存分配】
堆空間可以分為新生代、老年代和永久區(qū)。新生代通常占整個堆空間的3/8,可以分為edon、from和to三個子區(qū)域,比例分別為8:1:1,新創(chuàng)建的對象通常情況下會被分配到edon區(qū)域,在第一次垃圾回收后會被轉(zhuǎn)移到其他區(qū)域。老年代存放的是存活時間較長的對象,或是對象的空間較大,在初始化創(chuàng)建或第一次垃圾回收時,就被直接放入。永久區(qū)嚴格意義上說,是方法區(qū)的實現(xiàn),在JDK1.7后被移入堆內(nèi)存,在JDK1.8后被移入元區(qū)。
【010 JVM內(nèi)存異?!?/p>
內(nèi)存異常,常見的兩個錯誤是OutofMemoryException和StackOverFlowException。前一個異常在內(nèi)存的所有區(qū)域都可能拋出,原因是空間內(nèi)存不足或者空間泄漏。后一個異常出現(xiàn)在棧區(qū),由于調(diào)用的深度超出限制,在實際的操作中,由于??臻g的大小和遞歸深度正相關(guān),通常棧區(qū)更容易出現(xiàn)StackOverFlowException。
【011 JVM內(nèi)存參數(shù)】
① 堆的分配參數(shù):-Xmx和-Xms,最大堆空間和最小堆空間,一般設(shè)置為一樣大小,否則在運行過程中JVM需要動態(tài)調(diào)整堆空間大小;-Xmn,新生代大?。?XX:NewRatio:設(shè)置新生代的比例;-XX:SurvivoRatio:設(shè)置edon:from的比例;-XX:OnOutOfMemoryError:內(nèi)存溢出時可以指定腳本報錯或重啟JVM。
② 棧的內(nèi)存分配:-Xss,通常根據(jù)內(nèi)存的大小分配在幾百K到1M之間。
③ 永久區(qū)分配:-XX:PermSize,-XX:MaxPErmSize,永久區(qū)的最小和最大空間。
【012 JVM內(nèi)存模型】
JVM的共享內(nèi)存,在被線程使用時,通常會拷貝一個副本放入線程工作區(qū)域,做到數(shù)據(jù)的互相隔離,當然這也會造成多個線程共享的數(shù)據(jù)不同步問題。對于Volatile變量,規(guī)定線程每次讀取時必須從主內(nèi)存中加載,而修改后必須同步到主內(nèi)存中,保證數(shù)據(jù)的同步,但是加載(read,load)和更新(store,write)操作都不是原子性的操作,會造成線程不安全。對于Synchronized變量,是通過鎖機制,保證數(shù)據(jù)在同一時間內(nèi)只能被獲得鎖的線程訪問,從而保證線程安全,但只有釋放鎖后才會把數(shù)據(jù)寫回到主內(nèi)存。對于final變量,雖然和普通變量一樣使用,但由于其是不可修改的,因此也不存在同步問題。
【013 垃圾回收器算法】
GC是JVM的后臺線程,只有在系統(tǒng)空閑并且內(nèi)存區(qū)域不足時才會執(zhí)行。
① 引用計數(shù)法,循環(huán)引用問題,Python
② 標記-清理算法,標記從root可達的所有對象,清除不可達對象,會造成零碎空間問題
③ 標記-壓縮算法,標記從root可達的所有對象,移動到連續(xù)內(nèi)存,然后清除其他所有空間
④ 復制算法,兩個一樣的內(nèi)存塊空間,把其中一塊的可達對象移動到另一塊連續(xù)存儲,然后清除前一塊的空間
【014擴展什么是root可達?】
從??臻g/方法區(qū)的Static成員/常量池/Native方法棧存在對象引用鏈,可以訪問到對象。
【015 堆區(qū)采用的GC算法】
① 新生代的edon區(qū)域采用標記-清理算法,80%的新生代對象在第一次GC時就會被清理,標記-清理算法是最快的;
② 新生代的from和to區(qū)域采用復制算法,超過年齡限制的對象會被清理到老年代;
③ 老年代的GC并不是太頻繁,采用標記-壓縮算法,盡量避免零碎區(qū)域FULL GC。
【016 GC收集器實例】
① 串行回收器,暫停所有用戶線程,新生代復制,老年代標記-壓縮,適用于單CPU的應(yīng)用;
② 并行回收器ParNew,暫停所有用戶線程,新生代回收器,并行復制,適用于多核應(yīng)用;
③ 并行回收器Parallel,暫停所有用戶線程,新生代并行復制,老年代并行標記-壓縮,可分開使用,更注重吞吐量;
④ 并發(fā)回收器CMS,是目前JVM虛擬機默認的GC算法,目標是最短時間回收,允許用戶線程與回收線程并發(fā)執(zhí)行,采用標記-清理算法,常用于老年代;CMS的回收分為初始標記、并發(fā)標記、重新標記、并發(fā)清理和并發(fā)重置,其中初始標記和重新標記是不允許并發(fā)的,并發(fā)重置是對產(chǎn)生的零碎空間進行壓縮,但不是每次都要執(zhí)行;
⑤ G1回收器,是JDK1.7之后提出的回收算法,把堆劃分成若干個大小一致的區(qū)域,打破了新生代、老年代的物理隔離;標記算法同CMS,但是回收時會優(yōu)先選擇要回收區(qū)域最大的空間進行回收,保證可用的連續(xù)空間最大化。
【016擴展1 finalize和System.GC】
① Finalize是Object類的方法,會在第一次調(diào)用GC時被觸發(fā),對類進行清理,但也可以重寫該方法復活對象,創(chuàng)建新的引用指向自身this;另外不推薦在該方法中進行資源的釋放,因為該方法執(zhí)行的時間不可控。
② System.GC是系統(tǒng)方法,可以通過顯示調(diào)用通知JVM虛擬機需要進行垃圾回收,但并不能保證虛擬機的立即執(zhí)行。
【016擴展2 STOP-THE-WORLD】
STOP-THE-WORLD是指從執(zhí)行效果看,JVM虛擬機出現(xiàn)全局暫停的問題,GC是導致該現(xiàn)象的一個原因。這一現(xiàn)象對系統(tǒng)運行存在危害,例如主從備份的機器,主機發(fā)現(xiàn)系統(tǒng)暫停,會主動切換到備機執(zhí)行,此時主機恢復運行,會出現(xiàn)不同步問題。
【016擴展3 JDK1.8的分代變化】
最大的變化是將永久區(qū)移到了元空間,其垃圾回收算法采用標記-壓縮。該變化的原因是為了更好的控制類加載器和class對象的內(nèi)存空間,因為永久區(qū)很少會執(zhí)行GC,導致不用的類加載器和class對象不能被及時刪除。
【017 JVM指令重排】
指令重排是指編譯器或運行時環(huán)境,會根據(jù)指令的執(zhí)行情況,優(yōu)化各指令的執(zhí)行順序。指令重排對于一個線程的執(zhí)行順序沒有影響,但是多個不同線程的執(zhí)行中,會造成執(zhí)行順序變化,得到不同的執(zhí)行結(jié)果。因此在指令執(zhí)行過程中,需要通過各種鎖機制保證執(zhí)行順序。
【018 JVM鎖機制】
JVM鎖與Java語言中的Lock和Synchronized對應(yīng)的層次不同,是虛擬機執(zhí)行的真正機制。JVM的鎖機制更適用于競爭不頻繁的場景,避免真正加鎖,如果競爭激勵,建議關(guān)閉JVM鎖優(yōu)化。
① 偏向鎖,首次使用的線程會占有該鎖,同一線程再次訪問不受限制,如果其他線程需要訪問,則偏向結(jié)束,需要進行Lock競爭;
② 輕量級鎖,如果對象沒有被鎖定,則可以使用,如果存在競爭,則需要進行Lock競爭;
③ 自旋鎖,如果需要的對象被鎖定,可以執(zhí)行一段空語句,再看是否還被占用;
④ 無鎖CAS,check and swap,每次執(zhí)行都檢查對象是否被修改,被修改則進行修正。
【019 Java語法糖】
① 泛型與類型擦除:List經(jīng)過編譯后的實際類型為List,因此如果要真正限制集合類型,可以使用List,泛型擦除后的結(jié)果為List;
② 自動裝箱和拆箱:包裝類型Integer和基本類型int之間的互相比較,會默認轉(zhuǎn)為int類型;Integer與Integer類型的==比較,[-128,127]區(qū)間范圍內(nèi)編譯器會認為是int類型比較,否則就是對象地址比較;
③ foreach的遍歷循環(huán),編譯時需要轉(zhuǎn)化為下標方式,因此執(zhí)行效率較低。