JVM相關知識點

另可參考: http://www.stevenwash.xin/2018/04/21/JVM%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/

一、Java運行時數據區域

程序計數器

看做是當前線程說執行的字節碼的行號指示器。由於多線程是通過線程的輪流切換來分配CPU的處理時間的,所以,爲了使切換後的線程能正確按照之前的順序執行,則每個線程都有一個自己的程序計數器,各條線程之間的計數器是獨立存儲的,所以這個存儲空間的線程私有的。

Note:如何正在執行的是Native方法,則計數器的值爲空,此內存區域是唯一一個Java中沒有規定任何OutOfMemeryError情況的區域。

Java虛擬機棧

同程序計數器一樣,是屬於線程私有的。虛擬機棧描述的是每個方法運行時的內存模型,通過棧幀來存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

其中,局部變量表中存放了基本數據類型、對象引用類型(指向堆內存中對象的引用)和returnAddress類型(指向一條字節碼地址的指令)

Java堆

是被所有線程共享的內存區域,唯一目的就是存放對象實例,所以也是垃圾回收的主要場所,幾乎所有的對象都是在堆上分配。

Special:JIT和逃逸技術的發展,使得可能會發生一些小變化,這就導致所有對象都分配在堆上就不絕對了

內存回收的角度:
分代收集算法,分爲新生代和老年代,更細的就是Eden空間、From Survivor空間、To Survivor空間。

內存劃分的角度:
還會劃分出線程私有的分配緩衝區(TLAB),但是無論如何劃分存放的都是對象的實例。

本地方法棧

作用和Java虛擬機棧的作用一樣,只是這個是爲Java提供Native方法服務的。

方法區

是線程共享的內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器(JIT)編譯後的代碼等數據。

在這個區域也有少量的垃圾收集,是針對常量池的回收和類型的卸載。

運行時常量池:在方法區中用來存放常量信息(各種在類加載後放到常量池中的字面量和符號引用)

Special:直接內存

不是屬於JAVA運行時數據區的一部分。由於引入的NIO,是一種基於管道的和緩衝區的IO方式,直接分配堆外內存。

二、判斷對象是否死了

引用計數的算法

原理:給每一個對象添加一個引用計數器,每當有地方引用了這個對象,計數器就+1,某個地方的引用失效了則計數器-1。任何時刻計數器都爲0的對象,就表示已經不可能再被引用了。

缺陷:無法解決循環依賴引用的問題。因爲如果兩個對象循環引用了,則兩個對象的引用計數器永遠都不會爲0,於是無法回收。

可達性分析算法

通過一系列的成爲GC Roots的對象集(Set)來做爲起點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何一條引用鏈相連的時候,就證明此對象是不可用的,即可被回收。

可作爲GC Roots的對象

1、虛擬機棧中(棧幀中的本地變量表中)引用的對象
2、方法區中靜態引用指向的對象
3、方法區中常量引用指向的對象
4、本地方法棧中的Native方法引用的對象

當一個對象被判定爲不可達,則要經歷兩次標記纔會清理:
如果這個對象有必要執行的finalize()方法的時候,就會將這個對象放到F-Queue中等待虛擬機執行,但並不會承諾會執行完。如果在finalize方法中想要拯救自己,只需要將自己連上一個類變量或者對象的成員變量,此時就會存活下來。否則,就會真的被回收了。

如果沒有必要執行的finalize()方法:兩種情況:1、沒有覆蓋finalize()方法 2、該方法已經被執行過了,此時就會直接回收了。

三、垃圾回收算法(關注的是堆和方法區)

標記-清除算法(最基礎的回收算法)

兩個步驟:
1、首先標記出所有的要被回收的對象(就是通過可達性分析找到的要被回收的對象)
2、然後在標記完成之後統一回收所有被標記的對象。

不足:
1、標記和清除的兩個過程的效率都不高(需要暫停應用)
2、標記清除之後會產生大量不連續的內存碎片

複製算法

解決效率問題。
方法:
1、先將能使用的內存劃分爲兩個相等的塊,每次只使用其中的一塊
2、當這塊使用完了,就將所有存活的對象複製到另一塊中,然後把之前這一塊的內存一次清理完

優點:每次都是對整個區域回收,也不用考慮內存碎片的問題,實現簡單,高效
不足:將內存縮小了一半

實際應用:
由於98%的對象都是很快就會死了,所以可以將內存劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,可能回藉助分配擔保機制進入老年代。

標記-整理算法

步驟:
1、首先將可回收的對象進行標記
2、然後將所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

分代收集算法

將Java內存分爲幾個區域,然後針對不同區域進行最適合的方式來收集。
新生代:採用複製算法(因爲存活率低)
老年代:採用標記-清楚或者標記整理算法

四、內存分配和回收的策略

