您好,登錄后才能下訂單哦!
首先通過數(shù)據(jù)類型來引入一個高級語言的核心概念,堆和棧。JAVA的基本類型包括:byte, short, int, long, returnAddress等,其存儲在棧上;引用類型包括:類類型,接口類型和數(shù)組,其存儲在堆上。在java中,一個線程就會有相應的線程棧與之對應,而堆則是所有線程共享的。棧是運行單位,因此里面存儲的信息都是跟當前線程相關(guān)信息的,包括局部變量、程序運行狀態(tài)、方法返回值等;而堆只負責存儲對象信息。
之所以將對和棧分離,有如下幾點原因:棧代表了邏輯處理,而堆代表數(shù)據(jù),滿足分治的思想;堆中的內(nèi)容可以被多個棧共享,即提供數(shù)據(jù)交換的方式又節(jié)省空間;使得存儲地址動態(tài)增長成為可能,相應棧中只需要記錄堆中的一個地址即可;對面向?qū)ο蟮脑忈專瑢ο蟮膶傩跃褪菙?shù)據(jù),存放在堆中,對象的行為是運行邏輯,放在棧中;堆和棧中,棧是程序運行最根本的東西,程序運行可以沒有堆,但不能沒有棧,而堆是為棧進行數(shù)據(jù)存儲服務(wù)的,就是一塊共享的內(nèi)存,這種思想使得垃圾回收成為可能。
Java對象的大小:一個空Object對象的大小是8byte,以及其地址空間4byte(32位),比如對于int這個基礎(chǔ)類型,其包裝類型Integer大小為8+4=12byte,但由于java對象大小需要時8byte的倍數(shù),因而為16byte,因此包裝類型的消耗是基礎(chǔ)類型的2倍。
強引用、軟引用、弱引用和虛引用:強引用是一般虛擬機生成的引用,虛擬機嚴格的將通過它判斷是否需要回收;軟引用一般作為緩存使用,當內(nèi)存緊張時,會對其進行回收;弱引用,也是作為緩存使用,不過一定會被垃圾回收。
JVM的組成,可以通過下圖對其有個大體的了解。
Class Loader:加載大Class文件,該文件的格式由《JVM Specification》規(guī)定,包括父類,接口,版本,字段,方法等元數(shù)據(jù)信息。
Execution Engine:執(zhí)行引擎也叫解釋器,負責解釋命令,提交OS執(zhí)行。所謂的JIT指的就是提前將中間語言字節(jié)碼轉(zhuǎn)化為目標文件obj。
Native Interface:為了融合不同語言,java開辟了一塊區(qū)域用于處理標記為native的代碼,現(xiàn)在已很少使用。
Runtime data area:運行數(shù)據(jù)區(qū)是JVM的重點,所寫的程序就被加載在這兒。
此外,jvm的寄存器包括:pc,java程序計數(shù)器;optop,指向操作數(shù)棧頂?shù)闹羔?;frame,指向當前執(zhí)行方法的執(zhí)行環(huán)境指針;vars,指向當前執(zhí)行方法的局部變量區(qū)第一個變量的指針。
JVM的內(nèi)存管理,所有的數(shù)據(jù)都是放在運行數(shù)據(jù)區(qū),接下來介紹其中最復雜的棧(Stack),也叫棧內(nèi)存,是java程序的運行區(qū),在線程創(chuàng)建時創(chuàng)建,它的生命周期跟隨線程的生命周期,線程結(jié)束棧內(nèi)存就釋放,對于棧來說不存在垃圾回收。棧中的數(shù)據(jù)是以棧幀(Stack Frame)來存放的,其是一塊內(nèi)存區(qū)塊,是一個有關(guān)方法和運行期數(shù)據(jù)的數(shù)據(jù)集,當方法A被調(diào)用時就產(chǎn)生一個棧幀F(xiàn)1,并壓入到棧中,A方法又調(diào)用了B方法,于是產(chǎn)生的棧幀F(xiàn)2也被壓入棧,執(zhí)行完畢后,先彈出F2,再彈出F1,遵循"先進后出"原則,JAVA Stack的大體結(jié)構(gòu)如下所示。
回收算法的分類方式有很多,接下來通過一張表格對其進行一個簡單的介紹。
算法類別 | 原理闡述 |
按基本回收策略分 | |
引用計數(shù)(Reference Counting) | 針對某個對象,其每有一個引用,即增加一個計數(shù),刪除一個就減少一個計數(shù),垃圾回收時只收集計數(shù)為0的對象,缺點是無法處理循環(huán)引用的情況 |
標記-清除(Mark-Sweep) | 分為兩個階段,首先從引用根結(jié)點開始標識所有引用的對象,之后遍歷整個堆,把未標記的對象刪除,此算法需要暫停整個應用,同時會產(chǎn)生內(nèi)存碎片 |
復制(Copying) | 把內(nèi)存空間劃分為2個相等區(qū)域,每次使用一個,當垃圾回收時,遍歷當前使用區(qū)域,把使用中對象賦值到另一個區(qū)域,該復制操作成本較小。并可以進行內(nèi)存整理,缺點是需要兩倍的內(nèi)存空間 |
標記-整理(Mark-Compact) | 該算法結(jié)合了"標記-清除"和"復制"的優(yōu)點,第一階段從根結(jié)點開始標記對象,第二階段遍歷整個堆,清除未標記對象并把存活對象壓縮到堆的其中一塊,按順序排放,同時解決碎片和空間問題。 |
按分區(qū)對待的方式分 | |
增量收集(Incremental Collecting) | 實時垃圾回收,即在應用進行的同時進行 |
分代收集(Generational Collecting) | 基于對象生命周期分析得出的算法,把對象分為年輕代、年老代和持久代,對不同生命周期的對象使用不同的算法。 |
垃圾回收的判斷:由于引用計數(shù)方式無法解決循環(huán)引用,因而實際上,回收算法都是從根結(jié)點出發(fā),遍歷整個對象引用,查找存活對象。搜索的起點為棧(例如java的Main函數(shù))或者是運行時的寄存器,通過其代表的引用找到堆中對象,逐步迭代,直到以null引用或基本類型結(jié)束,該結(jié)果是一個對象樹,回收器會對未在該樹的對象進行回收。
分代的概念:由于不同對象的生命周期不同,根據(jù)其自己的特點采取不同的收集方式可以大幅提高回收效率。比如與業(yè)務(wù)相關(guān)的對象一般生命周期較長,而臨時變量生命周期很短,通過分代,可以避免長生命周期的對象被遍歷,以此來減少消耗。
如何分代:虛擬機分為年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。所有新生成的對象首先是放在年輕代中,該代的目標就是盡快回收那些短生命周期的對象,其分為3個區(qū),一個Eden區(qū),兩個Survior區(qū)。大部分對象在Eden區(qū)生成,當該區(qū)滿時,將存活對象復制到Survivor區(qū)(兩個中的一個),當該區(qū)也滿了時,將存活對象復制到另一個Survivor,當這個Survivor也滿了時,將從第一個Survivor區(qū)復制過來的并且還存活的對象復制到年老區(qū)Tenured,因此在年老區(qū)中主要存放生命周期較長的對象。而持久代,用于存放靜態(tài)文件,如Java類、方法等。持久代對垃圾回收無顯著影響,但App使用較多反射時,需要增加持久代的大小,通過設(shè)置-XX:MaxPermSize=<N>。接下來通過一張圖,對該部分有個宏觀的了解。
垃圾回收算法的觸發(fā):由于對象進行了分代處理,因此垃圾回收的區(qū)域和時間也有了不同,主要包括如下兩種類型的GC。
Scavenge GC:一般當新對象生成,并且在Eden申請空間失敗時,觸發(fā)。將清除Eden區(qū)的非存活對象,并把存貨對象移動到Survivor,然后整理兩個Survivor區(qū)。該方式不會影響到老年代,此外,該GC推薦使用速度快,效率高的算法,使Eden區(qū)盡快空閑出來。
Full GC:對整個堆進行整理,包括Young、Tenured和Perm,因此為了提高系統(tǒng)性能,需要減少FullGC的次數(shù)。發(fā)生FullGC的場景有:年老代寫滿,持久代被寫滿和System.gc()被顯示調(diào)用,上一次GC后Heap各域分配策略動態(tài)變化。
接下來通過一個表格來連接不同的收集器的優(yōu)缺點。
收集器名稱 | 詮釋 |
串性收集 | 使用單線程處理所有垃圾回收,簡單高效,適合數(shù)據(jù)量小的場景。通過-XX:+UseSerialGC打開 |
并行收集 | 對年輕代進行并行垃圾回收,因此可以減少垃圾回收時間,使用-XX:+UseParallelGC打開。 可以對老年代進行并行收集,默認使用單線程垃圾回收,使用-XX:+UseParallelOldGC打開 使用-XX:ParallelGCThreads=<N>設(shè)置并行垃圾回收的線程數(shù),此值可以和機器處理器數(shù)相等 通過-XX:MaxGCPauseMillis=<N>設(shè)置最大垃圾回收暫定時間 通過-XX:GCTimeRatio=<N>垃圾回收時間與非垃圾回收時間的比值,那么1/(1+N)即為當先系統(tǒng)的吞吐量,N默認值為99,即1%時間用于垃圾回收 |
并發(fā)收集 | 前兩者在垃圾回收時,應用會有明顯的暫停,該方式可以減少該影響,保證大部分工作并發(fā)進行(應用不停止),適合中大規(guī)模應用,使用-XX:+UseConcMarkSweepGC打開,由于并發(fā)收集比較復雜,接下來介紹幾個基本概念。 浮動垃圾:由于在應用運行時進行垃圾回收,所有有些垃圾可能在垃圾回收進行完成時產(chǎn)生,這樣就造成了"Floating Garbage",這些垃圾需要在下次垃圾回收周期才能回收,所以并發(fā)收集器需要保留20%的預留空間用于這些浮動垃圾。 Concurrent Mode Failure:由于在垃圾回收時系統(tǒng)運行,需要保證有足夠空間給程序使用,否則堆滿時,會發(fā)生"并發(fā)模式失敗",整個應用暫停,進行垃圾回收。可以通過設(shè)置-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少神域堆空間時開始執(zhí)行并發(fā)收集 |
新一代的垃圾回收算法(Garbage First, G1):該算法是為大型應用準備的,支持很大的堆和高吞吐量。該算法簡單來說,就是把整個堆劃分為一個個等大小的區(qū)域。內(nèi)存的回收和劃分都以region為單位,同時汲取CMS特點,把垃圾回收過程分為幾個階段。G1在掃描了region以后,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間,因為活躍對象小,里面可以認為多數(shù)都是垃圾,所有這種方式被稱為Garbage First,即垃圾優(yōu)先回收,整個垃圾回收過程包含如下幾個步驟。
初始標記(Initial Marking):G1對于每個region都保存了兩個標記用的bitmap,一個為previous marking bitmap,一個next marking bitmap,bitmap中包含了一個bit的地址信息指向?qū)ο蟮钠鹗键c。在開始標記前,首先并發(fā)的清空next marking bitmap,然后停止所有應用線程,并掃描標識出每個region中root可直接訪問的對象,將region的top值放入next top at mark start(TAMS),之后恢復所有線程。
并發(fā)標記(Concurrent Marking):按照之前的標記掃描對象,以標識這些對象的下層對象的活躍狀態(tài),將在此期間使用線程并發(fā)修改的先關(guān)記錄寫入remembered set logs中,新創(chuàng)建的對象則放入比top值更高的地址區(qū)間中,這些新創(chuàng)建的對象默認狀態(tài)即為活躍的,同時修改top值。
最終標記暫停(Final Marking Pause):當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,因此需要在此步驟中處理remembered set logs并修改相應的remembered set。
存活對象計算并清除(Live Data Counting and Cleanup):該步驟的觸發(fā)依賴內(nèi)存空間是否達到H(H=(1-h)*HeapSize, h為JVM Heap大小的百分比閥值)。
JVM的相關(guān)配置項非常的多,首先通過一個通用的配置理解堆相關(guān)的配置。
Java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:NewRatio=4 –XX:SurvivorRatio=4 –xx:MaxPermSize=64m –XX:MaxTenuringThreshold=0 |
-Xmx3550:設(shè)置JVM最大可用內(nèi)存為3550M
-Xms3550:設(shè)置JVM的初始內(nèi)存為3550M,此值可以與最大內(nèi)存一致,避免每次垃圾回收后JVM重新分配內(nèi)存
-Xmn2g:設(shè)置年紀代大小為2G,整個堆大小=年輕代大小+年老代大小+持久代大小。持久代默認大小為64m,所有增加年輕代會減少年老代大小,因此此值非常重要,推薦為整個堆大小的3/8
-Xss128k:設(shè)置線程的堆棧大小,默認為1M,實際中需要根據(jù)應用進行調(diào)整,一般OS推薦的線程數(shù)為3000-5000。
-XX:NewRatio=4:設(shè)置年輕代與老年代的比值,即年親代占年老代的1/4。
-XX:SurvivorRatio=4:設(shè)置年輕代中Eden區(qū)域Survivor區(qū)的大小比值,設(shè)置為4,即兩個Survior區(qū)與一個Eden區(qū)的比值為2:4。
-XX:MaxPermSize=64m:設(shè)置持久代大小為64m
-XX:MaxTenuringThreshold=0:設(shè)置垃圾最大年齡,如果設(shè)置為0,則年輕代將不經(jīng)過Survivor區(qū),直接進入老年代,適合老年代較多的場景。
接下里介紹吞吐量優(yōu)先的并行收集器和響應時間優(yōu)先的并發(fā)收集器。Tip:這類應用推薦將年輕代設(shè)置的盡可能的大,尤其是吞吐量大的應用。
并行收集器
java -Xmx3550m -Xms3550 –Xmn2g –Xss128k –XX:+UseParallelGC –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 –XX:UseAdaptiveSizePolicy |
-XX:+UseParallelGC:選擇年輕代的垃圾收集器為并行收集器
-XX:ParallelGCThreads=20:設(shè)置并行收集器的線程數(shù),最好和處理器數(shù)目一致
-XX:+UseParallelOldGC:配置年老代垃圾收集方式為并行收集
-XX:MaxGCPauseMillis=100:設(shè)置每次年輕代垃圾回收的最長時間,如果滿足,則自動調(diào)整年親代大小以滿足此值。
-XX:+UseAdaptiveSizePolicy:設(shè)置此選項后,并行收集器自動選擇年輕代區(qū)大小和相應Survivor區(qū)比例,推薦一直打開。
并發(fā)收集器
java -Xmx3550m -Xms3550 –Xmn2g –Xss128k –XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC –XX:+UseParNewGC -XX:CMSFullGCBeforeCompaction=5 –XX:UseCMSCompactAtFullCollection |
-XX:+UseConcMarkSweepGC(CMS):設(shè)置年老代為并發(fā)收集
-XX:+UseParNewGC:設(shè)置年輕代為并行收集,可以與CMS收集同時進行,現(xiàn)有版本無需設(shè)置
-XX:CMSFullGCBeforeCompaction=5:設(shè)置運行多少次GC后對內(nèi)存空間進行壓縮、整理
-XX:UseCMSCompactAtFullCollection:打開年老代的壓縮,可以消除碎片但會影響性能
此外,還有一些展示GC輔助信息的配置: -XX:PrintGC, -XX:+PrintGCDetails, -XX:PrintGCTimeStamps, Xloggc:filename。
Java內(nèi)存模型:不同的平臺,內(nèi)存模型是不一樣的,但jvm內(nèi)存模型規(guī)范是統(tǒng)一的,java多線程并發(fā)問題都會反映在java的內(nèi)存模型上,所謂線程安全就是要控制多個線程對某個資源的有序訪問和修改。總結(jié)的Java的內(nèi)存模型,需要注意2個主要問題:可見性和有序性。
Tip:這部分內(nèi)容理解起來有一定難度,需要多復習。
可見性:多個線程之間是不能相互傳遞數(shù)據(jù)通信的,它們之間的溝通需要通過共享變量。Java內(nèi)存模型規(guī)定了jvm有主內(nèi)存,主內(nèi)存是多個線程共享的,當new一個對象時,也是被分配子啊主內(nèi)存中的,每個線程都有自己的工作內(nèi)存,工作內(nèi)存存儲了主存的某些對象的副本。當線程操作某個對象時,其執(zhí)行順序為:從主內(nèi)復制變量當前工作內(nèi)存(read and load);執(zhí)行代碼,改變共享變量值(use and assign);用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容(store and write)。JVM規(guī)范定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作內(nèi)存中都有副本時,如果一個線程修改了這個共享變量,那么其他線程應該可以看到這個被修改后的值,這就是多線程的可見性問題。
有序性:線程在引用變量時不能直接從主內(nèi)存中引用,如果線程工作內(nèi)存中沒有該變量,則會從主內(nèi)存中拷貝一個副本到工作內(nèi)存中,這個過程為read-load,完成后線程會引用該副本。當同一線程再度引用該字段時,就有可能重新從主內(nèi)存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說read,load,use順序可以有JVM實現(xiàn)系統(tǒng)決定。線程不能直接為主存中字段賦值,它會將值指定給工作內(nèi)存中的副本變量(assign),完成后這個變量副本會同步到主存儲去(store-write),至于何時同步過去,也有JVM決定。為了這部分操作的有序性,需要使用synchronized關(guān)鍵字,可以將方法變?yōu)橥椒椒╬ublic synchronized void add(),也可以增加同步變量static Object lock=new Object(),然后synchronized(lock)。每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲被阻塞的線程,當一個線程被喚醒(nitify)后,才能進入到就緒隊列,等待cpu調(diào)度。例如,當一個線程a第一次執(zhí)行account.add方法是,jvm會檢查鎖對象account的就緒隊列是否已經(jīng)有線程在等待,如果有說明account被占用,此時是第一次運行,因此account就緒隊列為空,所以線程a獲得鎖,執(zhí)行方法。如果恰好這是線程b要執(zhí)行account.withdraw方法,由于線程a獲得的鎖還未釋放,因此b要進入account的就緒隊列,等得到鎖再執(zhí)行。
簡單來說,一個線程執(zhí)行臨界區(qū)代碼過程為:獲得同步鎖李晴空工作內(nèi)存;從主存拷貝變量副本到工作內(nèi)存;對這些變量進行計算;將變量從工作內(nèi)存寫回到主存;釋放鎖。
生產(chǎn)者-消費者模型:這是一個非常經(jīng)典的線程同步模型,有時不光需要保證多個線程多一個共享資源操作的互斥性,往往多個線程見都是有協(xié)作的,一個簡單的例子如下所示。
View Code
Volatile關(guān)鍵字:volatile是java的一種輕量級同步手段,它只提供多線程內(nèi)存的可見性,不保證執(zhí)行的有序性。其意義在于,任何線程對volatile修飾的變量進行修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內(nèi)存和主存同步。其使用場景為:對變量的寫操作不依賴于當前值;該變量沒有包含在具有其他變量的不定式中。
JVM調(diào)用工具:常見的包括Jconsole、JProfile和VisualVM,推薦使用VisualVM。所有的調(diào)優(yōu)都源于對線上應用的監(jiān)控和分析,主要需要觀察內(nèi)存的釋放情況、集合類檢查、對象樹等。如下圖所示,通過查看集合實例的情況來分析。通過這類堆信息查看,可以分析出年老代年輕代劃分是否合理、內(nèi)存是否泄漏、垃圾回收算法是否合適等問題。
此外,還可以通過線程監(jiān)控了解系統(tǒng)的線程數(shù)量和線程的狀態(tài),是否死鎖等;通過抽樣器查看CPU和內(nèi)存熱點的情況;通過快照來了解不同時刻相關(guān)狀態(tài)的差異。
內(nèi)存泄漏的檢查:內(nèi)存泄漏一般可以理解為系統(tǒng)資源在錯誤使用的情況下,導致使用完畢的資源無法回收,從而導致新的資源分配請求無法完成,引起系統(tǒng)錯誤。其常見場景為:年老代堆空間被占滿(java.lang.OutOfMemoryError:Java heap space),可以通過堆大小的變化發(fā)現(xiàn)問題;持久代被占滿(java.lang.OutOfMemoryError:PermGen space),在大量使用反射時會出現(xiàn);堆棧溢出(java.lang.StackOverflowError),一般因為錯誤的遞歸和循環(huán)造成;線程堆棧滿(Fatal:Stack size too small),可以通過修改-Xss解決,不過還是主要注意是否是因為線程棧過深造成;系統(tǒng)內(nèi)存被占滿(java.lang.OutOfMemoryError:unable to create new native thread),由于OS沒有足夠的資源來產(chǎn)生線程造成的,可以考慮減少單個線程的消耗或重新設(shè)計這部分程序。
常見問題
1.堆和棧的區(qū)別:堆是存放對象的,但是對象內(nèi)臨時變量是存在棧內(nèi)存中的。棧是跟隨線程的,有線程就有棧,堆是跟隨JVM的,有JVM就有堆內(nèi)存。
2.堆內(nèi)存中到底存在什么:對象,包括對象變量和對象方法。
3.類變量和實例變量有什么區(qū)別:靜態(tài)變量(有static修飾)是類變量,非靜態(tài)變量是實例變量。靜態(tài)變量存在方法區(qū)中,實例變量存在堆內(nèi)存中。有個說法是類變量是在JVM啟動時就初始化好了,其實不對。
4.Java的方法到底是傳值還是傳引用:都不是,而是以傳值的方式傳遞地址,具體的說就是原始數(shù)據(jù)類型傳遞的值,引用類型傳遞的地址。對于原始數(shù)據(jù)類型,JVM的處理方法是從Method Area或Heap中拷貝到Stack,然后運行Frame中方法,運行完畢再將變量拷貝回去。
5.為什么會產(chǎn)生OutOfMemory:原因是Heap內(nèi)存中沒有可用空間了或永久區(qū)滿了,有時會發(fā)現(xiàn)對象不多仍出現(xiàn)該情況,一般是由繼承層次過多造成,因為Heap中產(chǎn)生的對象都是先產(chǎn)生父類,然后產(chǎn)生子類。
6.為什么會產(chǎn)生StackOverFlowError:因為線程把??臻g消耗完了,一般都是遞歸函數(shù)造成的。
7.JVM中那些共享的,那些是私有的:Heap和Method Area是共享的,其他都是私有的。
8.還有那些需要注意的補充概念:常量池(constant pool),按照順序存放程序中的常量,且進行索引編號,默認0到127放在常量池,string也是;安全管理器(Security Manager),提供java運行期的安全控制,類加載器只有在通過認證后才能加載class文件;方法索引表(Methods table),記錄每個method的地址信息,Stack和Heap中的地址指針其實指向Methods table的地址。
9.為什么不能調(diào)用System.gc():因為該操作會進行Full GC并停止所有活動。
10.CGLib是什么:用于Spring和Hibernate等技術(shù)對類進行增強時,其可以直接操作字節(jié)碼動態(tài)生成Class文件。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。