該視頻主要講解的內容如下所示:
1、虛擬機的內存結構
1、每一個線程都有一個虛擬機棧,線程中每調用一個方法都會開啟一個棧幀,棧幀里面保存方法中的局部變量。
2、方法區在java8以后改名為永久區域perment,存在的class 文件 字符串常量等信息,存儲類相關的信息
2、堆 heap
對象分配的方式:new 一個對象,如果該對象很大,就直接分配到老年區,如果不是很大就分配帶新生代的eden區域,第一次GC的時候,會把eden區域沒有被回收的對象(有引用)拷貝到s0區域,第二次內存回收的時候會把eden區域沒有被回收的和s0區域中的對象拷貝
到s1區域,並且情況s0區域。
再次內存回收的時候,會把eden區域沒有被內存回收的對象和s1區域的對象拷貝到s0區域,然后情況s1區域,一直這樣s0區域和s1區域交替使用
如果一個對象在GC的過程中,經過很多次都沒有被GC,最終會被移動到老年區,這個次數可以通過參數來進行配置
eden里面的對象大部分都會被GC回收,例如100個對象,GC回收異常98個對象都會被回收。
老年代tenured中的對象都是經過很多次GC沒有被回收的對象,通常配置eden:s0:s1區域的內存比例是8:1:1,new新生代區域:old區域的內存比例是1:3
3、垃圾回收器
垃圾:
1. 什么樣的對象是垃圾?一般來說,所有指向對象的引用都已失效,不可能再有程序能調用到這個對象,那么這個對象就成了垃圾,應該被回收。
1.1 根據這個思路,很容易就能想到用《引用計數》的辦法來確定一個對象是否是垃圾。即每當多一個引用指向對象時,引用計數加一,每當少一個引用指向對象時,引用計數減一,引用計數減到零,對象就可以被回收了。
1.2 然而引用計數有一個致命問題不好解決,就是循環引用的問題。比如說一個循環鏈表,他們循環引用者,引用計數永遠不會為零,但是實際上程序已經不能訪問他們了,他們應該被回收。
1.3 所以Java實際上是使用基於GC Roots的可達性分析,什么是GC Roots?所有類的靜態變量,每個線程調用棧上的本地變量。(實際上我們編程時也是要從這些地方開始訪問數據),所有這些對象,以及被這些對象所指向的對象,都是活的對象。活的對象所指向的對象也是活的對象。
1.4 所以只要在GC的時刻,讓程序暫停運行,然后從GC Roots開始分析,最后沒有被標記為活對象的對象就是垃圾了。
1.引用計數算法(已被淘汰的算法)
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
目前主流的java虛擬機都摒棄掉了這種算法,最主要的原因是它很難解決對象
之間相互循環引用的問題。盡管該算法執行效率很高。
public class Main { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } }
最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不為0,那么垃圾收集器就永遠不會回收它們。
2.可達性分析算法
目前主流的編程語言(java,C#等)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。
強可及對象永遠不會被gcc垃圾回收器回收
軟可及對象當系統內存不足的時候,gcc會把軟可及對象回收掉
弱可及對象當gcc發現這個對象是弱可及對象就馬上將對象回收
1、標記清除算法 黑色的部分就是可以清除的。但是存在內存碎片化問題,未使用的內存存在不連續的情況
2、第二種算法 復制壓縮算法
我們把上圖中前兩行標記為A區域,后面兩行標記為B語言,把內存分成兩個部分,一個4G的內存,有2G不能使用浪費內存
第一次GC的時候,A區域中經過垃圾可達性算法得到垃圾對象和存活對象,在GC的時候,將灰色的存活對象拷貝到B區域,在B區域中也就是第三行,存活對象在內存第三行中是連續的,然后把A區域的垃圾對象全部回收,A區域全部變成未使用的
在第二次回收的時候,B區域中的對象經過垃圾可達性算法得到垃圾對象和存活對象,在GC的時候,將灰色的存活對象拷貝到A區域,例如在A區域中也就是第一行,存活對象在第一行內存中是連續的,然后把B區域的垃圾對象全部回收,B區域全部變成未使用的
這樣A和B區域交互使用
在新生代中就是采用復制壓縮的算法
該算法缺點是比較浪費內存,通常配置eden:s0:s1區域的內存比例是8:1:1,這樣就可以節約內存也可以保證效率
在老年代中使用,因為老年代中可回收的垃圾比較少,所以也能保證效率,但是效率比復制壓縮算法較低,但是節約內存資源。
年輕代(Young Generation)
1.所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
2.新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象復制到一個survivor0區,然后清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然后清空eden和這個survivor0區,此時survivor0區是空的,然后將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)
年老代(Old Generation)
1.在年輕代中經歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。
2.內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
持久代(Permanent Generation)
用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。在java 1.7之前也叫方法區
參看博客:https://www.cnblogs.com/andy-zcx/p/5522836.html
Java有四種類型的垃圾回收器:
串行垃圾回收器(Serial Garbage Collector)
並行垃圾回收器(Parallel Garbage Collector)
並發標記掃描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 Garbage Collector)
ImportNew
首頁所有文章資訊Web架構基礎技術書籍教程Java小組工具資源
Java GC系列(3):垃圾回收器種類
2014/11/19 | 分類: 基礎技術, 教程 | 0 條評論 | 標簽: GC, 垃圾回收教程
分享到: 24
本文由 ImportNew - 好好先生 翻譯自 javapapers。歡迎加入翻譯小組。轉載請見文末要求。
目錄
垃圾回收介紹
垃圾回收是如何工作的?
垃圾回收的類別
垃圾回收監視和分析
在這篇教程中我們將學習幾種現有的垃圾回收器。在Java中,垃圾回收是一個自動的進程可以替代程序員進行內存的分配與回收這些復雜的工作。這篇是垃圾回 收教程系列的第三篇,在前面的第2部分我們看到了在Java中垃圾回收是如何工作的,那是篇有意思的文章,我推薦你去看一下。第一部分介紹了Java的垃 圾回收,主要有JVM體系結構,堆內存模型和一些Java術語。
Java有四種類型的垃圾回收器:
串行垃圾回收器(Serial Garbage Collector)
並行垃圾回收器(Parallel Garbage Collector)
並發標記掃描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 Garbage Collector)
每種類型都有自己的優勢與劣勢。重要的是,我們編程的時候可以通過JVM選擇垃圾回收器類型。我們通過向JVM傳遞參數進行選擇。每種類型在很大程度上有 所不同並且可以為我們提供完全不同的應用程序性能。理解每種類型的垃圾回收器並且根據應用程序選擇進行正確的選擇是非常重要的。
1、串行垃圾回收器
串行垃圾回收器通過持有應用程序所有的線程進行工作。它為單線程環境設計,只使用一個單獨的線程進行垃圾回收,通過凍結所有應用程序線程進行工作,所以可能不適合服務器環境。它最適合的是簡單的命令行程序。
通過JVM參數-XX:+UseSerialGC可以使用串行垃圾回收器。
2、並行垃圾回收器
並行垃圾回收器也叫做 throughput collector 。它是JVM的默認垃圾回收器。與串行垃圾回收器不同,它使用多線程進行垃圾回收。相似的是,它也會凍結所有的應用程序線程當執行垃圾回收的時候
3、並發標記掃描垃圾回收器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。
並發標記垃圾回收使用多線程掃描堆內存,標記需要清理的實例並且清理被標記過的實例。並發標記垃圾回收器只會在下面兩種情況持有應用程序所有線程。
當標記的引用對象在tenured區域;
在進行垃圾回收的時候,堆內存的數據被並發的改變。
相比並行垃圾回收器,並發標記掃描垃圾回收器使用更多的CPU來確保程序的吞吐量。如果我們可以為了更好的程序性能分配更多的CPU,那么並發標記上掃描垃圾回收器是更好的選擇相比並發垃圾回收器。
通過JVM參數 XX:+USeParNewGC 打開並發標記掃描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器適用於堆內存很大的情況,他將堆內存分割成不同的區域,並且並發的對其進行垃圾回收。G1也可以在回收內存之后對剩余的堆內存空間進行壓縮。並發掃描標記垃圾回收器在STW情況下壓縮內存。G1垃圾回收會優先選擇第一塊垃圾最多的區域
通過JVM參數 –XX:+UseG1GC 使用G1垃圾回收器
不清楚的看博客:
https://www.cnblogs.com/ityouknow/p/5614961.html
http://www.importnew.com/13827.html
java中-表示標准參數 -xx表示jdk 1.5存在,可能jdk 1.6就不存在了。
java對象的分配:
生成一個對象的時候,如何開啟了jvm優化開啟了棧上分配的優化,如果是小對象就會在棧上分配,棧上的對象就不用進行垃圾回收,當棧幀中方法結束之后會自動回收。
TLAB本質上如下所示:在java多線程中對應ThreadLocal這個類
逃逸行為:在方法中分配的對象,在方法銷毀的時候對象沒有在方法棧中被銷毀,而被其他全局變量引用,導致對象不能被回收。jvm進行優化的時候在棧上分配的對象是不能存在逃逸行為的,在棧上分配對象不能存在逃逸行為,
-XX:+DoEscapeAnalysis -XX:+PrintGC 是開啟逃逸分析,如果寫成-XX:-DoEscapeAnalysis 禁止逃逸分析,對象就不能分配在棧上。默認情況下是開啟逃逸分析的
package com.weiyuan.test; import java.lang.management.ManagementFactory; public class Test01 { /** * 逃逸分析優化-棧上分配 * 棧上分配,意思是方法內局部變量(未發生逃逸)生成的實例在棧上分配,不用在堆中分配,分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。 * 一般生成的實例都是放在堆中的,然后把實例的指針或引用壓入棧中。 * 虛擬機參數設置如下,表示做了逃逸分析 消耗時間在10毫秒以下 * -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC * * 虛擬機參數設置如下,表示沒有做逃逸分析 消耗時間在1000毫秒以上 * -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC * @author 734621 * */ public static class User{ private int i ; public User(){ } } public static void alloc(){ User usr = new User(); } public static void main(String [] args){ long b = System.currentTimeMillis(); for(int i=0;i<1000000000;i++){ alloc(); } long e = System.currentTimeMillis(); System.out.println("消耗時間為:" + (e - b)); } }
開啟逃逸分析,允許在棧上分配:運行的時間是消耗時間為:2
關閉在棧上分配,對象不能分配在棧上分配,也不允許在TLAB上分配,只允許在堆上分配 -server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:-UseTLAB -XX:+PrintGC
命令: -XX:-DoEscapeAnalysis 禁止在棧上分配,-XX:-UseTLAB禁止在TLAB上分配,所以只能在堆上分配
[GC 3080K->520K(10240K), 0.0002258 secs]
[GC 3080K->520K(10240K), 0.0002543 secs]
[GC 3080K->520K(10240K), 0.0002729 secs]
[GC 3080K->520K(10240K), 0.0002425 secs]
[GC 3080K->520K(10240K), 0.0003005 secs]
[GC 3080K->520K(10240K), 0.0002861 secs]
[GC 3080K->520K(10240K), 0.0003239 secs]
[GC 3080K->520K(10240K), 0.0002813 secs]
[GC 3080K->520K(10240K), 0.0002765 secs]
[GC 3080K->520K(10240K), 0.0003053 secs]
[GC 3080K->520K(10240K), 0.0002210 secs]
[GC 3080K->520K(10240K), 0.0002335 secs]
[GC 3080K->520K(10240K), 0.0002101 secs]
[GC 3080K->520K(10240K), 0.0002367 secs]
[GC 3080K->520K(10240K), 0.0002810 secs]
[GC 3080K->520K(10240K), 0.0002447 secs]
[GC 3080K->520K(10240K), 0.0003416 secs]
[GC 3080K->520K(10240K), 0.0003339 secs]
[GC 3080K->520K(10240K), 0.0002489 secs]
[GC 3080K->520K(10240K), 0.0002415 secs]
[GC 3080K->520K(10240K), 0.0002794 secs]
[GC 3080K->520K(10240K), 0.0002149 secs]
[GC 3080K->520K(10240K), 0.0002691 secs]
[GC 3080K->520K(10240K), 0.0002916 secs]
[GC 3080K->520K(10240K), 0.0002502 secs]
[GC 3080K->520K(10240K), 0.0002839 secs]
[GC 3080K->520K(10240K), 0.0066899 secs]
[GC 3080K->520K(10240K), 0.0003018 secs]
[GC 3080K->520K(10240K), 0.0003053 secs]
[GC 3080K->520K(10240K), 0.0002319 secs]
[GC 3080K->520K(10240K), 0.0003124 secs]
消耗時間為:13922
運行的時間是棧上運行的幾千倍
逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優化技術。
逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他地方中,稱為方法逃逸。
public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; }
StringBuffer sb是一個方法內部變量,上述代碼中直接將sb返回,這樣這個StringBuffer有可能被其他方法所改變,這樣它的作用域就不只是在方法內部,雖然它是一個局部變量,稱其逃逸到了方法外部。
甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。
上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:
public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
不直接返回 StringBuffer,那么StringBuffer將不會逃逸出方法。
如果能證明一個對象不會逃逸到方法或線程外,則可能為這個變量進行一些高效的優化。
1. 棧上分配
我們都知道Java中的對象都是在堆上分配的,而垃圾回收機制會回收堆中不再使用的對象,但是篩選可回收對象,回收對象還有整理內存都需要消耗時間。如果能夠通過逃逸分析確定某些對象不會逃出方法之外,那就可以讓這個對象在棧上分配內存,這樣該對象所占用的內存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
在一般應用中,如果不會逃逸的局部對象所占的比例很大,如果能使用棧上分配,那大量的對象就會隨着方法的結束而自動銷毀了。
Java程序中,每個線程都有自己的Stack Space(堆棧)。這個Stack Space不是來自Heap的分配。所以Stack Space的大小不會受到-Xmx和-Xms的影響,這2個JVM參數僅僅是影響Heap的大小。
Stack Space用來做方法的遞歸調用時壓入Stack Frame(棧幀)。所以當遞歸調用太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。
-Xss128k:設置每個線程的堆棧大小。JDK5.0以后每個線程堆 棧大小為1M,以前每個線程堆棧大小為256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一 個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
package com.weiyuan.test; import java.lang.management.ManagementFactory; public class Test02 { static int count = 0; static void alloc(){ //無限制遞歸,會導致棧空間溢出 System.out.println("alloc is called"+count); count = count +1; alloc(); } public static void main(String [] args){ try{ System.out.println("count ="+count); alloc(); }catch(Exception e){ System.out.println(""+count); } } }
先將棧大小設置成512K
日志打印如下:
alloc is called5996
alloc is called5997
Exception in thread "main" java.lang.StackOverflowError
最大可以遞歸5997次
如果改成1024K
alloc is called12548
alloc is called12549
Exception in thread "main" java.lang.StackOverflowError
我們可以設置棧的大小達到我們的需求