GC常發生的是在堆區,主要分爲新生代和老年代,其中新生代又可以劃分爲Eden和兩個Survivor區。

對象優先在Eden中進行分配。

當Eden的空間不夠的時候就會在Eden區域發生一次Minor GC,因爲大多數對象都朝生夕滅,所以Minor GC發生的次數非常的頻繁,而且效率高。

大對象直接進入老年代。

大對象是指需要大量連續內存空間的Java對象,比如很長的字符串以及數組等。可以通過參數設置進入老年代的對象的大小值。

再就是在Eden中經過N次回收都沒有被回收的,即年齡在N以上的會進入到老年代。

老年代GC(Major GC/Full GC):發生在老年代,一般會伴隨至少一次的Minor GC,速度很慢,比Minor GC慢10倍以上。

長期存活的對象進入老年代

對象年齡計數器:
①、在Eden中對象經歷過第一次Minor GC之後任然存活在Survivor的對象年齡增加1
②、在Survivor中每熬過一次Minor GC的年齡就會增加1歲

當對象年齡增加到一定的時候(默認是15歲),就會進入老年代,這個年齡閾值可以通過參數設置。

五、內存溢出和內存泄露

內存溢出:內存不夠使用
內存泄露:是指申請的內存空間無法釋放。

內存泄露累計起來之後就會導致內存溢出。

內存泄露的原因:
1、長生命週期的對象引用了短生命週期對象
2、沒有將無用的對象設置爲null來提供給GC即時回收

六、類加載器

類與類加載器

對於任意一個類,都需要由加載這個類的類加載器和這個類本身來確立其在JVM中的唯一性。即,即使是同一個class文件,被同意一個虛擬機加載,但是加載它們的類加載器不一樣,這樣兩個類就不一樣。

雙親委派模型

3中系統提供的類加載器:啓動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用程序類加載器(Application ClassLoader)

除了頂層的啓動類加載器之外,其他的類加載器都有自己的父類加載器(這種父子關係是通過組合實現)。

雙親委派模型:當一個類加載器接收到類的加載請求的時候,不會自己進行加載,而是將加載的任務委派給該類加載器的父類加載器,每一層的類加載器都是如此,所以所有的加載請求最終都會傳到頂層的啓動類加載器上進行加載。只有當父加載器無法完成加載任務的時候,子加載器纔會嘗試自己加載。

這就可以使得JAVA的類具有優先層級的關係,比如Object類,在rt.jar中,最終都是有啓動類加載器進行加載,這就保證了Object的唯一。

破壞雙親委派模型

第一次破壞:JDK1.2發佈之前

第二次破壞:基礎類又要回調用戶代碼。比如JNDI服務,JNDI的類由啓動類加載器進行加載,但是在使用JNDI對資源進行集中管理的時候,它有需要回調獨立廠商提供的SPI代碼,此時的啓動類加載器是無法識別這些代碼的。

解決方法:引入線程上下文類加載器(ThreadContext ClassLoader),可以通過setContextClassLoader進行設置,默認是應用程序類加載器

第三次破壞:用戶對程序動態性的追求導致的,如:代碼熱替換、模塊熱部署等。

OSGI(開放服務網關倡議) java模塊化標準。類加載器成爲了一種複雜的網狀結構,而不是雙親委派的樹狀結構,其中每一個程序模塊都有一個自己的類加載器。

七、類加載機制

類加載的過程:加載、驗證、準備、解析、初始化、使用、卸載

其中加載、驗證、準備、初始化和卸載這五個步驟是順序執行的,但是解析和使用不一定。

因爲JAVA語言支持動態綁定,也就是在某些情況下可以進行完初始化之後再進行解析。

類加載的時機

有五種情況下必須進行類的初始化,因此加載、驗證、準備應該要在初始化之前完成。

1、遇到new,getstatic,putstatic,invokestatic這四條字節碼指令的時候,如果類還沒有進行初始化,則需要先出發初始化操作,對應JAVA中的操作就是:①、使用new關鍵字實例化對象②、獲取或者設置靜態字段③、調用一個類的靜態方法
2、通過反射的方式對一個類進行反射調用的時候,如果沒有初始化則會對這個類進行初始化操作。
3、當初始化一個類的時候,發現其父類還沒有被初始化,則會先觸發其父類的初始化操作
4、當虛擬機啓動的時候,用戶需要指定一個主類(即包含了main方法的類),虛擬機會先初始化這個主類
5、在jdk1.7中,使用java.lang.invoke.MethodHandle的時候,將會被解析成REF_getstatic等方法的句柄,並且在這個方法句柄對應的類沒有進行初始化,則會觸發該類進行初始化。

類加載的過程

即加載、驗證、準備、解析和初始化這個五個步驟

加載

