線程并發(fā)庫&JVM優(yōu)化

1. 線程池

1.1 為什么用線程池?

  1. 線程復用(創(chuàng)建/銷毀線程伴隨著系統(tǒng)開銷,過于頻繁的創(chuàng)建/銷毀線程,會很大程度上影響處理效率)
  2. 控制并發(fā)數(shù)量(線程并發(fā)數(shù)量過多,搶占系統(tǒng)資源從而導致阻塞)
  3. 管理線程(對線程進行一些簡單的管理)

1.2 任務被添加進線程池的執(zhí)行策略

  • 線程數(shù)量未達到corePoolSize,則新建一個線程(核心線程)執(zhí)行任務
  • 線程數(shù)量達到了corePoolSize,則將任務移入隊列等待空閑線程將其取出去執(zhí)行(通過getTask()方法從阻塞隊列中獲取等待的任務,如果隊列中沒有任務,getTask方法會被阻塞并掛起,不會占用cpu資源,整個getTask操作在自旋下完成)
  • 隊列已滿,新建線程(非核心線程)執(zhí)行任務
  • 隊列已滿,總線程數(shù)又達到了maximumPoolSize,就會執(zhí)行任務拒絕策略。

1.3 常見四種線程池

1.3.1 可緩存線程池CachedThreadPool()

  • 這種線程池內(nèi)部沒有核心線程,線程的數(shù)量是有沒限制的。
  • 在創(chuàng)建任務時,若有空閑的線程時則復用空閑的線程,若沒有則新建線程。
  • 沒有工作的線程(閑置狀態(tài))在超過了60S還不做事,就會銷毀。
  • 適用:執(zhí)行很多短期異步的小程序或者負載較輕的服務器。

1.3.2 定長線程池FixedThreadPool

  • 該線程池的最大線程數(shù)等于核心線程數(shù),所以在默認情況下,該線程池的線程不會因為閑置狀態(tài)超時而被銷毀
  • 如果當前線程數(shù)小于核心線程數(shù),并且也有閑置線程的時候提交了任務,這時也不會去復用之前的閑置線程,會創(chuàng)建新的線程去執(zhí)行任務。如果當前執(zhí)行任務數(shù)大于了核心線程數(shù),大于的部分就會進入隊列等待。等著有閑置的線程來執(zhí)行這個任務。
  • 適用:執(zhí)行長期的任務,性能好很多。

1.3.3 單線程池SingleThreadPool

  • 有且僅有一個工作線程執(zhí)行任務
  • 所有任務按照指定順序執(zhí)行,即遵循隊列的入隊出隊規(guī)則。
  • 適用:一個任務一個任務執(zhí)行的場景。

1.3.4 調(diào)度線程池ScheduledThreadPool

  • DEFAULT_KEEPALIVE_MILLIS就是默認10L,這里就是10秒。這個線程池有點像是CachedThreadPool和FixedThreadPool 結(jié)合了一下。
  • 不僅設置了核心線程數(shù),最大線程數(shù)也是Integer.MAX_VALUE。
  • 這個線程池是上述4個中唯一一個有延遲執(zhí)行和周期執(zhí)行任務的線程池。
  • 適用:周期性執(zhí)行任務的場景(定期的同步數(shù)據(jù))

總結(jié):除了new ScheduledThreadPool 的內(nèi)部實現(xiàn)特殊一點之外,其它線程池內(nèi)部都是基于ThreadPoolExecutor類(Executor的子類)實現(xiàn)的。

1.4 ThreadPoolExecutor類構(gòu)造器語法形式:

ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,timeUnit,workQueue,threadFactory,handle);

