java虛擬機

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運行時區域

clipboard.png

程序計數器

內存中較小的內存空間,經過計數器的值能夠選取下一條執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。html

線程私有,生命週期跟線程相同。java

若是正在執行一個Native方法,那麼這個計數器值將爲空。程序員

虛擬機棧

線程私有,生命週期跟線程相同。算法

每一個方法在執行同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。apache

在Java虛擬機規範中,對這個區域規定了兩種異常狀況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;
若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。ubuntu

本地方法棧

跟虛擬機棧所發揮的做用類似,區別在於虛擬機棧爲虛擬機執行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 詳解

HotSpot虛擬機對象

對象的建立

在語言層上,建立對象一般僅僅是一個new關鍵字而已,而當虛擬機遇到一條new執行時,將由一下步驟:

  • 檢查類是否加載、解析、初始化過,沒有則先執行相應的類加載過程。
  • 在堆中分配內存

    • 劃分可用空間:

      • 指針碰撞:堆內存規整
      • 空閒列表:堆內存不規整
    • 併發問題

      • 同步:採用CAS配上失敗重試的方式保證更新操做的原子性
      • 把內存分配動做按照線程劃分在不一樣的空間之中進行
  • 將分配到的內存空間都初始化零值
  • 設置對象的類實例、元數據、哈希碼、GC分代年齡等信息。
  • 執行<init>方法

對象的內存佈局

對象在內存中儲存的佈局能夠分爲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.jar

jstack

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,我這裏用第三個,輸出以下:

clipboard.png

TIME列就是各個Java線程耗費的CPU時間,CPU時間最長的是線程ID爲21742的線程,用

printf "%x\n" 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

打印進程的類加載器和類加載器加載的持久代對象信息,輸出:類加載器名稱、對象是否存活(不可靠)、對象地址、父類加載器、已加載的類大小等信息,以下圖:

clipboard.png

