摘要:虛擬機棧線程私有,生命周期跟線程相同。堆用于存放對象實例,是虛擬機所管理的內存中最大的一塊,同時也是所有線程共享的一塊內存區域。統計監測工具語法格式如下是虛擬機,在系統上一般就是進程。
JDK、JRE、JVM三者的關系
JDK(Java Development Kit)是針對Java開發的產品、是整個Java的核心,包括Java運行環境JRE、Java工具包和Java基礎類庫。
JRE(Java Runtime Environment)是運行Java程序所必須的環境的集合,包含JVM標準實現及Java核心類庫。
JVM(Java Virtual Machine)是整個Java跨平臺的最核心的部分,能夠運行以Java語言寫作的軟件程序。所有的Java程序都會首先被編譯為.class文件,這種類文件可以在虛擬機上運行,class文件并不直接與機器的操作系統相對應,而是經過虛擬機間接與操作系統交互,由虛擬機將程序解釋給本地系統執行。
Java運行時區域 程序計數器內存中較小的內存空間,通過計數器的值可以選取下一條執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
線程私有,生命周期跟線程相同。
如果正在執行一個Native方法,那么這個計數器值將為空。
虛擬機棧線程私有,生命周期跟線程相同。
每個方法在執行同時都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
在Java虛擬機規范中,對這個區域規定了兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;
如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
跟虛擬機棧所發揮的作用相似,區別在于虛擬機棧為虛擬機執行Java(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
Java堆用于存放對象實例,是Java虛擬機所管理的內存中最大的一塊,同時也是所有線程共享的一塊內存區域。
因為Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC"堆。由于現在收集器基本都采用分代收集算法,所以Java堆還可以細分為
新生代
老年代
永久代(永久代是Hotspot虛擬機特有的概念,是方法區的一種實現,別的JVM都沒有這個東西。在Java 8中,永久代被徹底移除,取而代之的是另一塊與堆不相連的本地內存——元空間。)
當一個對象被創建時,它首先進入新生代,之后有可能被轉移到老年代中。
新生代存放著大量的生命很短的對象,因此新生代在三個區域中垃圾回收的頻率最高。為了更高效地進行垃圾回收,把新生代繼續劃分成以下三個空間:
Eden
From Survivor
To Survivor
方法區與Java堆一樣,各個線程共享的內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯后的代碼等數據。
運行時常量池方法區的一部分,用于存放編譯器生成的各種字面量和符號引用。
運行時常量池相對于class文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。
直接內存在JDK1.4中新加入了NIO類,引入了一種基于通道與緩沖區的I/O方法,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。
堆外內存之 DirectByteBuffer 詳解
在語言層上,創建對象通常僅僅是一個new關鍵字而已,而當虛擬機遇到一條new執行時,將由一下步驟:
檢查類是否加載、解析、初始化過,沒有則先執行相應的類加載過程。
在堆中分配內存
劃分可用空間:
指針碰撞:堆內存規整
空閑列表:堆內存不規整
并發問題
同步:采用CAS配上失敗重試的方式保證更新操作的原子性
把內存分配動作按照線程劃分在不同的空間之中進行
將分配到的內存空間都初始化零值
設置對象的類實例、元數據、哈希碼、GC分代年齡等信息。
執行
對象在內存中儲存的布局可以分為3塊區域:
對象頭
對象運行時數據、哈希碼、GC分代年齡、鎖狀態標記、線程持有的鎖、偏向線程ID等
類型執行:即對象執向它的類元數據的指針,指明對象數據哪個類的實例。
實例數據
對象真正存儲的有效信息
對齊填充
占位符作用
對象的訪問定位句柄定位
直接指針
內存溢出內存溢出out of memory,是指程序在申請空間時,沒有足夠的內存空間供其使用,出現了Out of memory error。
堆內存溢出當new一個對象或者數組時,如果超出了Jvm的head內存最大限制就會爆出異常。
偽代碼:
while(ture){ new Object(); }棧內存溢出
在Java虛擬機規范中,對這個棧規定了兩種異常情況,如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOutFlowError異常,如果虛擬機可以動態擴展(當前大部分Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),當擴展時無法申請得到足夠的內存時將會拋出OutOfMemory。
StackOutFlowError線程中的stack是線程私有的,默認大小通常為1M,可以通過-Xss來設置,-Xss越大,則線程獲取的內存越大。
常見問題在線程內過度的調用函數,函數調用會消耗棧空間。
偽代碼:
public void SOFETest(){ SOFETest(); }OutOfMemoryError
Java的棧空間被所有線程分配成一塊一塊的,每個線程只占一塊。而Jvm的棧空間的最小分配單位有-Xss來決定。-Xss有兩個語義,即定義每個線程的棧大小,也定義了虛擬機的最小棧內存的分配單位。
如果申請的線程沒有獲得棧空間可以分配了就會拋出OutOfMemoryError。表示棧空間不足,溢出異常。
代碼:該代碼可能導致JVM無法申請得到太多的棧內存而導致操作系統因為棧空間不足假死。
public class Main { public static void main(String[] args) throws ClassNotFoundException { CountDownLatch countDownLatch = new CountDownLatch(1); for(int i =0;i<1020000000;i++){ new Thread(new Runnable(){ @Override public void run() { int a = 1000; try { countDownLatch.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }).start(); } countDownLatch.countDown(); } }內存泄漏
內存泄漏memory leak,指程序在申請內存之后,無法釋放已申請的內存空間,一次內存泄漏危害可以忽略,多次memory leak將導致oom。
內存泄漏是指你向系統申請分配內存進行使用(new),可是使用完了以后卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程序。
jvm性能調優監控工具使用詳解該部分內容轉自:JVM性能調優監控工具jps、jstack、jmap、jhat、jstat、hprof使用詳解
jps(Java Virture Machine Process Status Tool)jps主要用來輸出JVM中運行的進程狀態信息。語法格式如下:
jps [options] [hostid]
如果不指定hostid就默認為當前主機或服務器。
命令行參數選項說明如下:
-q 不輸出類名、Jar名和傳入main方法的參數 -m 輸出傳入main方法的參數 -l 輸出main類或Jar的全限名 -v 輸出傳入JVM的參數
比如下面:
root@ubuntu:/# jps -m -l 2458 org.artifactory.standalone.main.Main /usr/local/artifactory-2.2.5/etc/jetty.xml 29920 com.sun.tools.hat.Main -port 9998 /tmp/dump.dat 3149 org.apache.catalina.startup.Bootstrap start 30972 sun.tools.jps.Jps -m -l 8247 org.apache.catalina.startup.Bootstrap start 25687 com.sun.tools.hat.Main -port 9999 dump.dat 21711 mrf-center.jarjstack
jstack主要用來查看某個Java進程內的線程堆棧信息。語法格式如下:
jstack [option] pid jstack [option] executable core jstack [option] [server-id@]remote-hostname-or-ip
命令行參數選項說明如下:
-l long listings,會打印出額外的鎖信息,在發生死鎖時可以用jstack -l pid來觀察鎖持有情況 -m mixed mode,不僅會輸出Java堆棧信息,還會輸出C/C++堆棧信息(比如Native方法)
jstack可以定位到線程堆棧,根據堆棧信息我們可以定位到具體代碼,所以它在JVM性能調優中使用得非常多。下面我們來一個實例找出某個Java進程中最耗費CPU的Java線程并定位堆棧信息,用到的命令有ps、top、printf、jstack、grep。
第一步先找出Java進程ID,我部署在服務器上的Java應用名稱為mrf-center:
root@ubuntu:/# ps -ef | grep mrf-center | grep -v grep root 21711 1 1 14:47 pts/3 00:02:10 java -jar mrf-center.jar
得到進程ID為21711,第二步找出該進程內最耗費CPU的線程,可以使用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid,我這里用第三個,輸出如下:
TIME列就是各個Java線程耗費的CPU時間,CPU時間最長的是線程ID為21742的線程,用
printf "%x " 21742
得到21742的十六進制值為54ee,下面會用到。
OK,下一步終于輪到jstack上場了,它用來輸出進程21711的堆棧信息,然后根據線程ID的十六進制值grep,如下:
root@ubuntu:/# jstack 21711 | grep 54ee "PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]
可以看到CPU消耗在PollIntervalRetrySchedulerThread這個類的Object.wait(),我找了下我的代碼,定位到下面的代碼:
// Idle wait getLog().info("Thread [" + getName() + "] is idle waiting..."); schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting; long now = System.currentTimeMillis(); long waitTime = now + getIdleWaitTime(); long timeUntilContinue = waitTime - now; synchronized(sigLock) { try { if(!halted.get()) { sigLock.wait(timeUntilContinue); } } catch (InterruptedException ignore) { } }
它是輪詢任務的空閑等待代碼,上面的sigLock.wait(timeUntilContinue)就對應了前面的Object.wait()。
jmap(Memory Map)和jhat(Java Heap Analysis Tool)jmap用來查看堆內存使用狀況,一般結合jhat使用。
jmap語法格式如下:
jmap [option] pid jmap [option] executable core jmap [option] [server-id@]remote-hostname-or-ip
如果運行在64位JVM上,可能需要指定-J-d64命令選項參數。
jmap -permstat pid
打印進程的類加載器和類加載器加載的持久代對象信息,輸出:類加載器名稱、對象是否存活(不可靠)、對象地址、父類加載器、已加載的類大小等信息,如下圖:
使用jmap -heap pid查看進程堆內存使用情況,包括使用的GC算法、堆配置參數和各代中堆內存使用情況。比如下面的例子:
root@ubuntu:/# jmap -heap 21711 Attaching to process ID 21711, please wait... Debugger attached successfully. Server compiler detected. JVM version is 20.10-b01 using thread-local object allocation. Parallel GC with 4 thread(s) Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 2067791872 (1972.0MB) NewSize = 1310720 (1.25MB) MaxNewSize = 17592186044415 MB OldSize = 5439488 (5.1875MB) NewRatio = 2 SurvivorRatio = 8 PermSize = 21757952 (20.75MB) MaxPermSize = 85983232 (82.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 6422528 (6.125MB) used = 5445552 (5.1932830810546875MB) free = 976976 (0.9317169189453125MB) 84.78829520089286% used From Space: capacity = 131072 (0.125MB) used = 98304 (0.09375MB) free = 32768 (0.03125MB) 75.0% used To Space: capacity = 131072 (0.125MB) used = 0 (0.0MB) free = 131072 (0.125MB) 0.0% used PS Old Generation capacity = 35258368 (33.625MB) used = 4119544 (3.9287033081054688MB) free = 31138824 (29.69629669189453MB) 11.683876009235595% used PS Perm Generation capacity = 52428800 (50.0MB) used = 26075168 (24.867218017578125MB) free = 26353632 (25.132781982421875MB) 49.73443603515625% used ....
使用jmap -histo[:live] pid查看堆內存中的對象數目、大小統計直方圖,如果帶上live則只統計活對象,如下:
root@ubuntu:/# jmap -histo:live 21711 | more num #instances #bytes class name ---------------------------------------------- 1: 38445 55977362: 38445 5237288 3: 3500 3749504 4: 60858 3242600 5: 3500 2715264 6: 2796 2131424 7: 5543 1317400 [I 8: 13714 1010768 [C 9: 4752 1003344 [B 10: 1225 639656 11: 14194 454208 java.lang.String 12: 3809 396136 java.lang.Class 13: 4979 311952 [S 14: 5598 287064 [[I 15: 3028 266464 java.lang.reflect.Method 16: 280 163520 17: 4355 139360 java.util.HashMap$Entry 18: 1869 138568 [Ljava.util.HashMap$Entry; 19: 2443 97720 java.util.LinkedHashMap$Entry 20: 2072 82880 java.lang.ref.SoftReference 21: 1807 71528 [Ljava.lang.Object; 22: 2206 70592 java.lang.ref.WeakReference 23: 934 52304 java.util.LinkedHashMap 24: 871 48776 java.beans.MethodDescriptor 25: 1442 46144 java.util.concurrent.ConcurrentHashMap$HashEntry 26: 804 38592 java.util.HashMap 27: 948 37920 java.util.concurrent.ConcurrentHashMap$Segment 28: 1621 35696 [Ljava.lang.Class; 29: 1313 34880 [Ljava.lang.String; 30: 1396 33504 java.util.LinkedList$Entry 31: 462 33264 java.lang.reflect.Field 32: 1024 32768 java.util.Hashtable$Entry 33: 948 31440 [Ljava.util.concurrent.ConcurrentHashMap$HashEntry;
class name是對象類型,說明如下:
B byte C char D double F float I int J long Z boolean [ 數組,如[I表示int[] [L+類名 其他對象
還有一個很常用的情況是:用jmap把進程內存使用情況dump到文件中,再用jhat分析查看。jmap進行dump命令格式如下:
jmap -dump:format=b,file=dumpFileName pid
我一樣地對上面進程ID為21711進行Dump:
root@ubuntu:/# jmap -dump:format=b,file=/tmp/dump.dat 21711 Dumping heap to /tmp/dump.dat ... Heap dump file created
dump出來的文件可以用MAT、VisualVM等工具查看,這里用jhat查看:
root@ubuntu:/# jhat -port 9998 /tmp/dump.dat Reading from /tmp/dump.dat... Dump file created Tue Jan 28 17:46:14 CST 2014 Snapshot read, resolving... Resolving 132207 objects... Chasing references, expect 26 dots.......................... Eliminating duplicate references.......................... Snapshot resolved. Started HTTP server on port 9998 Server is ready.
注意如果Dump文件太大,可能需要加上-J-Xmx512m這種參數指定最大堆內存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat。然后就可以在瀏覽器中輸入主機地址:9998查看了:
上面紅線框出來的部分大家可以自己去摸索下,最后一項支持OQL(對象查詢語言)。
jstat(JVM統計監測工具)語法格式如下:
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]
vmid是Java虛擬機ID,在Linux/Unix系統上一般就是進程ID。interval是采樣時間間隔。count是采樣數目。比如下面輸出的是GC信息,采樣時間間隔為250ms,采樣數為4:
root@ubuntu:/# jstat -gc 21711 250 4 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 192.0 192.0 64.0 0.0 6144.0 1854.9 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 2109.7 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649
要明白上面各列的意義,先看JVM堆內存布局:
可以看出:
堆內存 = 年輕代 + 年老代 + 永久代 年輕代 = Eden區 + 兩個Survivor區(From和To)
現在來解釋各列含義:
S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used) EC、EU:Eden區容量和使用量 OC、OU:年老代容量和使用量 PC、PU:永久代容量和使用量 YGC、YGT:年輕代GC次數和GC耗時 FGC、FGCT:Full GC次數和Full GC耗時 GCT:GC總耗時hprof(Heap/CPU Profiling Tool)
hprof能夠展現CPU使用率,統計堆內存使用情況。
語法格式如下:
java -agentlib:hprof[=options] ToBeProfiledClass java -Xrunprof[:options] ToBeProfiledClass javac -J-agentlib:hprof[=options] ToBeProfiledClass
完整的命令選項如下:
Option Name and Value Description Default --------------------- ----------- ------- heap=dump|sites|all heap profiling all cpu=samples|times|old CPU usage off monitor=y|n monitor contention n format=a|b text(txt) or binary output a file=write data to file java.hprof[.txt] net= : send data over a socket off depth= stack trace depth 4 interval= sample interval in ms 10 cutoff= output cutoff point 0.0001 lineno=y|n line number in traces? y thread=y|n thread in traces? n doe=y|n dump on exit? y msa=y|n Solaris micro state accounting n force=y|n force output to y verbose=y|n print messages about dumps y
來幾個官方指南上的實例。
CPU Usage Sampling Profiling(cpu=samples)的例子:
java -agentlib:hprof=cpu=samples,interval=20,depth=3 Hello
上面每隔20毫秒采樣CPU消耗信息,堆棧深度為3,生成的profile文件名稱是java.hprof.txt,在當前目錄。
CPU Usage Times Profiling(cpu=times)的例子,它相對于CPU Usage Sampling Profile能夠獲得更加細粒度的CPU消耗信息,能夠細到每個方法調用的開始和結束,它的實現使用了字節碼注入技術(BCI):
javac -J-agentlib:hprof=cpu=times Hello.java
Heap Allocation Profiling(heap=sites)的例子:
javac -J-agentlib:hprof=heap=sites Hello.java
Heap Dump(heap=dump)的例子,它比上面的Heap Allocation Profiling能生成更詳細的Heap Dump信息:
javac -J-agentlib:hprof=heap=dump Hello.java
雖然在JVM啟動參數中加入-Xrunprof:heap=sites參數可以生成CPU/Heap Profile文件,但對JVM性能影響非常大,不建議在線上服務器環境使用。
垃圾收集器程序計數器、虛擬機棧和本地方法棧這三個區域屬于線程私有的,只存在于線程的生命周期內,線程結束之后也會消失,因此不需要對這三個區域進行垃圾回收。垃圾回收主要是針對 Java 堆和方法區進行。
判斷對象是否死亡 引用計數算法給對象添加一個引用計數器,每當有一個地方引用它,計數器值就加1;引用時效時,計算器值就減1;當計數器值為0的對象就是不可能再被使用的。
當兩個對象相互引用時,此時引用計數器的值永遠不為0,導致無法對它們進行垃圾回收。
public class ReferenceCountingGC { public Object instance = null; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA .instance = objB ; objB .instance = objA ; objA = null; objB = null; System.gc(); } }可達性分析算法
以GC Roots為起始點,從這些節點開始向下搜索,能夠搜索到的對象都是存活的,不可達的對象則為不可用。
在Java語言中,可作為GC Roots的對象包括下面幾種:
虛擬機棧中引用的對象
方法區中靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中Native方法引用的對象
引用類型無論是引用計數算法還是可達性分析算法判斷對象是否存活都與引用有關。在JDK1.2之后,Java對引用的概念進行了擴充,劃分為強度不同的四個的引用類型。
強引用通過new來創建對象的引用類型,被強引用的對象永遠不會被垃圾收集器回收。
Object obj = new Object();軟引用
通過SortReference類來實現,只有在內存不足的時候才會被回收。
Object obj = new Object(); SoftReference弱引用
通過WeakReference類來實現,只能存活到下一次垃圾收集發生之前。
Object obj = new Object(); WeakReferencewr = new WeakReference (obj); obj = null;
WeakHashMap 的 Entry 繼承自 WeakReference,主要用來實現緩存。
private static class Entry
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 來實現緩存功能。ConcurrentCache 采取的是分代緩存,經常使用的對象放入 eden 中,而不常用的對象放入 longterm。eden 使用 ConcurrentHashMap 實現,longterm 使用 WeakHashMap,保證了不常使用的對象容易被回收。
public final class ConcurrentCache虛引用{ private final int size; private final Map eden; private final Map longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { V v = this.eden.get(k); if (v == null) { v = this.longterm.get(k); if (v != null) this.eden.put(k, v); } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { this.longterm.putAll(this.eden); this.eden.clear(); } this.eden.put(k, v); } }
也稱為幽靈引用或者幻影引用,是最弱的一種引用關系。
通過PhantomReference類來實現,為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
Object obj = new Object(); PhantomReference垃圾收集算法 標記清除wr = new PhantomReference (obj, null); obj = null;
算法分為“標記”跟“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成之后統一回收所有被標記的對象。
不足:
效率問題,標記跟清除兩個過程的效率都不高
空間問題,標記清除之后會產生大量不連續的內存碎片。
復制算法將內存分為大小相等的兩塊,每次只使用其中的一塊,當這塊內存用完了,就將還存活的對象負責到另一塊上面,然后再把一是要難過過得內存空間一次清理掉。
不足:
代價太高,只使用一半內存。
標記整理算法首先標記出所有需要回收的對象,然后將所有存活的對象都向一端移動,最后清理掉端邊界以外的內存。
分代收集算法根據對象的存活周期將內存劃分為幾塊。一般將Java堆分為新生代跟老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
新生代:復制算法
老年代:標記整理或標記清除算法。
垃圾收集器如果說手機算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。
上圖展示了7種不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。
知道目前為止還沒有最好的收集器出現,更加沒有萬能的收集器,所以我們只能選擇對具體應用最合適的收集器。
Serial收集器最基本、最悠久的收集器,單線程收集器,復制算法
在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
相比較與其他收集器,它具有:簡單而高效的特點,對于限定CPU環境來說,Serial收集器沒有線程交互的開銷。
依然是虛擬機運行在Client模式下的默認新生代收集器。
ParNew收集器Serial的多線程版本、并行,復制算法。
是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為目前除了Serial收集器外,只有它能與CMS收集器配合使用。
默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
Parallel Scavenge收集器新生代、并行的多線程收集器,復制算法。
Parallel Scavenge收集器的目標是達到一個可控制的吞吐量:CPU用戶運行用戶代碼的時間與CPU的執行時間,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。
停頓時間越短越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。
Parallel Scavenge收集器提供了兩個參數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
Serial Old收集器Serial的老年代版本,單線程,標記-整理算法。
這個收集器的主要意義在于給Client模式下的虛擬機使用,如果在Server默認下,它還有兩大用途:
在JDK1.5以及之前版本中與Parallel Scavenge收集器搭配使用。
作為CMS收集器的后備方案。
Parallel Old收集器parallel Scavenge的老年代版本,多線程,標記-整理算法,JDK1.6之后提供。
在注重吐吞量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。
CMS收集器CMS(Concurrent Mark Sweep),從名字來就可以看出,基于標記-清除算法。
并發收集、低停頓。
運算過程分為4個步驟:
初始標記:標記GC Roots能直接關聯得到的對象,速度很快
并發標記:進行GC Roots Tracing過程,時間較長,不停頓
重新標記:修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,停頓時間一般比初始標記稍長一些,但遠比并發標記短。
并發清理:耗時較長,不停頓。
整個過程耗時最長的并發標記和并發清除過程收集器線程都可以與用戶安城一起工作。
CMS還遠達不到完美的程度,還有以下3個缺點:
對CPU資源敏感,并發階段占用CPU資源而導致用戶線程變慢;低停頓時間是以犧牲吞吐量為代價的,導致CPU利用率不高。
無法處理浮動垃圾,由于CMS并發清理階段用戶線程還在運行著,伴隨著程序運行自然還會有新的垃圾產生,這部分垃圾CMS無法在檔次收集中處理掉它們,只有留待下一次GC時再清理。也是由于垃圾收集階段用戶還需要運行,故還需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了在進行收集,需要預留一部分空間提供并發收集時的程序運作使用。
標記-清除算法會導致大量的空間碎片,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩余卻無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。
G1收集器一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替代掉JDK1.5中發布的CMS收集器。
與其他收集器相比,G1具有以下特點:
并發與并行:使用多個CPU來縮短Stop-The-World停頓的時間。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保存,雖然G1可以不需要其他收集器配合就能獨立管理GC堆,但它能夠采用不同的方式去處理新創建的對象和一存活了一段時間、熬過多次GC的舊對象。
空間整合:從整體來看是基于標記-整理算法實現的收集器,從局部(兩個Region之間)來看是基于復制算法來實現的,意味著G1運作期間不會產生內存空間碎片。
可預測停頓:能夠讓使用者明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
G1將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離級別,它們都是一部分Region的集合。
G1收集器之所以能夠建立可預測的停頓時間模型,是因為它可以有計劃低避免在整個Java堆中進行全區域的垃圾回收。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。
虛擬機使用Remembered Set來避免全棧掃描,G1中每個Region都有一個與之對應的Remembered Set,用來記錄該Region對象的引用對象所在的Region。
如果不計算Remembered Set的操作,G1收集器的運作大致可分為:
初始標記
并發標記
最終標記:為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變化的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合并到Remembered Set中,這階段需要停頓線程,但是可以并行執行。
篩選回收:首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。此階段其實可以做到與用戶程序一起并發執行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
回收策略新生代GC(Minor GC):發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生息滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(major GC/full GC):發生在老年代的GC,出現了Major GC經常會伴隨一次的Minor GC(但非絕對,在paraller Scavenge收集器的手機策略里就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
Full GC的觸發條件
調用System.gc():只是建議虛擬機執行Full GC,但是虛擬機不一定真正地執行,不建議使用這種方式,而是讓虛擬機管理內存。
老年代空間不足:老年代空間不足的最常見場景為前文所講的大對象直接進入老年代,長期存活的對象進入老年代等。為了避免以上原因引起的 Full GC,應當盡量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬機參數調大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。
空間分配擔保失敗:使用復制算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。具體內容請參考上面的第五小節。
JDK 1.7 及以前的永久代空間不足:在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的為一些 Class 的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError。為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉為使用 CMS GC。
Concurrent Mode Failure:執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(有時候“空間不足”是指 CMS GC 當前的浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,并觸發 Full GC。
內存分配策略對象的內存分配規則并不是百分百固定的,其細節取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存有關的參數設置。
對象優先在eden分配
大對象直接進入老年代
長期存活對象進入老年代:Survivor區對象每經歷一次Minor GC,年齡就增加1,當年齡達到MaxTenuringThreshold設置的值(默認15),就將會被晉升到老年代。
動態對象年齡判定:如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代。
空間分配擔保:在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC就是安全的。如果不成立,則虛擬機會先查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將會嘗試進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
類加載機制 類加載的時機虛擬機規范嚴格規定了有且只有下面5種情況必須立即對類進行初始化:
遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果累沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的java代碼場景:使用new關鍵字實例化對象,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)、調用一個類的靜態方法。
使用java.lang.reflect包的方法對類進行反射調用,如果類沒有初始化則需要先觸發其初始化。
當虛擬機加載一個類的時候,父類還沒有進行初始化,則需先觸發父類初始化。
但虛擬機啟東市,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先執行這個主類。
當使用JDK1.7的動態語言規則時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法所對應的類還沒有進行初始化,則需要先觸發其初始化。
以下情況不會初始化:
通過子類引用父類靜態變量,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段只會初始化父類。
通過數組定義來引用類。SuperClass[] sca = new SuperClass[];。
引用常量,常量在編輯階段會存入調用類的常用池中,本質上并沒有直接引用到定義常量的類。
類加載的過程 加載在加載階段,虛擬機需要完成下面三件事:
通過一個類的全限定名來獲取定義此類的二進制字節流。
將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
在內存中生成一個代表這個類的java.lang.Class對象,作為方法去對這個類的各種數據的訪問入口。
驗證驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前的虛擬機要求,并且不會危害虛擬機自身的安全。
從整體看,驗證階段大致上會完成4個階段的檢驗動作:
文件格式驗證:驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。(魔數、主、次版本號等)
元數據驗證:對字節碼描述的信息進行語義分析,驗證點:是否有父類、是否繼承了不被允許繼承的類、如果不是抽象類,是否實現了其父類或接口之中要求實現的所有方法。
字節碼驗證:通過數據流和控制分析確定程序的語義是否合法、符合邏輯。
符號引用驗證:發生在虛擬機將符號引用轉化為直接飲用的時候,這個轉化動作在解析階段發生。符號引用可以看做對類自身以外(常量池中各種符號引用)的信息進行匹配性檢驗:能夠通過字符串描述的全限定名找到對應的類、方法、字段以及訪問性能否被當前類訪問。
準備正式為類變量分配內存并設置初始值的階段,這些變量所使用的內存都將在方法區中進行分配。同時設置變量的初始值(零值)。
解析將常量池中的符號引用替換成直接飲用的過程。
符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
直接引用:直接指向目標的執行、相對偏移量或能間接定位到目標的句柄。
初始化開始執行類中定義的java程序代碼,根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者說執行類構造器
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成
虛擬機保證一個類的
虛擬機設計團隊把類加載階段中“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到java虛擬機外部去實現,以便讓應用程序自己決定如何獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。
對于任意一個類,都需要由加載它的加載器和這個類本身確立其在Java虛擬機中的唯一性,每一個類加載器都擁有一個獨立的類名稱空間。兩個類“相等”包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字作對象所屬關系判定等情況。
從Java虛擬機的角度來講,只存在兩種不同的類加載器:
啟動類加載器(Bootstrap ClassLoader):這個類加載器使用C++語言實現,是虛擬機自身的一部分。
所有其他的類加載器:由Java語言實現獨立于虛擬機外部,并且全部繼承自抽象類java.lang.ClassLoader。
從Java開發人員的角度來看,類加載器劃分為更細致一些:
啟動類加載器(Bootstrp ClassLoader):負責將存在
擴展類加載器(Extemsion ClassLoader):由sun.misc.Launcher$ExtClassLoader實現,負責加載
應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$App-ClassLoader實現。由于這個類加載器是Classloader中的getSystemClassLoader()的返回值,所以一般也稱為系統類加載器。負責加載用戶路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器。如果應用程序沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
雙親委派模型應用程序都是由三種類加載器互相配合進行加載的,如果有必要還可以加入自己定義的類加載器,這些類加載器之間的關系一般如下:
圖中展示的類加載器之間的這種層次關系,稱為類加載器的雙親委派模型,雙親委派模型除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器,這里加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都是用組合(Composition)關系來服用父加載器的代碼。
如果一個類加載器收到了類加載的請求,它首先會把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有父加載器反饋自己無法嘗試完成這個加載請求時,子加載器才會嘗試自己去加載。
Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。
例如類java.lang.Object存放在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器來進行加載,因此Object類在程序的各種加載器環境中都是一個類。相反如果沒有雙親委派模型,如果用戶自己編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那么系統中將會出現多個不同的Object類。
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71621.html
摘要:由虛擬機加載的類,被加載到虛擬機內存中之后,虛擬機會讀取并執行它里面存在的字節碼指令。虛擬機中執行字節碼指令的部分叫做執行引擎。 什么是Java虛擬機? 作為一個Java程序員,我們每天都在寫Java代碼,我們寫的代碼都是在一個叫做Java虛擬機的東西上執行的。但是如果要問什么是虛擬機,恐怕很多人就會模棱兩可了。在本文中,我會寫下我對虛擬機的理解。因為能力所限,可能有些地方描述的不夠欠...
摘要:虛擬機發展史注本文大部分摘自深入理解虛擬機第二版作為一名開發人員,不能局限于語言規范,更需要對虛擬機規范有所了解。虛擬機規范有多種實現,其中是和中所帶的虛擬機,也是目前使用范圍最廣的虛擬機。世界第一款商用虛擬機。號稱世界上最快的虛擬機。 Java虛擬機發展史 注:本文大部分摘自《深入理解Java虛擬機(第二版)》 作為一名Java開發人員,不能局限于Java語言規范,更需要對Java虛...
摘要:虛擬機運行時數據區分為以下幾個部分。程序計數器也是在虛擬機規范中唯一沒有規定任何異常情況的區域。在方法運行期間不會改變局部變量表的大小。長度在位和位的虛擬機中,分別為官方稱它為。 Java虛擬機運行時數據區 詳解 2.1 概述 本文參考的是周志明的 《深入理解Java虛擬機》第二章 ,為了整理思路,簡單記錄一下,方便后期查閱。 2.2 運行時數據區域 Java虛擬機在Java程序運行時...
摘要:此處指定的虛擬機與平臺兼容,并支持語言規范中指定的編程語言。第章說明了虛擬機的指令集,按字母順序顯示操作碼助記符。 介紹 一點歷史 Java?編程語言是一種通用的、并發的、面向對象的語言,它的語法類似于C和C++,但它省略了許多使C和C++復雜、混亂和不安全的特性。最初開發Java平臺是為了解決為聯網的消費者設備構建軟件的問題,它旨在支持多種主機架構,并允許安全交付軟件組件,為了滿足這...
摘要:虛擬機在執行程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。棧幀棧幀是用于支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。棧幀的概念結構如下運行時數據區腦圖高 這里我們先說句題外話,相信大家在面試中經常被問到介紹Java內存模型,我在面試別人時也會經常問這個問題。但是,往往都會令我比較尷尬,我還話音未落,面試者就會背誦一段(Java虛擬...
摘要:原始類型和值虛擬機支持的原始數據類型是數字類型布爾類型和類型。,其值為位帶符號的二進制補碼整數,其默認值為零。 Java虛擬機的結構 本文檔指定了一個抽象機器,它沒有描述Java虛擬機的任何特定實現。 要正確實現Java虛擬機,你只需要能夠讀取類文件格式并正確執行其中指定的操作,不屬于Java虛擬機規范的實現細節會不必要地限制實現者的創造力。例如,運行時數據區的內存布局、使用的垃圾收集...
閱讀 2453·2021-11-23 09:51
閱讀 503·2019-08-30 13:59
閱讀 1820·2019-08-29 11:20
閱讀 2529·2019-08-26 13:41
閱讀 3237·2019-08-26 12:16
閱讀 729·2019-08-26 10:59
閱讀 3321·2019-08-26 10:14
閱讀 601·2019-08-23 17:21