方法參數(shù):

  1. corePoolSize:核心線程數(shù)(最小存活的工作線程數(shù)量)
  2. maxPoolSize:最大線程數(shù)
  3. keepAliveTime:線程存活時間(在corePoreSize<maxPoolSize情況下有用,線程的空閑時間超過了keepAliveTime就會銷毀)
  4. timeUnit:存活時間的時間單位
  5. workQueue:阻塞隊列,用來保存等待被執(zhí)行的任務(①synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執(zhí)行新來的任務;②LinkedBlockingQueue:基于鏈表的先進先出隊列,如果創(chuàng)建時沒有指定此隊列大小,則默認為Integer.MAX_VALUE;③ArrayBlockingQueue:基于數(shù)組的先進先出隊列,此隊列創(chuàng)建時必須指定大?。?/li>
  6. threadFactory:線程工廠,主要用來創(chuàng)建線程;
  7. handler:表示當拒絕處理任務時的策略(①丟棄任務并拋出RejectedExecutionException異常;②丟棄任務,但是不拋出異常;③丟棄隊列最前面的任務,然后重新嘗試執(zhí)行任務;④由調(diào)用線程處理該任務)

1.5 在ThreadPoolExecutor類中幾個重要的方法

  1. execute()

    實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現(xiàn),這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執(zhí)行。

  2. submit()

    是在ExecutorService中聲明的方法,在AbstractExecutorService就已經(jīng)有了具體的實現(xiàn),在ThreadPoolExecutor中并沒有對其進行重寫,這個方法也是用來向線程池提交任務的,實際上它還是調(diào)用的execute()方法,只不過它利用了Future來獲取任務執(zhí)行結(jié)果。

  3. shutdown()

    不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執(zhí)行完后才終止,但再也不會接受新的任務。

  4. shutdownNow()

    立即終止線程池,并嘗試打斷正在執(zhí)行的任務,并且清空任務緩存隊列,返回尚未執(zhí)行的任務。

  5. isTerminated()

    調(diào)用ExecutorService.shutdown方法的時候,線程池不再接收任何新任務,但此時線程池并不會立刻退出,直到添加到線程池中的任務都已經(jīng)處理完成,才會退出。在調(diào)用shutdown方法后我們可以在一個死循環(huán)里面用isTerminated方法判斷是否線程池中的所有線程已經(jīng)執(zhí)行完畢,如果子線程都結(jié)束了,我們就可以做關(guān)閉流等后續(xù)操作了。

1.6 線程池中的最大線程數(shù)

  • 一般說來,線程池的大小經(jīng)驗值應該這樣設置:(其中N為CPU的個數(shù))
  1. 如果是CPU密集型應用,則線程池大小設置為N+1
  2. 如果是IO密集型應用,則線程池大小設置為2N+1
  • 但是,IO優(yōu)化中,這樣的估算公式可能更適合:
  1. 最佳線程數(shù)目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數(shù)目
  2. 因為很顯然,線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。

2. JVM優(yōu)化

2.1 JVM的作用

JVM屏蔽了平臺的不同,提供了統(tǒng)一的運行環(huán)境,讓Java代碼無需考慮平臺的差異,運行在相同的環(huán)境中.

2.2 JVM的組成

[圖片上傳失敗...(image-9e6488-1569804516966)]
大致分為以下組件:

  1. 類加載器子系統(tǒng)
  2. 運行時數(shù)據(jù)區(qū)
    方法區(qū) 堆 虛擬機棧 本地方法棧 程序計數(shù)器
  3. 執(zhí)行引擎
  4. 本地方法庫

2.2.1 類加載器子系統(tǒng)

2.2.1.1 類加載的過程

  1. 加載:找到字節(jié)碼文件,讀取到內(nèi)存中.類的加載方式分為隱式加載和顯示加載兩種。隱式加載指的是程序在使用new關(guān)鍵詞創(chuàng)建對象時,會隱式的調(diào)用類的加載器把對應的類加載到jvm中。顯示加載指的是通過直接調(diào)用class.forName()方法來把所需的類加載到jvm中。
  2. 驗證:驗證此字節(jié)碼文件是不是真的是一個字節(jié)碼文件,畢竟后綴名可以隨便改,而內(nèi)在的身份標識是不會變的.在確認是一個字節(jié)碼文件后,還會檢查一系列的是否可運行驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證等.Java虛擬機規(guī)范對此要求很嚴格,在Java 7的規(guī)范中,已經(jīng)有130頁的描述驗證過程的內(nèi)容.
  3. 準備:為類中static修飾的變量分配內(nèi)存空間并設置其初始值為0或null.可能會有人感覺奇怪,在類中定義一個static修飾的int,并賦值了123,為什么這里還是賦值0.因為這個int的123是在初始化階段的時候才賦值的,這里只是先把內(nèi)存分配好.但如果你的static修飾還加上了final,那么就會在準備階段就會賦值.
  4. 解析:解析階段會將java代碼中的符號引用替換為直接引用.比如引用的是一個類,我們在代碼中只有全限定名來標識它,在這個階段會找到這個類加載到內(nèi)存中的地址.
    初始化:如剛才準備階段所說的,這個階段就是對變量的賦值的階段

2.2.1.2 類與類加載器

每一個類,都需要和它的類加載器一起確定其在JVM中的唯一性.換句話來說,不同類加載器加載的同一個字節(jié)碼文件,得到的類都不相等.我們可以通過默認加載器去加載一個類,然后new一個對象,再通過自己定義的一個類加載器,去加載同一個字節(jié)碼文件,拿前面得到的對象去instanceof,會得到的結(jié)果是false.

2.2.1.3 雙親委派機制

[圖片上傳失敗...(image-7084f3-1569804516966)]

類加載器一般有4種,其中前3種是必然存在的

  1. 啟動類加載器:加載<JAVA_HOME>\lib下的
  2. 擴展類加載器:加載<JAVA_HOME>\lib\ext下的
  3. 應用程序類加載器:加載Classpath下的
  4. 自定義類加載器(這種一般大企業(yè)才會有)

而雙親委派機制是如何運作的呢?

  1. 我們以應用程序類加載器舉例,它在需要加載一個類的時候,不會直接去嘗試加載,而是委托上級的擴展類加載器去加載,而擴展類加載器也是委托啟動類加載器去加載.
  2. 啟動類加載器在自己的搜索范圍內(nèi)沒有找到這么一個類,表示自己無法加載,就再讓擴展類加載器去加載,同樣的,擴展類加載器在自己的搜索范圍內(nèi)找一遍,如果還是沒有找到,就委托應用程序類加載器去加載.如果最終還是沒找到,那就會直接拋出異常了.

而為什么要這么麻煩的從下到上,再從上到下呢?
這是為了安全著想,保證按照優(yōu)先級加載.如果用戶自己編寫一個名為java.lang.Object的類,放到自己的Classpath中,沒有這種優(yōu)先級保證,應用程序類加載器就把這個當做Object加載到了內(nèi)存中,從而會引發(fā)一片混亂.而憑借這種雙親委派機制,先一路向上委托,啟動類加載器去找的時候,就把正確的Object加載到了內(nèi)存中,后面再加載自行編寫的Object的時候,是不會加載運行的.

2.2.2 運行時數(shù)據(jù)區(qū)

運行時數(shù)據(jù)區(qū)分為虛擬機棧,本地方法棧,堆區(qū),方法區(qū)和程序計數(shù)器.

2.2.2.1 程序計數(shù)器

程序計數(shù)器是線程私有的,雖然名字叫計數(shù)器,但主要用途還是用來確定指令的執(zhí)行順序,比如循環(huán),分支,跳轉(zhuǎn),異常捕獲等.而JVM對于多線程的實現(xiàn)是通過輪流切換線程實現(xiàn)的,所以為了保證每個線程都能按正確順序執(zhí)行,將程序計數(shù)器作為線程私有.程序計數(shù)器是唯一一個JVM沒有規(guī)定任何OOM的區(qū)塊.

OOM:out of memory

程序計數(shù)器是一塊非常小的內(nèi)存空間,可以看做是當前線程執(zhí)行字節(jié)碼的行號指示器,每個線程都有一個獨立的程序計數(shù)器,因此程序計數(shù)器是線程私有的一塊空間,此外,程序計數(shù)器是Java虛擬機規(guī)定的唯一不會發(fā)生內(nèi)存溢出的區(qū)域。

2.2.2.2 Java虛擬機棧

  1. Java虛擬機棧也是線程私有的,每個方法執(zhí)行都會創(chuàng)建一個棧幀,局部變量就存放在棧幀中,還有一些其他的動態(tài)鏈接之類的.通常有兩個錯誤會跟這個有關(guān)系,一個是StackOverFlowError,一個是OOM(OutOfMemoryError).前者是因為線程請求棧深度超出虛擬機所允許的范圍,后者是動態(tài)擴展棧的大小的時候,申請不到足夠的內(nèi)存空間.而前者提到的棧深度,也就是剛才說到的每個方法會創(chuàng)建一個棧幀,棧幀從開始執(zhí)行方法時壓入Java虛擬機棧,執(zhí)行完的時候彈出棧.當壓入的棧幀太多了,就會報出這個StackOverflowError.
  2. 虛擬機會為每個線程分配一個虛擬機棧,每個虛擬機棧中都有若干個棧幀,每個棧幀中存儲了局部變量表、操作數(shù)棧、動態(tài)鏈接、返回地址等。一個棧幀就對應Java代碼中的一個方法,當線程執(zhí)行到一個方法時,就代表這個方法對應的棧幀已經(jīng)進入虛擬機棧并且處于棧頂?shù)奈恢?,每一個Java方法從被調(diào)用到執(zhí)行結(jié)束,就對應了一個棧幀從入棧到出棧的過程。

