- 1. 線程池
- 2. JVM優化
1. 線程池
1.1 為什么用線程池?
- 線程復用(創建/銷毀線程伴隨著系統開銷,過于頻繁的創建/銷毀線程,會很大程度上影響處理效率)
- 控制并發數量(線程并發數量過多,搶占系統資源從而導致阻塞)
- 管理線程(對線程進行一些簡單的管理)
1.2 任務被添加進線程池的執行策略
- 線程數量未達到corePoolSize,則新建一個線程(核心線程)執行任務
- 線程數量達到了corePoolSize,則將任務移入隊列等待空閑線程將其取出去執行(通過getTask()方法從阻塞隊列中獲取等待的任務,如果隊列中沒有任務,getTask方法會被阻塞并掛起,不會占用cpu資源,整個getTask操作在自旋下完成)
- 隊列已滿,新建線程(非核心線程)執行任務
- 隊列已滿,總線程數又達到了maximumPoolSize,就會執行任務拒絕策略。
1.3 常見四種線程池
1.3.1 可緩存線程池CachedThreadPool()
- 這種線程池內部沒有核心線程,線程的數量是有沒限制的。
- 在創建任務時,若有空閑的線程時則復用空閑的線程,若沒有則新建線程。
- 沒有工作的線程(閑置狀態)在超過了60S還不做事,就會銷毀。
- 適用:執行很多短期異步的小程序或者負載較輕的服務器。
1.3.2 定長線程池FixedThreadPool
- 該線程池的最大線程數等于核心線程數,所以在默認情況下,該線程池的線程不會因為閑置狀態超時而被銷毀
- 如果當前線程數小于核心線程數,并且也有閑置線程的時候提交了任務,這時也不會去復用之前的閑置線程,會創建新的線程去執行任務。如果當前執行任務數大于了核心線程數,大于的部分就會進入隊列等待。等著有閑置的線程來執行這個任務。
- 適用:執行長期的任務,性能好很多。
1.3.3 單線程池SingleThreadPool
- 有且僅有一個工作線程執行任務
- 所有任務按照指定順序執行,即遵循隊列的入隊出隊規則。
- 適用:一個任務一個任務執行的場景。
1.3.4 調度線程池ScheduledThreadPool
- DEFAULT_KEEPALIVE_MILLIS就是默認10L,這里就是10秒。這個線程池有點像是CachedThreadPool和FixedThreadPool 結合了一下。
- 不僅設置了核心線程數,最大線程數也是Integer.MAX_VALUE。
- 這個線程池是上述4個中唯一一個有延遲執行和周期執行任務的線程池。
- 適用:周期性執行任務的場景(定期的同步數據)
總結:除了new ScheduledThreadPool 的內部實現特殊一點之外,其它線程池內部都是基于ThreadPoolExecutor類(Executor的子類)實現的。
1.4 ThreadPoolExecutor類構造器語法形式:
ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,timeUnit,workQueue,threadFactory,handle);
方法參數:
- corePoolSize:核心線程數(最小存活的工作線程數量)
- maxPoolSize:最大線程數
- keepAliveTime:線程存活時間(在corePoreSize<maxPoolSize情況下有用,線程的空閑時間超過了keepAliveTime就會銷毀)
- timeUnit:存活時間的時間單位
- workQueue:阻塞隊列,用來保存等待被執行的任務(①synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務;②LinkedBlockingQueue:基于鏈表的先進先出隊列,如果創建時沒有指定此隊列大小,則默認為Integer.MAX_VALUE;③ArrayBlockingQueue:基于數組的先進先出隊列,此隊列創建時必須指定大小)
- threadFactory:線程工廠,主要用來創建線程;
- handler:表示當拒絕處理任務時的策略(①丟棄任務并拋出RejectedExecutionException異常;②丟棄任務,但是不拋出異常;③丟棄隊列最前面的任務,然后重新嘗試執行任務;④由調用線程處理該任務)
1.5 在ThreadPoolExecutor類中幾個重要的方法
-
execute()
實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執行。
-
submit()
是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中并沒有對其進行重寫,這個方法也是用來向線程池提交任務的,實際上它還是調用的execute()方法,只不過它利用了Future來獲取任務執行結果。
-
shutdown()
不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完后才終止,但再也不會接受新的任務。
-
shutdownNow()
立即終止線程池,并嘗試打斷正在執行的任務,并且清空任務緩存隊列,返回尚未執行的任務。
-
isTerminated()
調用ExecutorService.shutdown方法的時候,線程池不再接收任何新任務,但此時線程池并不會立刻退出,直到添加到線程池中的任務都已經處理完成,才會退出。在調用shutdown方法后我們可以在一個死循環里面用isTerminated方法判斷是否線程池中的所有線程已經執行完畢,如果子線程都結束了,我們就可以做關閉流等后續操作了。
1.6 線程池中的最大線程數
- 一般說來,線程池的大小經驗值應該這樣設置:(其中N為CPU的個數)
- 如果是CPU密集型應用,則線程池大小設置為N+1
- 如果是IO密集型應用,則線程池大小設置為2N+1
- 但是,IO優化中,這樣的估算公式可能更適合:
- 最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目
- 因為很顯然,線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
2. JVM優化
2.1 JVM的作用
JVM屏蔽了平臺的不同,提供了統一的運行環境,讓Java代碼無需考慮平臺的差異,運行在相同的環境中.
2.2 JVM的組成
[圖片上傳失敗...(image-9e6488-1569804516966)]
大致分為以下組件:
- 類加載器子系統
- 運行時數據區
方法區 堆 虛擬機棧 本地方法棧 程序計數器 - 執行引擎
- 本地方法庫
2.2.1 類加載器子系統
2.2.1.1 類加載的過程
- 加載:找到字節碼文件,讀取到內存中.類的加載方式分為隱式加載和顯示加載兩種。隱式加載指的是程序在使用new關鍵詞創建對象時,會隱式的調用類的加載器把對應的類加載到jvm中。顯示加載指的是通過直接調用class.forName()方法來把所需的類加載到jvm中。
- 驗證:驗證此字節碼文件是不是真的是一個字節碼文件,畢竟后綴名可以隨便改,而內在的身份標識是不會變的.在確認是一個字節碼文件后,還會檢查一系列的是否可運行驗證,元數據驗證,字節碼驗證,符號引用驗證等.Java虛擬機規范對此要求很嚴格,在Java 7的規范中,已經有130頁的描述驗證過程的內容.
- 準備:為類中static修飾的變量分配內存空間并設置其初始值為0或null.可能會有人感覺奇怪,在類中定義一個static修飾的int,并賦值了123,為什么這里還是賦值0.因為這個int的123是在初始化階段的時候才賦值的,這里只是先把內存分配好.但如果你的static修飾還加上了final,那么就會在準備階段就會賦值.
- 解析:解析階段會將java代碼中的符號引用替換為直接引用.比如引用的是一個類,我們在代碼中只有全限定名來標識它,在這個階段會找到這個類加載到內存中的地址.
初始化:如剛才準備階段所說的,這個階段就是對變量的賦值的階段
2.2.1.2 類與類加載器
每一個類,都需要和它的類加載器一起確定其在JVM中的唯一性.換句話來說,不同類加載器加載的同一個字節碼文件,得到的類都不相等.我們可以通過默認加載器去加載一個類,然后new一個對象,再通過自己定義的一個類加載器,去加載同一個字節碼文件,拿前面得到的對象去instanceof,會得到的結果是false.
2.2.1.3 雙親委派機制
[圖片上傳失敗...(image-7084f3-1569804516966)]
類加載器一般有4種,其中前3種是必然存在的
- 啟動類加載器:加載<JAVA_HOME>\lib下的
- 擴展類加載器:加載<JAVA_HOME>\lib\ext下的
- 應用程序類加載器:加載Classpath下的
- 自定義類加載器(這種一般大企業才會有)
而雙親委派機制是如何運作的呢?
- 我們以應用程序類加載器舉例,它在需要加載一個類的時候,不會直接去嘗試加載,而是委托上級的擴展類加載器去加載,而擴展類加載器也是委托啟動類加載器去加載.
- 啟動類加載器在自己的搜索范圍內沒有找到這么一個類,表示自己無法加載,就再讓擴展類加載器去加載,同樣的,擴展類加載器在自己的搜索范圍內找一遍,如果還是沒有找到,就委托應用程序類加載器去加載.如果最終還是沒找到,那就會直接拋出異常了.
而為什么要這么麻煩的從下到上,再從上到下呢?
這是為了安全著想,保證按照優先級加載.如果用戶自己編寫一個名為java.lang.Object的類,放到自己的Classpath中,沒有這種優先級保證,應用程序類加載器就把這個當做Object加載到了內存中,從而會引發一片混亂.而憑借這種雙親委派機制,先一路向上委托,啟動類加載器去找的時候,就把正確的Object加載到了內存中,后面再加載自行編寫的Object的時候,是不會加載運行的.
2.2.2 運行時數據區
運行時數據區分為虛擬機棧,本地方法棧,堆區,方法區和程序計數器.
2.2.2.1 程序計數器
程序計數器是線程私有的,雖然名字叫計數器,但主要用途還是用來確定指令的執行順序,比如循環,分支,跳轉,異常捕獲等.而JVM對于多線程的實現是通過輪流切換線程實現的,所以為了保證每個線程都能按正確順序執行,將程序計數器作為線程私有.程序計數器是唯一一個JVM沒有規定任何OOM的區塊.
OOM:out of memory
程序計數器是一塊非常小的內存空間,可以看做是當前線程執行字節碼的行號指示器,每個線程都有一個獨立的程序計數器,因此程序計數器是線程私有的一塊空間,此外,程序計數器是Java虛擬機規定的唯一不會發生內存溢出的區域。
2.2.2.2 Java虛擬機棧
- Java虛擬機棧也是線程私有的,每個方法執行都會創建一個棧幀,局部變量就存放在棧幀中,還有一些其他的動態鏈接之類的.通常有兩個錯誤會跟這個有關系,一個是StackOverFlowError,一個是OOM(OutOfMemoryError).前者是因為線程請求棧深度超出虛擬機所允許的范圍,后者是動態擴展棧的大小的時候,申請不到足夠的內存空間.而前者提到的棧深度,也就是剛才說到的每個方法會創建一個棧幀,棧幀從開始執行方法時壓入Java虛擬機棧,執行完的時候彈出棧.當壓入的棧幀太多了,就會報出這個StackOverflowError.
- 虛擬機會為每個線程分配一個虛擬機棧,每個虛擬機棧中都有若干個棧幀,每個棧幀中存儲了局部變量表、操作數棧、動態鏈接、返回地址等。一個棧幀就對應Java代碼中的一個方法,當線程執行到一個方法時,就代表這個方法對應的棧幀已經進入虛擬機棧并且處于棧頂的位置,每一個Java方法從被調用到執行結束,就對應了一個棧幀從入棧到出棧的過程。
2.2.2.3 本地方法棧
本地方法棧與虛擬機棧的區別是,虛擬機棧執行的是Java方法,本地方法棧執行的是本地方法(Native Method),其他基本上一致,在HotSpot中直接把本地方法棧和虛擬機棧合二為一,這里暫時不做過多敘述。
2.2.2.4 堆內存
確切來說JVM規范中方法區就是堆的一個邏輯分區,就是一個所有線程共享的,存放對象的區域,也是GC的主要區域.其中的分區分為新生代,老年代。新生代中又可以細分為一個Eden,兩個Survivor區(From,To)。Eden中存放的是通過new或者newInstance方法創建出來的對象,絕大多數都是很短命的。正常情況下經歷一次gc之后,存活的對象會轉入到其中一個Survivor區,然后再經歷默認15次的gc,就轉入到老年代。
堆內存主要用于存放對象和數組,它是JVM管理的內存中最大的一塊區域,堆內存和方法區都被所有線程共享,在虛擬機啟動時創建。在垃圾收集的層面上來看,由于現在收集器基本上都采用分代收集算法,因此堆還可以分為新生代(YoungGeneration)和老年代(OldGeneration),新生代還可以分為Eden、From Survivor、To Survivor
堆是垃圾回收主要區域:
- 新生代 Eden、From Survivor、To Survivor 垃圾回收使用Minor GC
- 老年代垃圾回收使用Full GC
2.2.2.5 元空間
上面說到,jdk1.8中,已經不存在永久代(方法區),替代它的一塊空間叫做“元空間”,和永久代類似,都是JVM規范對方法區的實現,但是元空間并不在虛擬機中,而是使用本地內存,元空間的大小僅受本地內存限制,但可以通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize來指定元空間的大小
2.2.3 JVM內存溢出
2.2.3.1 堆內存溢出
堆內存中主要存放對象、數組等,只要不斷地創建這些對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾收集回收機制清除這些對象,當這些對象所占空間超過最大堆容量時,就會產生OutOfMemoryError的異常。
看到 java.lang.OutOfMemoryError: Java heap space 的信息,說明在堆內存空間產生內存溢出的異常。
新產生的對象最初分配在新生代,新生代滿后會進行一次Minor GC,如果Minor GC后空間不足會把該對象和新生代滿足條件的對象放入老年代,老年代空間不足時會進行Full GC,之后如果空間還不足以存放新對象則拋出OutOfMemoryError異常。常見原因:內存中加載的數據過多如一次從數據庫中取出過多數據;集合對對象引用過多且使用完后沒有清空;代碼中存在死循環或循環產生過多重復對象;堆內存分配不合理;網絡連接問題、數據庫問題等。
2.2.3.2 虛擬機棧/本地方法棧溢出
- StackOverflowError:當線程請求的棧的深度大于虛擬機所允許的最大深度,則拋出StackOverflowError,簡單理解就是虛擬機棧中的棧幀數量過多(一個線程嵌套調用的方法數量過多)時,就會拋出StackOverflowError異常。最常見的場景就是方法無限遞歸調用
- OutOfMemoryError:如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError。
在線程較少的時候,某個線程請求深度過大,會報StackOverflow異常,解決這種問題可以適當加大棧的深度(增加棧空間大小),也就是把-Xss的值設置大一些,但一般情況下是代碼問題的可能性較大;在虛擬機產生線程時,無法為該線程申請棧空間了,會報OutOfMemoryError異常,解決這種問題可以適當減小棧的深度,也就是把-Xss的值設置小一些,每個線程占用的空間小了,總空間一定就能容納更多的線程,但是操作系統對一個進程的線程數有限制,經驗值在3000~5000左右。在jdk1.5之前-Xss默認是256k,jdk1.5之后默認是1M,這個選項對系統硬性還是蠻大的,設置時要根據實際情況,謹慎操作。
2.2.3.3 方法區溢出
- 方法區主要用于存儲虛擬機加載的類信息、常量、靜態變量,以及編譯器編譯后的代碼等數據,所以方法區溢出的原因就是沒有足夠的內存來存放這些數據。
- 由于在jdk1.6之前字符串常量池是存在于方法區中的,所以基于jdk1.6之前的虛擬機,可以通過不斷產生不一致的字符串(同時要保證和GC Roots之間保證有可達路徑)來模擬方法區的OutOfMemoryError異常;但方法區還存儲加載的類信息,所以基于jdk1.7的虛擬機,可以通過動態不斷創建大量的類來模擬方法區溢出。
2.2.3.4 本機直接內存溢出
本機直接內存(DirectMemory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但Java中用到NIO相關操作時(比如ByteBuffer的allocteDirect方法申請的是本機直接內存),也可能會出現內存溢出的異常。
2.2.4 JVM垃圾回收
垃圾回收,就是通過垃圾收集器把內存中沒用的對象清理掉。
垃圾回收涉及到的內容有:
- 判斷對象是否已死;
- 選擇垃圾收集算法;
- 選擇垃圾收集的時間;
- 選擇適當的垃圾收集器清理垃圾(已死的對象)。
2.2.4.1 判斷對象是否為垃圾
- 引用計數算法
- 給每一個對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1;每當有一個地方不再引用它時,計數器值減1,這樣只要計數器的值不為0,就說明還有地方引用它,它就不是無用的對象。如下圖,對象2有1個引用,它的引用計數器值為1,對象1有兩個地方引用,它的引用計數器值為2 。
- 這種方法看起來非常簡單,但目前許多主流的虛擬機都沒有選用這種算法來管理內存,原因就是當某些對象之間互相引用時,無法判斷出這些對象是否已死,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數器的值仍然是1,無法判斷他們是死對象,垃圾回收器也就無法回收。
- 可達性分析算法
- 了解可達性分析算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為GC Roots的有虛擬機棧中本地變量表中引用的對象、方法區中靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。
- 當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。
- 被判了死刑的對象(object5、object6、object7)并不是必死無疑,還有挽救的余地。進行可達性分析后對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接著會判斷如果對象沒有覆蓋Object的finalize()方法或者finalize()方法已經被虛擬機調用過,那么它們就會被行刑(清除);如果對象覆蓋了finalize()方法且還沒有被調用,則會執行finalize()方法中的內容,所以在finalize()方法中如果重新與GC Roots引用鏈上的對象關聯就可以拯救自己,但是一般不建議這么做.
- 方法區回收
- 上面說的都是對堆內存中對象的判斷,方法區中主要回收的是廢棄的常量和無用的類
- 判斷常量是否廢棄可以判斷是否有地方引用這個常量,如果沒有引用則為廢棄的常量。
- 判斷類是否廢棄需要同時滿足如下條件:
- 該類所有的實例已經被回收(堆中不存在任何該類的實例)
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象在任何地方沒有被引用(無法通過反射訪問該類的方法)
2.2.4.2 常用垃圾回收算法
常用的垃圾回收算法有三種:標記-清除算法、復制算法、標記-整理算法。
- 標記-清除算法:
- 分為標記和清除兩個階段,首先標記出所有需要回收的對象,標記完成后統一回收所有被標記的對象
- 缺點:標記和清除兩個過程效率都不高;標記清除之后會產生大量不連續的內存碎片。
- 復制算法:
- 把內存分為大小相等的兩塊,每次存儲只用其中一塊,當這一塊用完了,就把存活的對象全部復制到另一塊上,同時把使用過的這塊內存空間全部清理掉,往復循環
- 缺點:實際可使用的內存空間縮小為原來的一半
- 標記-整理算法:
- 先對可用的對象進行標記,然后所有被標記的對象向一段移動,最后清除可用對象邊界以外的內存
- 分代收集算法:
- 把堆內存分為新生代和老年代,新生代又分為Eden區、From Survivor和To Survivor。一般新生代中的對象基本上都是朝生夕滅的,每次只有少量對象存活,因此采用復制算法,只需要復制那些少量存活的對象就可以完成垃圾收集;老年代中的對象存活率較高,就采用標記-清除和標記-整理算法來進行回收。
- 在這些區域的垃圾回收大概有如下幾種情況:
- 新生代使用時minor gc
- 老年代使用的full gc
- 大多數情況下,新的對象都分配在Eden區,當Eden區沒有空間進行分配時,將進行一次Minor GC,清理Eden區中的無用對象。清理后,Eden和From Survivor中的存活對象如果小于To Survivor的可用空間則進入To Survivor,否則直接進入老年代);Eden和From Survivor中還存活且能夠進入To Survivor的對象年齡增加1歲(虛擬機為每個對象定義了一個年齡計數器,每執行一次Minor GC年齡加1),當存活對象的年齡到達一定程度(默認15歲)后進入老年代,可以通過-XX:MaxTenuringThreshold來設置年齡的值。
- 當進行了Minor GC后,Eden還不足以為新對象分配空間(那這個新對象肯定很大),新對象直接進入老年代。
- 占To Survivor空間一半以上且年齡相等的對象,大于等于該年齡的對象直接進入老年代,比如Survivor空間是10M,有幾個年齡為4的對象占用總空間已經超過5M,則年齡大于等于4的對象都直接進入老年代,不需要等到MaxTenuringThreshold指定的歲數。
- 在進行Minor GC之前,會判斷老年代最大連續可用空間是否大于新生代所有對象總空間,如果大于,說明Minor GC是安全的,否則會判斷是否允許擔保失敗,如果允許,判斷老年代最大連續可用空間是否大于歷次晉升到老年代的對象的平均大小,如果大于,則執行Minor GC,否則執行Full GC。
- 當在java代碼里直接調用System.gc()時,會建議JVM進行Full GC,但一般情況下都會觸發Full GC,一般不建議使用,盡量讓虛擬機自己管理GC的策略。
- 永久代(方法區)中用于存放類信息,jdk1.6及之前的版本永久代中還存儲常量、靜態變量等,當永久代的空間不足時,也會觸發Full GC,如果經過Full GC還無法滿足永久代存放新數據的需求,就會拋出永久代的內存溢出異常。
- 大對象(需要大量連續內存的對象)例如很長的數組,會直接進入老年代,如果老年代沒有足夠的連續大空間來存放,則會進行Full GC。
Minor GC和Full GC
- 在說這兩種回收的區別之前,我們先來說一個概念,“Stop-The-World”。
- 如字面意思,每次垃圾回收的時候,都會將整個JVM暫停,回收完成后再繼續。如果一邊增加廢棄對象,一邊進行垃圾回收,完成工作似乎就變得遙遙無期了。
- 而一般來說,我們把新生代的回收稱為Minor GC,Minor意思是次要的,新生代的回收一般回收很快,采用復制算法,造成的暫停時間很短。而Full GC一般是老年代的回收,并伴隨至少一次的Minor GC,新生代和老年代都回收,而老年代采用標記-整理算法,這種GC每次都比較慢,造成的暫停時間比較長,通常是Minor GC時間的10倍以上。
- 所以很明顯,我們需要盡量通過Minor GC來回收內存,而盡量少的觸發Full GC。畢竟系統運行一會兒就要因為GC卡住一段時間,再加上其他的同步阻塞,整個系統給人的感覺就是又卡又慢。
2.2.4.3 選擇垃圾收集的時間
- 當程序運行時,各種數據、對象、線程、內存等都時刻在發生變化,當下達垃圾收集命令后就立刻進行收集嗎?肯定不是。這里來了解兩個概念:安全點(safepoint)和安全區(safe region)。
- 安全點:從線程角度看,安全點可以理解為是在代碼執行過程中的一些特殊位置,當線程執行到安全點的時候,說明虛擬機當前的狀態是安全的,如果有需要,可以在這里暫停用戶線程。當垃圾收集時,如果需要暫停當前的用戶線程,但用戶線程當時沒在安全點上,則應該等待這些線程執行到安全點再暫停。舉個例子,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽等一下,讓我吃完這塊再掃。”兒子吃完這塊西瓜把瓜皮扔到地上后就是一個安全點,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。理論上,解釋器的每條字節碼的邊界上都可以放一個安全點,實際上,安全點基本上以“是否具有讓程序長時間執行的特征”為標準進行選定。
- 安全區:安全點是相對于運行中的線程來說的,對于如sleep或blocked等狀態的線程,收集器不會等待這些線程被分配CPU時間,這時候只要線程處于安全區中,就可以算是安全的。安全區就是在一段代碼片段中,引用關系不會發生變化,可以看作是被擴展、拉長了的安全點。還以上面的例子說明,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽你繼續掃地吧,我還得吃10分鐘呢!”兒子吃瓜的這段時間就是安全區,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。 一段連續的安全點
2.2.4.4 常見垃圾收集器
現在常見的垃圾收集器有如下幾種:
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、CMS、Parallel Old
堆內存垃圾收集器:G1
- Serial 收集器
- ParNew 收集器
- Parallel Scavenge 收集器
- Serial Old收集器
- CMS(Concurrent Mark Sweep) 收集器
- Parallel Old 收集器
- G1 收集器
適用場景:要求盡可能可控GC停頓時間;內存占用較大的應用。可以用-XX:+UseG1GC使用G1收集器,jdk9默認使用G1收集器。
2.3 JVM的優化
JVM調優目標:使用較小的內存占用來獲得較高的吞吐量或者較低的延遲。
程序在上線前的測試或運行中有時會出現一些大大小小的JVM問題,比如cpu load過高、請求延遲、tps降低等,甚至出現內存泄漏(每次垃圾收集使用的時間越來越長,垃圾收集頻率越來越高,每次垃圾收集清理掉的垃圾數據越來越少)、內存溢出導致系統崩潰,因此需要對JVM進行調優,使得程序在正常運行的前提下,獲得更高的用戶體驗和運行效率。
- 內存占用:程序正常運行需要的內存大小。
- 延遲:由于垃圾收集而引起的程序停頓時間。
- 吞吐量:用戶程序運行時間占用戶程序和垃圾收集占用總時間的比值。
當然,和CAP原則一樣,同時滿足一個程序內存占用小、延遲低、高吞吐量是不可能的,程序的目標不同,調優時所考慮的方向也不同,在調優之前,必須要結合實際場景,有明確的的優化目標,找到性能瓶頸,對瓶頸有針對性的優化,最后進行測試,通過各種監控工具確認調優后的結果是否符合目標。
調優可以依賴、參考的數據有系統運行日志、堆棧錯誤信息、gc日志、線程快照、堆轉儲快照等。