Java 虛擬機(Java virtual machine,JVM)是運行 Java 程序必不可少的機制。JVM實現了Java語言最重要的特征:即平臺無關性。原理:編譯后的 Java 程序指令并不直接在硬件系統的 CPU 上執行,而是由 JVM 執行。JVM屏蔽了與具體平臺相關的信息,使Java語言編譯程序只需要生成在JVM上運行的目標字節碼(.class),就可以在多種平臺上不加修改地運行。Java 虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。因此實現java平臺無關性。它是 Java 程序能在多平臺間進行無縫移植的可靠保證,同時也是 Java 程序的安全檢驗引擎(還進行安全檢查)。
JVM 是 編譯后的 Java 程序(.class文件)和硬件系統之間的接口 ( 編譯后:javac 是收錄于 JDK 中的 Java 語言編譯器。該工具可以將后綴名為. java 的源文件編譯為后綴名為. class 的可以運行于 Java 虛擬機的字節碼。)
JVM = 類加載 classloader subsystem + 執行引擎 execution engine + 運行時數據區域 runtime data area
Classloader 把硬盤上的class 文件加載到JVM中的運行時數據區域, 但是它不負責這個類文件能否執行,而這個是 執行引擎 負責的。
類加載 Class Loader?Sub System?
JVM將類的加載分為3個步驟:加載(Load)、鏈接(Link)、初始化(Initialize),如下圖所示:
1) 加載:查找并加載類的二進制數據(查找和導入Class文件)
加載是類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:
1、通過一個類的全限定名來獲取其定義的二進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
3、在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口。
相對于類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
加載階段完成后,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。
雙親委派模型(Parent Delegation Model):
類的加載過程采用雙親委托機制,這種機制能更好的保證 Java 平臺的安全。
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,并不繼承自java.lang.ClassLoader。
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader()來獲取它。
除了系統提供的類加載器以外,開發人員可以通過繼承java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。
類加載器 classloader 是具有層次結構的,也就是父子關系。其中,Bootstrap 是所有類加載器的父親。如下圖所示:
雙親委派模型的工作過程為:
1.當前 ClassLoader 首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。(每個類加載器都有自己的加載緩存,當一個類被加載了以后就會放入緩存,等下次加載的時候就可以直接返回了)
2.當前 classLoader 的緩存中沒有找到被加載的類的時候,委托父類加載器去加載,父類加載器采用同樣的策略,首先查看自己的緩存,然后委托父類的父類去加載,一直到 bootstrap ClassLoader.
3.當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,并將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。
使用這種模型來組織類加載器之間的關系的好處:
主要是為了安全性,避免用戶自己編寫的類動態替換 Java 的一些核心類,比如 String,同時也避免了重復加載,因為 JVM 中區分不同類,不僅僅是根據類名,相同的 class 文件被不同的 ClassLoader 加載就是不同的兩個類,如果相互轉型的話會拋java.lang.ClassCaseException.
2) 鏈接(分3個步驟)
? ??1、驗證:確保被加載的類的正確性
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證:驗證字節流是否符合Class文件格式的規范;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;例如:這個類是否有父類,除了java.lang.Object之外。
字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那么可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
? ??2、準備:為類的靜態變量分配內存,并將其初始化為默認值
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:
1、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
2、這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設一個類變量的定義為:public static int value = 3; 那么變量value在準備階段過后的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放于類構造器方法之中的,所以把value賦值為3的動作將在初始化階段才會執行。
? ??3、解析:把類中的符號引用轉換為直接引用
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
3) 初始化:對類的靜態變量,靜態代碼塊執行初始化操作
初始化,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。
類的初始化步驟 / JVM初始化步驟:
1)如果這個類還沒有被加載和鏈接,那先進行加載和鏈接
2)假如這個類存在直接父類,并且這個類還沒有被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類(不適用于接口)
3 ) 假如類中存在初始化語句(如static變量和static塊),那就依次執行這些初始化語句。
何時開始類的初始化
虛擬機規范中并沒強行約束何時開始“加載”,這點可以交給虛擬機的的具體實現自由把握,但是對于“初始化”階段虛擬機規范是嚴格規定了如下幾種情況,如果類未初始化會對類進行初始化。
? ? 1、遇到new,getstatic, putstatic, 或者 invokestatic 這4條字節碼指令時,如果沒有進行過初始化,則需要先觸發其初始化。常見場景有使用new實例化對象,讀取或設置一個類的靜態字段,調用一個類的靜態方法。
? ? 2、使用java.lang.reflect包的方法隊里進行反射調用的時候,如果沒有進行過初始化,則需先觸發其初始化。
? ? 3、當初始化一個類的時候,發現其父類沒有進行初始化,則先觸發其父類的初始化。
? ? 4、當虛擬機啟動的時候,用戶需要指定一個需要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
? ? 5、當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需先要觸發其初始化。
執行引擎 Execution Engine
通過類裝載器裝載的,被分配到JVM的運行時數據區的字節碼會被執行引擎執行。執行引擎以指令為單位讀取Java字節碼。它就像一個CPU一樣,一條一條地執行機器指令。每個字節碼指令都由一個1字節的操作碼和附加的操作數組成。執行引擎取得一個操作碼,然后根據操作數來執行任務,完成后就繼續執行下一條操作碼。
不過Java字節碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎必須把字節碼轉換成可以直接被JVM執行的語言。字節碼可以通過以下兩種方式轉換成合適的語言。
解釋器:一條一條地讀取,解釋并且執行字節碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋字節碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。字節碼這種“語言”基本來說是解釋執行的。
即時(Just-In-Time)編譯器:即時編譯器被引入用來彌補解釋器的缺點。執行引擎首先按照解釋執行的方式來執行,然后在合適的時候,即時編譯器把整段字節碼編譯成本地代碼。然后,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過本地代碼去執行它。執行本地代碼比一條一條進行解釋執行的速度快很多。編譯后的代碼可以執行的很快,因為本地代碼是保存在緩存里的。
不過,用JIT編譯器來編譯代碼所花的時間要比用解釋器去一條條解釋執行花的時間要多。因此,如果代碼只被執行一次的話,那么最好還是解釋執行而不是編譯后再執行。因此,內置了JIT編譯器的JVM都會檢查方法的執行頻率,如果一個方法的執行頻率超過一個特定的值的話,那么這個方法就會被編譯成本地代碼。
當需要加載某個類時,編譯器會首先找到其.class文件,然后將該類的字節碼裝入內存。此時有兩種方案可供選擇。一種是讓即時編譯器編譯所有代碼。但這種做法有兩個缺陷:這種加載動作散落在整個程序生命周期內,累加起來要花更多時間;并且會增加可執行代碼的長度(字節碼比即時編譯器展開后的本地機器碼小很多),這將導致頁面調度,從而降低程序速度。另一種做法稱為惰性評估,意思是即時編譯器只在必要的時候才編譯代碼。這樣,從不會執行的代碼也許就壓根不會被JIT所編譯。新版Java HotSpot就采用了類似的方法,代碼每次執行的時候都會做一些優化,所以執行的次數越多,它的速度就越快。
JVM規范沒有定義執行引擎該如何去執行。因此,JVM的提供者通過使用不同的技術以及不同類型的JIT編譯器來提高執行引擎的效率。
大部分的JIT編譯器都是按照下圖的方式來執行的:
JIT編譯器把字節碼轉換成一個中間層表達式,一種中間層的表示方式,來進行優化,然后再把這種表示轉換成本地代碼。
Oracle Hotspot VM使用一種叫做熱點編譯器的JIT編譯器。它之所以被稱作”熱點“是因為熱點編譯器通過分析找到最需要編譯的“熱點”代碼,然后把熱點代碼編譯成本地代碼。如果已經被編譯成本地代碼的字節碼不再被頻繁調用了,換句話說,這個方法不再是熱點了,那么Hotspot VM會把編譯過的本地代碼從cache里移除,并且重新按照解釋的方式來執行它。Hotspot VM分為Server VM和Client VM兩種,這兩種VM使用不同的JIT編譯器。
Client VM 和Server VM使用完全相同的Runtime,不過如上圖所示,它們所使用的JIT編譯器是不同的。Server VM用的是更高級的動態優化編譯器,這個編譯器使用了更加復雜并且更多種類的性能優化技術。
大部分Java程序的性能都是通過提升執行引擎的性能來達到的。正如JIT編譯器一樣,很多優化的技術都被引入進來使得JVM的性能一直能夠得到提升。最原始的JVM和最新的JVM最大的差別之處就是在于執行引擎。
運行時數據區域? Runtime Data Areas
JVM 運行時數據區 (JVM Runtime Area) 其實就是指 JVM 在運行期間,其對JVM內存空間的劃分和分配。JVM在運行時將數據劃分為了6個區域來存儲。
程序員寫的所有程序都被加載到運行時數據區域中,不同類別存放在堆(heap), 棧(stack), 方法區(method area), 本地方法棧(native method stack), 程序計數器(PC register)
下面對各個部分的功能和存儲的內容進行描述:
PC程序計數器:一塊較小的內存空間,可以看做是當前線程所執行的字節碼的行號指示器, NAMELY存儲每個線程下一步將執行的JVM指令,如果該方法為native的,則PC寄存器中不存儲任何信息。Java 的多線程機制離不開程序計數器,每個線程都有一個自己的PC,以便完成不同線程上下文環境的切換。
java虛擬機棧:與 PC 一樣,java 虛擬機棧也是線程私有的。每一個 JVM 線程都有自己的 java 虛擬機棧,這個棧與線程同時創建,它的生命周期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
本地方法棧:與虛擬機棧的作用相似,虛擬機棧為虛擬機執行執行java方法服務,而本地方法棧則為虛擬機使用到的本地方法服務。
Java堆:被所有線程共享的一塊存儲區域,在虛擬機啟動時創建,它是JVM用來存儲對象實例以及數組值的區域,可以認為Java中所有通過new創建的對象的內存都在此分配。
Java堆在JVM啟動的時候就被創建,堆中儲存了各種對象,這些對象被自動管理內存系統(Automatic Storage Management System,也即是常說的 “Garbage Collector(垃圾回收器)”)所管理。這些對象無需、也無法顯示地被銷毀。
JVM將Heap分為兩塊:新生代(New Generation)和舊生代(Old Generation)
Note:
堆在JVM是所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖,這也是new開銷比較大的原因。
鑒于上面的原因,Sun Hotspot JVM為了提升對象內存分配的效率,對于所創建的線程都會分配一塊獨立的空間,這塊空間又稱為TLAB
TLAB僅作用于新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效
方法區:方法區和堆區域一樣,是各個線程共享的內存區域,它用于存儲每一個類的結構信息,例如運行時常量池,成員變量和方法數據,構造函數和普通函數的字節碼內容,還包括一些在類、實例、接口初始化時用到的特殊方法。當開發人員在程序中通過Class對象中的getName、isInstance等方法獲取信息時,這些數據都來自方法區。
方法區也是全局共享的,在虛擬機啟動時候創建。在一定條件下它也會被GC。這塊區域對應Permanent Generation 持久代。 XX:PermSize指定大小。
運行時常量池:其空間從方法區中分配,存放的為類中固定的常量信息、方法和域的引用信息。
Note:
永久代的移除工作從JDK7開始 ,貯存在永久代的一部分數據已經轉移到了Java Heap或者是Native Heap。
但永久代仍然存在于JDK7,并沒有完全的移除:
符號引用(Symbols)轉移到了native heap;
字面量(interned strings)轉移到了java heap;
類的靜態變量(class statics)轉移到了java heap。
永久代在JDK8中被完全的移除了。
在JDK8中, 類的元數據信息 (class metadata)被存儲在叫做Metaspace的本地內存(native memory )。
類的元數據信息轉移到Metaspace的原因是PermGen很難調整。PermGen中類的元數據信息在每次FullGC的時候可能會被收集,但成績很難令人滿意。而且應該為PermGen分配多大的空間很難確定,因為PermSize的大小依賴于很多因素,比如JVM加載的class的總數,常量池的大小,方法的大小等。
此外,在HotSpot中的每個垃圾收集器需要專門的代碼來處理存儲在PermGen中的類的元數據信息。從PermGen分離類的元數據信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空間,因此Metaspace和Java Heap可以無縫的管理,而且簡化了FullGC的過程,以至將來可以并行的對元數據信息進行垃圾收集,而沒有GC暫停。
垃圾回收 GC
介紹JVM垃圾回收機制之前,需要對垃圾回收算法進行講解
垃圾回收算法
1、根搜索算法
根搜索算法是從離散數學中的圖論引入的,程序把所有引用關系看作一張圖,從一個節點GC ROOT 開始,尋找對應的引用節點,找到這個節點后,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。
上圖紅色為無用的節點,可以被回收。
目前Java中可以作為GC ROOT的對象有:
1、虛擬機棧中引用的對象(本地變量表)
2、方法區中靜態屬性引用的對象
3、方法區中常量引用的對象
4、本地方法棧中引用的對象(Native對象)
2、標記 - 清除算法
標記-清除算法采用從根集合進行掃描,對存活的對象進行標記,標記完畢后,再掃描整個空間中未被標記的對象進行直接回收,如上圖。
標記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活的對象比較多的情況下極為高效,但由于標記-清除算法直接回收不存活的對象,并沒有對還存活的對象進行整理,因此會導致內存碎片。
3、復制算法
復制算法將內存劃分為兩個區間,使用此算法時,所有動態分配的對象都只能分配在其中一個區間(活動區間),而另外一個區間(空間區間)則是空閑的。
復制算法采用從根集合掃描,將存活的對象復制到空閑區間,當掃描完畢活動區間后,會的將活動區間一次性全部回收。此時原本的空閑區間變成了活動區間。下次GC時候又會重復剛才的操作,以此循環。
復制算法在存活對象比較少的時候,極為高效,但是帶來的成本是犧牲一半的內存空間用于進行對象的移動。所以復制算法的使用場景,必須是對象的存活率非常低才行,而且最重要的是,我們需要克服50%內存的浪費。
4、標記 - 整理算法
標記-整理算法采用 標記-清除 算法一樣的方式進行對象的標記、清除,但在回收不存活的對象占用的空間后,會將所有存活的對象往左端空閑空間移動,并更新對應的指針。標記-整理 算法是在標記-清除 算法之上,又進行了對象的移動排序整理,因此成本更高,但卻解決了內存碎片的問題。
Java垃圾回收機制
Java垃圾回收機制是由垃圾收集器GC來實現的,GC是后臺的守護進程。它的特別之處是它是一個低優先級進程,但是可以根據內存的使用情況動態的調整他的優先級。因此,它是在內存中低到一定限度時才會自動運行,從而實現對內存的回收。這就是垃圾回收的時間不確定的原因。
為何要這樣設計:因為GC也是進程,也要消耗CPU等資源,如果GC執行過于頻繁會對java的程序的執行產生較大的影響(java解釋器本來就不快),因此JVM的設計者們選著了不定期的gc。
JVM GC回收哪個區域內的垃圾?
需要注意的是,JVM GC只回收堆區和方法區內的對象。而棧區的數據,在超出作用域后會被JVM自動釋放掉,所以其不在JVM GC的管理范圍內。
JVM GC什么時候執行?
eden區空間不夠存放新對象的時候,執行Minor GC(Young GC)。升到老年代的對象大于老年代剩余空間的時候執行Major GC(Full GC) 。調優主要是減少 Full GC 的觸發次數。
按代的垃圾回收機制
新生代(Young generation):絕大多數最新被創建的對象都會被分配到這里,由于大部分在創建后很快變得不可達,很多對象被創建在新生代,然后“消失”。對象從這個區域“消失”的過程我們稱之為:Minor GC或者Young GC。
舊生代(Old generation):對象沒有變得不可達,并且從新生代周期中存活了下來,會被拷貝到這里。其區域分配的空間要比新生代多。也正由于其相對大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代中消失的過程,稱之為:Major GC或者Full GC。
持久代(Permanent generation)也稱之為方法區(Method area):用于保存類常量以及字符串常量。注意,這個區域不是用于存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算為 Major GC 。只不過在這個區域發生GC的條件非常嚴苛,必須符合以下三種條件才會被回收:
1、所有實例被回收
2、加載該類的ClassLoader 被回收
3、Class 對象無法通過任何途徑訪問(包括反射)
可能我們會有疑問:
如果舊生代的對象需要引用新生代的對象,會發生什么呢?
為了解決這個問題,舊生代中存在一個card table,它是一個512byte大小的塊。所有舊生代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只需要查詢 card table 來決定是否可以被回收,而不用查詢整個舊生代。這個 card table 由一個write barrier來管理。write barrier給GC帶來了很大的性能提升,雖然由此可能帶來一些開銷,但完全是值得的。
默認的新生代(Young generation)、舊生代(Old generation)所占空間比例為 1 : 2 。
新生代空間的構成與邏輯
為了更好的理解GC,我們來學習新生代的構成,它用來保存那些第一次被創建的對象,它被分成三個空間:
· 一個伊甸園空間(Eden)
· 兩個幸存者空間(Fron Survivor、To Survivor)
默認新生代空間的分配:Eden : Fron : To = 8 : 1 : 1
每個空間的執行順序如下:
1、絕大多數剛剛被創建的對象會存放在伊甸園空間(Eden)。
2、在伊甸園空間執行第一次GC(Minor GC)之后,存活的對象被移動到其中一個幸存者空間(Survivor)。
3、此后,每次伊甸園空間執行GC后,存活的對象會被堆積在同一個幸存者空間。
4、當一個幸存者空間飽和,還在存活的對象會被移動到另一個幸存者空間。然后會清空已經飽和的哪個幸存者空間。
5、在以上步驟中重復N次(N = MaxTenuringThreshold(年齡閥值設定,默認15))依然存活的對象,就會被移動到老年代。
從上面的步驟可以發現,兩個幸存者空間,必須有一個是保持空的。如果兩個兩個幸存者空間都有數據,或兩個空間都是空的,那一定是你的系統出現了某種錯誤。
我們需要重點記住的是,對象在剛剛被創建之后,是保存在伊甸園空間的(Eden)。那些長期存活的對象會經由幸存者空間(Survivor)轉存到老年代空間(Old generation)。
也有例外出現,對于一些比較大的對象(需要分配一塊比較大的連續內存空間)則直接進入到老年代。一般在Survivor 空間不足的情況下發生。
老年代空間的構成與邏輯
老年代空間的構成其實很簡單,它不像新生代空間那樣劃分為幾個區域,它只有一個區域,里面存儲的對象并不像新生代空間絕大部分都是朝聞道,夕死矣。這里的對象幾乎都是從Survivor 空間中熬過來的,它們絕不會輕易的狗帶。因此,Full GC(Major GC)發生的次數不會有Minor GC 那么頻繁,并且做一次Major GC 的時間比Minor GC 要更長(約10倍)。
JVM為了優化內存的回收,使用了分代回收的方式,對于新生代內存的回收(Minor GC)主要采用復制算法。而對于老年代的回收(Major GC),大多采用標記-整理算法。詳情參考JVM 垃圾收集器。
新生代收集器:
Serial (-XX:+UseSerialGC)
ParNew(-XX:+UseParNewGC)
ParallelScavenge(-XX:+UseParallelGC)
G1 收集器
老年代收集器:
SerialOld(-XX:+UseSerialOldGC)
ParallelOld(-XX:+UseParallelOldGC)
CMS(-XX:+UseConcMarkSweepGC)
G1 收集器
JVM中將對象的引用分為了四種類型,不同的對象引用類型會造成GC采用不同的方法進行回收:
(1)強引用:默認情況下,對象采用的均為強引用(GC不會回收)
(2)軟引用:軟引用是Java中提供的一種比較適合于緩存場景的應用(只有在內存不夠用的情況下才會被GC)
(3)弱引用:在GC時一定會被GC回收
(4)虛引用:在GC時一定會被GC回收