2.2.2.3 本地方法棧

本地方法棧與虛擬機棧的區(qū)別是,虛擬機棧執(zhí)行的是Java方法,本地方法棧執(zhí)行的是本地方法(Native Method),其他基本上一致,在HotSpot中直接把本地方法棧和虛擬機棧合二為一,這里暫時不做過多敘述。

2.2.2.4 堆內(nèi)存

確切來說JVM規(guī)范中方法區(qū)就是堆的一個邏輯分區(qū),就是一個所有線程共享的,存放對象的區(qū)域,也是GC的主要區(qū)域.其中的分區(qū)分為新生代,老年代。新生代中又可以細分為一個Eden,兩個Survivor區(qū)(From,To)。Eden中存放的是通過new或者newInstance方法創(chuàng)建出來的對象,絕大多數(shù)都是很短命的。正常情況下經(jīng)歷一次gc之后,存活的對象會轉(zhuǎn)入到其中一個Survivor區(qū),然后再經(jīng)歷默認15次的gc,就轉(zhuǎn)入到老年代。

堆內(nèi)存主要用于存放對象和數(shù)組,它是JVM管理的內(nèi)存中最大的一塊區(qū)域,堆內(nèi)存和方法區(qū)都被所有線程共享,在虛擬機啟動時創(chuàng)建。在垃圾收集的層面上來看,由于現(xiàn)在收集器基本上都采用分代收集算法,因此堆還可以分為新生代(YoungGeneration)和老年代(OldGeneration),新生代還可以分為Eden、From Survivor、To Survivor