使用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        5597736  <constMethodKlass>
   2:         38445        5237288  <methodKlass>
   3:          3500        3749504  <constantPoolKlass>
   4:         60858        3242600  <symbolKlass>
   5:          3500        2715264  <instanceKlassKlass>
   6:          2796        2131424  <constantPoolCacheKlass>
   7:          5543        1317400  [I
   8:         13714        1010768  [C
   9:          4752        1003344  [B
  10:          1225         639656  <methodDataKlass>
  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  <objArrayKlassKlass>
  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查看了:

clipboard.png

上面紅線框出來的部分你們能夠本身去摸索下,最後一項支持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堆內存佈局:

clipboard.png

能夠看出:

堆內存 = 年輕代 + 年老代 + 永久代
年輕代 = 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=<file>            write data to file             java.hprof[.txt]
net=<host>:<port>      send data over a socket        off
depth=<size>           stack trace depth              4
interval=<ms>          sample interval in ms          10
cutoff=<value>         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 <file>         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爲起始點,從這些節點開始向下搜索,可以搜索到的對象都是存活的,不可達的對象則爲不可用。

clipboard.png

在Java語言中,可做爲GC Roots的對象包括下面幾種:

  • 虛擬機棧中引用的對象
  • 方法區中靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中Native方法引用的對象

引用類型

不管是引用計數算法仍是可達性分析算法判斷對象是否存活都與引用有關。在JDK1.2以後,Java對引用的概念進行了擴充,劃分爲強度不一樣的四個的引用類型。

強引用

經過new來建立對象的引用類型,被強引用的對象永遠不會被垃圾收集器回收。

Object obj = new Object();

軟引用

經過SortReference類來實現,只有在內存不足的時候纔會被回收。

Object obj = new Object();
    SoftReference<Object> sr = new SoftReference<Object>(obj);
    obj = null;

弱引用

經過WeakReference類來實現,只能存活到下一次垃圾收集發生以前。

Object obj = new Object();
    WeakReference<Object> wr = new WeakReference<Object>(obj);
    obj = null;

WeakHashMap 的 Entry 繼承自 WeakReference,主要用來實現緩存。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 來實現緩存功能。ConcurrentCache 採起的是分代緩存,常用的對象放入 eden 中,而不經常使用的對象放入 longterm。eden 使用 ConcurrentHashMap 實現,longterm 使用 WeakHashMap,保證了不常使用的對象容易被回收。

public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> 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<Object> wr = new PhantomReference<Object>(obj, null);
    obj = null;

垃圾收集算法

標記清除

算法分爲「標記」跟「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成以後統一回收全部被標記的對象。

不足:

  • 效率問題,標記跟清除兩個過程的效率都不高
  • 空間問題,標記清除以後會產生大量不連續的內存碎片。

clipboard.png

複製算法

將內存分爲大小相等的兩塊,每次只使用其中的一塊,當這塊內存用完了,就將還存活的對象負責到另外一塊上面,而後再把一是要難過過得內存空間一次清理掉。

不足:

  • 代價過高,只使用一半內存。

clipboard.png

標記整理算法

首先標記出全部須要回收的對象,而後將全部存活的對象都向一端移動,最後清理掉端邊界之外的內存。

clipboard.png

分代收集算法

根據對象的存活週期將內存劃分爲幾塊。通常將Java堆分爲新生代跟老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。

  • 新生代:複製算法
  • 老年代:標記整理或標記清除算法。

垃圾收集器

若是說手機算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

clipboard.png

上圖展現了7種不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。

知道目前爲止尚未最好的收集器出現,更加沒有萬能的收集器,因此咱們只能選擇對具體應用最合適的收集器。

Serial收集器

clipboard.png

最基本、最悠久的收集器,單線程收集器,複製算法

在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束。

相比較與其餘收集器,它具備:簡單而高效的特色,對於限定CPU環境來講,Serial收集器沒有線程交互的開銷。

依然是虛擬機運行在Client模式下的默認新生代收集器。

ParNew收集器

clipboard.png

Serial的多線程版本、並行,複製算法。

是許多運行在Server模式下的虛擬機中首選的新生代收集器,由於目前除了Serial收集器外,只有它能與CMS收集器配合使用。

默認開啓的收集線程數與CPU的數量相同,在CPU很是多的環境下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

Parallel Scavenge收集器

新生代、並行的多線程收集器,複製算法。

Parallel Scavenge收集器的目標是達到一個可控制的吞吐量:CPU用戶運行用戶代碼的時間與CPU的執行時間,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。

停頓時間越短越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

Serial Old收集器

clipboard.png

Serial的老年代版本,單線程,標記-整理算法。

這個收集器的主要意義在於給Client模式下的虛擬機使用,若是在Server默認下,它還有兩大用途:

  • 在JDK1.5以及以前版本中與Parallel Scavenge收集器搭配使用。
  • 做爲CMS收集器的後備方案。

Parallel Old收集器

clipboard.png

parallel Scavenge的老年代版本,多線程,標記-整理算法,JDK1.6以後提供。

在注重吐吞量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器

clipboard.png

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的集合。

clipboard.png

G1收集器之因此可以創建可預測的停頓時間模型,是由於它能夠有計劃低避免在整個Java堆中進行全區域的垃圾回收。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region。

虛擬機使用Remembered Set來避免全棧掃描,G1中每一個Region都有一個與之對應的Remembered Set,用來記錄該Region對象的引用對象所在的Region。

clipboard.png

若是不計算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[];
  • 引用常量,常量在編輯階段會存入調用類的經常使用池中,本質上並無直接引用到定義常量的類。

類加載的過程

clipboard.png

加載

在加載階段,虛擬機須要完成下面三件事:

  • 經過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法去對這個類的各類數據的訪問入口。

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前的虛擬機要求,而且不會危害虛擬機自身的安全。

從總體看,驗證階段大體上會完成4個階段的檢驗動做:

  • 文件格式驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。(魔數、主、次版本號等)
  • 元數據驗證:對字節碼描述的信息進行語義分析,驗證點:是否有父類、是否繼承了不被容許繼承的類、若是不是抽象類,是否實現了其父類或接口之中要求實現的全部方法。
  • 字節碼驗證:經過數據流和控制分析肯定程序的語義是否合法、符合邏輯。
  • 符號引用驗證:發生在虛擬機將符號引用轉化爲直接飲用的時候,這個轉化動做在解析階段發生。符號引用能夠看作對類自身之外(常量池中各類符號引用)的信息進行匹配性檢驗:可以經過字符串描述的全限定名找到對應的類、方法、字段以及訪問性可否被當前類訪問。

準備

正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區中進行分配。同時設置變量的初始值(零值)。

解析

將常量池中的符號引用替換成直接飲用的過程。

  • 符號引用:以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。
  • 直接引用:直接指向目標的執行、相對偏移量或能間接定位到目標的句柄。

初始化

開始執行類中定義的java程序代碼,根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源,或者說執行類構造器<clinit>()方法的過程。

  • <clinit>()方法和構造函數不一樣,虛擬機保證子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。
  • <clinit>()方法並非必需的,若是累沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。
  • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法,可是與類不一樣的是,執行藉口的<clinit>()方法先執行父接口的,只有當父接口定義的變量被使用時,父接口才會初始化。
  • 虛擬機保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步。

類加載器

虛擬機設計團隊把類加載階段中「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到java虛擬機外部去實現,以便讓應用程序本身決定如何獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。

對於任意一個類,都須要由加載它的加載器和這個類自己確立其在Java虛擬機中的惟一性,每個類加載器都擁有一個獨立的類名稱空間。兩個類「相等」包括表明類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關係斷定等狀況。

從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):這個類加載器使用C++語言實現,是虛擬機自身的一部分。
  • 全部其餘的類加載器:由Java語言實現獨立於虛擬機外部,而且所有繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,類加載器劃分爲更細緻一些:

  • 啓動類加載器(Bootstrp ClassLoader):負責將存在<JAVA_HOME>lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接飲用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給引導類加載器,那直接使用null替代便可。
  • 擴展類加載器(Extemsion ClassLoader):由sun.misc.Launcher$ExtClassLoader實現,負責加載<JAVA_HOME>libext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$App-ClassLoader實現。因爲這個類加載器是Classloader中的getSystemClassLoader()的返回值,因此通常也稱爲系統類加載器。負責加載用戶路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器。若是應用程序沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

雙親委派模型

應用程序都是由三種類加載器互相配合進行加載的,若是有必要還能夠加入本身定義的類加載器,這些類加載器之間的關係通常以下:

clipboard.png

圖中展現的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型,雙親委派模型除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器,這裏加載器之間的父子關係通常不會以繼承(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;
        }
    }
相關文章
相關標籤/搜索