1、通過類的全限定名來獲取定義此類的二進制流,獲取的地方可以來自於:ZIP包、網絡、動態代理生成、其他文件、數據庫等。
2、將這個字節流的靜態存儲結構轉化爲運行時方法區運行時數據結構
3、生成相應的Class對象

驗證

主要驗證的信息有:
1、文件格式的驗證:驗證文件是否符合class文件格式的規範
2、元數據驗證:驗證字節碼的描述信息,看是否符合JAVA語義規範
3、字節碼驗證:最複雜的過程,通過控制流和數據流的分析,判斷程序的語義是否符合規範和邏輯
4、符合引用驗證:主要發生在解析階段,對類自身以外的信息進行匹配性驗證

準備

爲類變量分配內存空間,並且給類變量設置初始值(是指零值),此時的類變量是在方法區中進行分配。

注意:是類變量,不是實例變量,實例變量隨着類的實例化將在堆中分配。

解析

一方面是驗證符號引用,另一方面將符號引用轉化爲直接引用。

解析動作主要針對:類或接口的解析、類方法和接口方法的解析、字段解析等。

初始化

在準備階段,變量已經賦值過一次系統要求的初始值,在初始化階段就是真正執行用戶代碼的地方了,即按照類構造器進行初始化。

八、Java內存模型與線程

內存模型

主內存和工作內存

所有的變量(這個變量包含的包括實例、靜態字段等,不是JAVA語言中的變量)都存在主內存中,每個線程還有一個自己的工作內存,工作內存中存放的是這個線程所使用到的主內存中的變量的副本拷貝。線程的每次對變量的操作都是只能在工作內存中進行,而不能直接讀寫主內存中的變量。

對於如何進行主內存和工作內存之間的交互,提供了8條命令進行交互:lock/unlock、read/write、load/store、use/assign

對volatile變量特殊規則

1、保證變量的可見性,也就是一個線程修改了某一個變量的值,這個變量的值對於其他的線程來說是立即可見的。

實現的原理是:在每次使用某個volatile變量之前,先將主內存中該變量的值刷新到當前線程的工作內存中,此時保證的就是使用的這個變量的值就是當前最新的值。然後每次修改完這個變量之後,都會立即將當前線程中的該變量的值刷新回工作內存中,這個就保證了每一次的修改都會立即同步到主內存中。

2、禁止指令重排

指令重排:是指爲了提高執行效率,CPU允許將多條指令不按照程序的順序進行執行,只要處理器能正確處理依賴情況以保證能得出正確的處理結果。(是指在機器級的指令優化)

但是在併發的環境,指令的重新排序可能導致執行的順序有問題,這個時候使用volatile關鍵字禁止指令的重排可以避免。

對於long和double類型的變量的特殊規則

允許虛擬機將沒有用volatile關鍵字修飾的64位數據的讀寫操作劃分爲兩次32位的操作來進行。

先行發生原則

用先行原則來保證兩項操作之間的順序關係,有以下可以直接使用的先序關係:
1、程序的控制流順序
2、管程鎖定順序:即先lock再unlock
3、volatile變量定義的特殊規則
4、傳遞規則(A先行發生於B,B先行發生於C,則A先行發生於C)
。。。

Java線程

Java線程實現的方式:使用內核線程實現、使用用戶線程實現以及使用用戶線程加輕量級進程混合實現

線程的實現

使用內核線程實現

先內核線程完成線程的切換等操作,實現起來比較簡單,但是,需要經常在用戶態和內核態中來回切換,因此係統調用的代價相對較高。

當然應用程序不會直接去使用內核線程,而是使用內核線程的一種高級接口–輕量級進程,這種輕量級進程和線程之間是一對一的關係。

使用用戶線程實現

完全在內核中進行,執行的速度非常快而且低消耗,但是在由於沒有系統內核的支持,所有的線程的切換、調度等都需要用戶自己實現,實現起來會比較複雜。

這中進程和用戶線程之間1:N的關係稱爲一對多線程模型。

使用用戶線程加輕量級進程混合實現

就是將用戶線程和內核線程(輕量級進程)混合使用。

模型就是N和用戶線程對應上M個內核線程(輕量級進程),這樣的一方面可以使用內核提供的線程調度的功能,同時還可以避免頻繁進行用戶態和內核態的切換。

Java線程調度

主要分爲協同調度和搶佔式調度。

協同式

線程運行的時間由線程本身控制,線程把自身執行完之後主動通知下一個線程進行執行。

實現簡單,但是不穩定,易造成系統的崩潰。

搶佔式

爲線程設置優先級,優先級高的可以搶佔優先級低的線程。

但是注意Java中的優先級需要和操作系統中的優先級對應。

線程狀態

5種:創建、可運行、有限等待、無限等待和終止

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章