堆是垃圾回收主要區(qū)域:

  1. 新生代 Eden、From Survivor、To Survivor 垃圾回收使用Minor GC
  2. 老年代垃圾回收使用Full GC

2.2.2.5 元空間

上面說到,jdk1.8中,已經(jīng)不存在永久代(方法區(qū)),替代它的一塊空間叫做“元空間”,和永久代類似,都是JVM規(guī)范對方法區(qū)的實現(xiàn),但是元空間并不在虛擬機中,而是使用本地內(nèi)存,元空間的大小僅受本地內(nèi)存限制,但可以通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize來指定元空間的大小

2.2.3 JVM內(nèi)存溢出

2.2.3.1 堆內(nèi)存溢出

堆內(nèi)存中主要存放對象、數(shù)組等,只要不斷地創(chuàng)建這些對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾收集回收機制清除這些對象,當這些對象所占空間超過最大堆容量時,就會產(chǎn)生OutOfMemoryError的異常。

看到 java.lang.OutOfMemoryError: Java heap space 的信息,說明在堆內(nèi)存空間產(chǎn)生內(nèi)存溢出的異常。

新產(chǎn)生的對象最初分配在新生代,新生代滿后會進行一次Minor GC,如果Minor GC后空間不足會把該對象和新生代滿足條件的對象放入老年代,老年代空間不足時會進行Full GC,之后如果空間還不足以存放新對象則拋出OutOfMemoryError異常。常見原因:內(nèi)存中加載的數(shù)據(jù)過多如一次從數(shù)據(jù)庫中取出過多數(shù)據(jù);集合對對象引用過多且使用完后沒有清空;代碼中存在死循環(huán)或循環(huán)產(chǎn)生過多重復對象;堆內(nèi)存分配不合理;網(wǎng)絡連接問題、數(shù)據(jù)庫問題等。

2.2.3.2 虛擬機棧/本地方法棧溢出

  1. StackOverflowError:當線程請求的棧的深度大于虛擬機所允許的最大深度,則拋出StackOverflowError,簡單理解就是虛擬機棧中的棧幀數(shù)量過多(一個線程嵌套調(diào)用的方法數(shù)量過多)時,就會拋出StackOverflowError異常。最常見的場景就是方法無限遞歸調(diào)用
  2. OutOfMemoryError:如果虛擬機在擴展棧時無法申請到足夠的內(nèi)存空間,則拋出OutOfMemoryError。

在線程較少的時候,某個線程請求深度過大,會報StackOverflow異常,解決這種問題可以適當加大棧的深度(增加??臻g大?。簿褪前?Xss的值設置大一些,但一般情況下是代碼問題的可能性較大;在虛擬機產(chǎn)生線程時,無法為該線程申請??臻g了,會報OutOfMemoryError異常,解決這種問題可以適當減小棧的深度,也就是把-Xss的值設置小一些,每個線程占用的空間小了,總空間一定就能容納更多的線程,但是操作系統(tǒng)對一個進程的線程數(shù)有限制,經(jīng)驗值在3000~5000左右。在jdk1.5之前-Xss默認是256k,jdk1.5之后默認是1M,這個選項對系統(tǒng)硬性還是蠻大的,設置時要根據(jù)實際情況,謹慎操作。

2.2.3.3 方法區(qū)溢出

  1. 方法區(qū)主要用于存儲虛擬機加載的類信息、常量、靜態(tài)變量,以及編譯器編譯后的代碼等數(shù)據(jù),所以方法區(qū)溢出的原因就是沒有足夠的內(nèi)存來存放這些數(shù)據(jù)。
  2. 由于在jdk1.6之前字符串常量池是存在于方法區(qū)中的,所以基于jdk1.6之前的虛擬機,可以通過不斷產(chǎn)生不一致的字符串(同時要保證和GC Roots之間保證有可達路徑)來模擬方法區(qū)的OutOfMemoryError異常;但方法區(qū)還存儲加載的類信息,所以基于jdk1.7的虛擬機,可以通過動態(tài)不斷創(chuàng)建大量的類來模擬方法區(qū)溢出。

2.2.3.4 本機直接內(nèi)存溢出

本機直接內(nèi)存(DirectMemory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但Java中用到NIO相關(guān)操作時(比如ByteBuffer的allocteDirect方法申請的是本機直接內(nèi)存),也可能會出現(xiàn)內(nèi)存溢出的異常。

2.2.4 JVM垃圾回收

垃圾回收,就是通過垃圾收集器把內(nèi)存中沒用的對象清理掉。

垃圾回收涉及到的內(nèi)容有:

  1. 判斷對象是否已死;
  2. 選擇垃圾收集算法;
  3. 選擇垃圾收集的時間;
  4. 選擇適當?shù)睦占髑謇砝ㄒ阉赖膶ο螅?/li>

2.2.4.1 判斷對象是否為垃圾

  1. 引用計數(shù)算法
    • 給每一個對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值加1;每當有一個地方不再引用它時,計數(shù)器值減1,這樣只要計數(shù)器的值不為0,就說明還有地方引用它,它就不是無用的對象。如下圖,對象2有1個引用,它的引用計數(shù)器值為1,對象1有兩個地方引用,它的引用計數(shù)器值為2 。
    • 這種方法看起來非常簡單,但目前許多主流的虛擬機都沒有選用這種算法來管理內(nèi)存,原因就是當某些對象之間互相引用時,無法判斷出這些對象是否已死,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數(shù)器的值仍然是1,無法判斷他們是死對象,垃圾回收器也就無法回收。
  2. 可達性分析算法
    • 了解可達性分析算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為GC Roots的有虛擬機棧中本地變量表中引用的對象、方法區(qū)中靜態(tài)屬性引用的對象、方法區(qū)中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。
    • 當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。
    • 被判了死刑的對象(object5、object6、object7)并不是必死無疑,還有挽救的余地。進行可達性分析后對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接著會判斷如果對象沒有覆蓋Object的finalize()方法或者finalize()方法已經(jīng)被虛擬機調(diào)用過,那么它們就會被行刑(清除);如果對象覆蓋了finalize()方法且還沒有被調(diào)用,則會執(zhí)行finalize()方法中的內(nèi)容,所以在finalize()方法中如果重新與GC Roots引用鏈上的對象關(guān)聯(lián)就可以拯救自己,但是一般不建議這么做.
  3. 方法區(qū)回收
    • 上面說的都是對堆內(nèi)存中對象的判斷,方法區(qū)中主要回收的是廢棄的常量和無用的類
    • 判斷常量是否廢棄可以判斷是否有地方引用這個常量,如果沒有引用則為廢棄的常量。
    • 判斷類是否廢棄需要同時滿足如下條件:
      • 該類所有的實例已經(jīng)被回收(堆中不存在任何該類的實例)
      • 加載該類的ClassLoader已經(jīng)被回收
      • 該類對應的java.lang.Class對象在任何地方?jīng)]有被引用(無法通過反射訪問該類的方法)

2.2.4.2 常用垃圾回收算法

常用的垃圾回收算法有三種:標記-清除算法、復制算法、標記-整理算法。

  1. 標記-清除算法:
    • 分為標記和清除兩個階段,首先標記出所有需要回收的對象,標記完成后統(tǒng)一回收所有被標記的對象
    • 缺點:標記和清除兩個過程效率都不高;標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片。
  2. 復制算法:
    • 把內(nèi)存分為大小相等的兩塊,每次存儲只用其中一塊,當這一塊用完了,就把存活的對象全部復制到另一塊上,同時把使用過的這塊內(nèi)存空間全部清理掉,往復循環(huán)
    • 缺點:實際可使用的內(nèi)存空間縮小為原來的一半
  3. 標記-整理算法:
    • 先對可用的對象進行標記,然后所有被標記的對象向一段移動,最后清除可用對象邊界以外的內(nèi)存
  4. 分代收集算法:
    • 把堆內(nèi)存分為新生代和老年代,新生代又分為Eden區(qū)、From Survivor和To Survivor。一般新生代中的對象基本上都是朝生夕滅的,每次只有少量對象存活,因此采用復制算法,只需要復制那些少量存活的對象就可以完成垃圾收集;老年代中的對象存活率較高,就采用標記-清除和標記-整理算法來進行回收。
    • 在這些區(qū)域的垃圾回收大概有如下幾種情況:
      • 新生代使用時minor gc
      • 老年代使用的full gc
  • 大多數(shù)情況下,新的對象都分配在Eden區(qū),當Eden區(qū)沒有空間進行分配時,將進行一次Minor GC,清理Eden區(qū)中的無用對象。清理后,Eden和From Survivor中的存活對象如果小于To Survivor的可用空間則進入To Survivor,否則直接進入老年代);Eden和From Survivor中還存活且能夠進入To Survivor的對象年齡增加1歲(虛擬機為每個對象定義了一個年齡計數(shù)器,每執(zhí)行一次Minor GC年齡加1),當存活對象的年齡到達一定程度(默認15歲)后進入老年代,可以通過-XX:MaxTenuringThreshold來設置年齡的值。
  • 當進行了Minor GC后,Eden還不足以為新對象分配空間(那這個新對象肯定很大),新對象直接進入老年代。
  • 占To Survivor空間一半以上且年齡相等的對象,大于等于該年齡的對象直接進入老年代,比如Survivor空間是10M,有幾個年齡為4的對象占用總空間已經(jīng)超過5M,則年齡大于等于4的對象都直接進入老年代,不需要等到MaxTenuringThreshold指定的歲數(shù)。
  • 在進行Minor GC之前,會判斷老年代最大連續(xù)可用空間是否大于新生代所有對象總空間,如果大于,說明Minor GC是安全的,否則會判斷是否允許擔保失敗,如果允許,判斷老年代最大連續(xù)可用空間是否大于歷次晉升到老年代的對象的平均大小,如果大于,則執(zhí)行Minor GC,否則執(zhí)行Full GC。
  • 當在java代碼里直接調(diào)用System.gc()時,會建議JVM進行Full GC,但一般情況下都會觸發(fā)Full GC,一般不建議使用,盡量讓虛擬機自己管理GC的策略。
  • 永久代(方法區(qū))中用于存放類信息,jdk1.6及之前的版本永久代中還存儲常量、靜態(tài)變量等,當永久代的空間不足時,也會觸發(fā)Full GC,如果經(jīng)過Full GC還無法滿足永久代存放新數(shù)據(jù)的需求,就會拋出永久代的內(nèi)存溢出異常。
  • 大對象(需要大量連續(xù)內(nèi)存的對象)例如很長的數(shù)組,會直接進入老年代,如果老年代沒有足夠的連續(xù)大空間來存放,則會進行Full GC。

Minor GC和Full GC

  • 在說這兩種回收的區(qū)別之前,我們先來說一個概念,“Stop-The-World”。
  • 如字面意思,每次垃圾回收的時候,都會將整個JVM暫停,回收完成后再繼續(xù)。如果一邊增加廢棄對象,一邊進行垃圾回收,完成工作似乎就變得遙遙無期了。
  • 而一般來說,我們把新生代的回收稱為Minor GC,Minor意思是次要的,新生代的回收一般回收很快,采用復制算法,造成的暫停時間很短。而Full GC一般是老年代的回收,并伴隨至少一次的Minor GC,新生代和老年代都回收,而老年代采用標記-整理算法,這種GC每次都比較慢,造成的暫停時間比較長,通常是Minor GC時間的10倍以上。
  • 所以很明顯,我們需要盡量通過Minor GC來回收內(nèi)存,而盡量少的觸發(fā)Full GC。畢竟系統(tǒng)運行一會兒就要因為GC卡住一段時間,再加上其他的同步阻塞,整個系統(tǒng)給人的感覺就是又卡又慢。

2.2.4.3 選擇垃圾收集的時間

  • 當程序運行時,各種數(shù)據(jù)、對象、線程、內(nèi)存等都時刻在發(fā)生變化,當下達垃圾收集命令后就立刻進行收集嗎?肯定不是。這里來了解兩個概念:安全點(safepoint)和安全區(qū)(safe region)。
  • 安全點:從線程角度看,安全點可以理解為是在代碼執(zhí)行過程中的一些特殊位置,當線程執(zhí)行到安全點的時候,說明虛擬機當前的狀態(tài)是安全的,如果有需要,可以在這里暫停用戶線程。當垃圾收集時,如果需要暫停當前的用戶線程,但用戶線程當時沒在安全點上,則應該等待這些線程執(zhí)行到安全點再暫停。舉個例子,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽等一下,讓我吃完這塊再掃?!眱鹤映酝赀@塊西瓜把瓜皮扔到地上后就是一個安全點,媽媽可以繼續(xù)掃地(垃圾收集器可以繼續(xù)收集垃圾)。理論上,解釋器的每條字節(jié)碼的邊界上都可以放一個安全點,實際上,安全點基本上以“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定。
  • 安全區(qū):安全點是相對于運行中的線程來說的,對于如sleep或blocked等狀態(tài)的線程,收集器不會等待這些線程被分配CPU時間,這時候只要線程處于安全區(qū)中,就可以算是安全的。安全區(qū)就是在一段代碼片段中,引用關(guān)系不會發(fā)生變化,可以看作是被擴展、拉長了的安全點。還以上面的例子說明,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽你繼續(xù)掃地吧,我還得吃10分鐘呢!”兒子吃瓜的這段時間就是安全區(qū),媽媽可以繼續(xù)掃地(垃圾收集器可以繼續(xù)收集垃圾)。 一段連續(xù)的安全點

2.2.4.4 常見垃圾收集器

現(xiàn)在常見的垃圾收集器有如下幾種:

  1. 新生代收集器:Serial、ParNew、Parallel Scavenge
  2. 老年代收集器:Serial Old、CMS、Parallel Old

堆內(nèi)存垃圾收集器:G1

  1. Serial 收集器
  2. ParNew 收集器
  3. Parallel Scavenge 收集器
  4. Serial Old收集器
  5. CMS(Concurrent Mark Sweep) 收集器
  6. Parallel Old 收集器
  7. G1 收集器

適用場景:要求盡可能可控GC停頓時間;內(nèi)存占用較大的應用??梢杂?XX:+UseG1GC使用G1收集器,jdk9默認使用G1收集器。

2.3 JVM的優(yōu)化

JVM調(diào)優(yōu)目標:使用較小的內(nèi)存占用來獲得較高的吞吐量或者較低的延遲。

程序在上線前的測試或運行中有時會出現(xiàn)一些大大小小的JVM問題,比如cpu load過高、請求延遲、tps降低等,甚至出現(xiàn)內(nèi)存泄漏(每次垃圾收集使用的時間越來越長,垃圾收集頻率越來越高,每次垃圾收集清理掉的垃圾數(shù)據(jù)越來越少)、內(nèi)存溢出導致系統(tǒng)崩潰,因此需要對JVM進行調(diào)優(yōu),使得程序在正常運行的前提下,獲得更高的用戶體驗和運行效率。

  • 內(nèi)存占用:程序正常運行需要的內(nèi)存大小。
  • 延遲:由于垃圾收集而引起的程序停頓時間。
  • 吞吐量:用戶程序運行時間占用戶程序和垃圾收集占用總時間的比值。

當然,和CAP原則一樣,同時滿足一個程序內(nèi)存占用小、延遲低、高吞吐量是不可能的,程序的目標不同,調(diào)優(yōu)時所考慮的方向也不同,在調(diào)優(yōu)之前,必須要結(jié)合實際場景,有明確的的優(yōu)化目標,找到性能瓶頸,對瓶頸有針對性的優(yōu)化,最后進行測試,通過各種監(jiān)控工具確認調(diào)優(yōu)后的結(jié)果是否符合目標。

調(diào)優(yōu)可以依賴、參考的數(shù)據(jù)有系統(tǒng)運行日志、堆棧錯誤信息、gc日志、線程快照、堆轉(zhuǎn)儲快照等。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡上收集的一些資料的整理,因此不免有一些不準確的地方,同時不同JDK版本的...
    高廣超閱讀 16,040評論 3 83
  • 《深入理解Java虛擬機》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,456評論 1 34
  • 第二部分 自動內(nèi)存管理機制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運行數(shù)據(jù)區(qū)域 程序計數(shù)器:當前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,275評論 0 2
  • http://www.cnblogs.com/angeldevil/p/3801189.html值得一看 Clas...
    snail_knight閱讀 1,610評論 1 0
  • 內(nèi)存溢出和內(nèi)存泄漏的區(qū)別 內(nèi)存溢出:out of memory,是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,...
    Aimerwhy閱讀 799評論 0 1

友情鏈接更多精彩內(nèi)容