1、運行時數據區域
-
Java 堆
在虛擬機啓動時創建,存儲對象實例和數組。是垃圾回收的主要區域,一般分爲 OLD、YOUNG(Eden,S0,S1)區域,進一步的劃分都是爲了更好的回收內存。邏輯上是連續的,但是可以處在物理上不連續的內存空間中。對象的組成包括對象頭、實例數據、對齊填充(不滿8的整數倍字節進行填充)。
- Java對象佈局中有鎖相關的信息,也就是說synchronized關鍵字實際上鎖的是對象。對象頭中運行時數據的大小爲JVM的一個WORD大小,即32位JVM的爲32位,64位JVM爲64位,爲了存儲更多的信息,最後的lock兩位爲標誌位,分別表示無鎖、偏向鎖、輕量級鎖、重量級鎖、GC標記。其中lock和biased_lock一起表示是否有鎖。age字段爲4位,最大值爲16,也就是minor GC的時候,S0和S1循環累加的最大值,超過就會進入到老年代。
- 無鎖情況:存儲25位的對象hashcode碼,調用了hashcode方法進行計算之後纔會存儲進去;偏向鎖情況:thread爲持有偏向鎖的線程ID,epoch爲偏向時間戳;輕量級鎖情況:指向棧中鎖記錄的指針;重量級鎖:指向管程Monitor的指針,也就是常用的synchronized鎖。
- synchronized加鎖的方式:同步實例方法,鎖的是對象實例;同步類方法,鎖的是class實;同步代碼塊,鎖的是括號裏面的對象,鎖住的是所有以該對象爲鎖的代碼塊;相比JUC包中的代碼,區別在於synchronized控制的代碼在沒有多線程的情況下也無法解除,造成性能低下,所以後期synchronized進行了升級,也就是鎖膨脹。
- 1.5之前使用操作系統進行加鎖,synchronized核心組件:
1、waitSet
調用wait方法後被阻塞的線程存放的位置
2、contentionList
競爭隊列,所有請求鎖的線程首先被放置到這裏
3、EntryList
contentionList中有資格成爲候選者的線程被放置到這裏
4、OnDeck
任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被設置爲OnDeck
5、Owner
當前已經獲取到鎖資源的線程
6、!Owner
當前釋放鎖的線程
//執行過程
JVM每次從隊列的尾部,取出一個線程作爲OnDeck,
過程:線程訪問代碼塊的時候,如果當前鎖處於重量級鎖,直接掛起線程;如果處於輕量級鎖,CAS嘗試修改鎖記錄的指針,失敗的話自旋幾次,如果自旋幾次都沒有成功,那麼當前鎖升級爲重量級鎖,當前線程掛起;如果處於偏向鎖,而且不是自己,說明多個線程加入了競爭,那麼開始撤銷偏向鎖,等待原持有偏向鎖的線程到達安全點,然後暫停原持有偏向鎖的線程,如果此時原線程沒有退出,則升級爲輕量級鎖,喚醒原有暫停的線程從安全點執行。
說明:偏向鎖主要是針對一個線程的,表示不需要進行同步操作,只需要簡單的判斷偏向鎖的標誌。此時如果有兩個線程來競爭,就會升級爲輕量級鎖了。
1、依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2、類測試
package com.vim.modules.web.controller;
import org.openjdk.jol.info.ClassLayout;
public class Test {
private boolean flag = false;
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Test()).toPrintable());
}
}
3、測試結果
com.vim.modules.web.controller.Test object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 9f 20 ed 27 (10011111 00100000 11101101 00100111) (669851807)
12 1 boolean Test.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
4、分析結果
8個字節的對象頭+4個字節的類元空間指針+1個字節的實例數據+3個字節的8倍填充
public Test{
public static void main(String[] args){
//test變量存儲在main棧幀的局部變量表中,指向的對象存儲在堆中
Test test = new Test();
//和test一樣,指向的是同一個類元信息
Test test2 = new Test();
}
}
-
方法區,也叫元空間
存儲已被虛擬機加載的類信息、常量、靜態變量,也常被稱爲永久代,也可以理解爲class文件在內存中的存放位置。是在類加載的時候,通過類的全限定名稱,去讀取類的二進制字節流(各種加載方式),將其轉化爲方法區的運行時數據結構存儲起來。
1)Class文件常量池,屬於非運行常量池,在編譯階段就已經確定。主要有兩種:字面量和符號引用量,如class類全限定名、方法名。在執行的時候會解析這些符號,轉化爲符號對應的直接引用,也就是指令碼,而運行時常量池,是jvm虛擬機在完成類裝載操作後,將class文件中的常量池載入到內存中,並保存在方法區中
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
解析:
1、s1 == s2,是由於在編譯期間,Hello字面量會直接放入class文件的常量池中,從而實現複用,載入運行時常量池後,s1、s2指向的是同一個內存地址,所以相等。
2、s1 == s3,在編譯期間進行了優化合並,所以也成立。
3、s1 == s4,在運行期間才能確定
4、s1 == s9,由於s9是兩個變量拼接而成,但是編譯期間無法確定,不能做優化。
5、s4 == s5,兩者都在堆中
6、s1 == s6,s5在堆中,內容爲Hello ,intern方法會嘗試將Hello字符串添加到常量池中,並返回其在常量池中的地址,因爲常量池中已經有了Hello字符串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等
-
本地方法棧
爲虛擬機使用到的 Native 方法服務 -
程序計數器 (PC register)
根據這個計數器的值來選取下一條需要執行的字節碼的指令,由於在任何一個確定的時刻,一個處理器都只有一個線程在工作,所以爲了在線程切換後能夠恢復到確定的位置。 -
虛擬機棧(Stacks)
每一個線程對應一個虛擬機棧,生命週期和線程相同,描述的是Java方法執行的內存模型。每一個方法執行時,會創建一個棧幀,壓入到棧中,執行完成後會出棧。
棧幀在執行時,會存在局部變量表、操作數棧、動態鏈接、方法出口等信息,其中局部變量表中存放了編譯器內可知的各種數據類型,包括對象引用的內存地址。局部變量表所需要的空間,在編譯期間就已經分配確定完畢,在執行方法的時候,已經將方法下面的一條指令地址存儲到方法出口中,以便執行返回,動態鏈接中放置的是對應的方法在類元信息中的地址。以下實例演示: javap -c Test.class 反編譯
public class Test {
public static int test(){
int a=3;
int b=4;
int c = a + b;
return c;
}
public static void main(String[] args){
test();
}
}
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public static int test();
Code:
0: iconst_3 //將常量2壓入棧中
1: istore_0 //將棧中的常量2取出,放入到局部變量表0位置
2: iconst_4 //將常量4壓入棧中
3: istore_1 //將棧中的常量4取出,放入到局部變量表1位置
4: iload_0 //將局部變量表中0位置取出,放入棧中
5: iload_1 //將局部變量表中1位置取出,放入棧中
6: iadd //將棧頂兩int型數值相加並將結果壓入棧頂
7: istore_2 //將棧中的結果取出,放入到局部變量表2位置
8: iload_2 //將局部變量表中2位置取出,放入棧中
9: ireturn //返回結果
//備註,istore 放到局部變量表 和 iload 從局部變量表取出
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method test:()I
3: pop
4: return
}
2、對象創建過程
-
創建的幾種方式
package com.vim.modules.web.controller;
import com.vim.modules.web.model.User;
public class Test {
public static void main(String[] args) throws Exception{
//使用new關鍵字
User user1 = new User();
//使用Class類的newInstance方法
User user2 = (User)Class.forName("com.vim.modules.web.model.User").newInstance();
User user3 = User.class.newInstance();
//使用Constructor類的newInstance方法
User user4 = User.class.getConstructor().newInstance();
}
}
-
初始化的觸發條件
new 實例化對象、讀取或設置一個類的靜態字段、調用類的靜態方法、反射,這幾種情況,如果類沒有進行初始化,會出發初始化的操作;當父類沒有初始化時,先初始化父類; -
非觸發條件情況
讀取類的靜態字段,該字段被final修飾;通過數組定義來引用類;
3、垃圾回收機制
- 垃圾回收的區域主要是在堆,分爲老年代、新生代(Eden、S0、S1),S0和S1屬於Survivor區域,新創建的對象都會放置到Eden區域,但是該區域最終會滿,會觸發minor GC,會將該區域無效的對象進行垃圾收集。會將Eden部分存活的對象放置到S0區域,S0滿了,會將存活的放置到S1中,S0和S1之間循環的放置會不斷的加年齡,到達了一定的次數,就會被移動到老年代。當老年代放置滿了,就會觸發full GC,通過jvisualvm命令可以看到動態的分配過程。
- 對象無用的條件是沒有指針指向這些對象。可達性分析算法:通過一系列“GC root”的對象作爲起點,從這些節點開始向下搜索,節點走過的路徑稱爲引用鏈,當一個對象到GC root沒有任何引用鏈的話,則證明此對象是不可用的。還有一種方式是引用計數法,爲每一個對象創建一個引用計數,引用時加1,釋放引用時減1,到達0時可以被回收,但是會存在循環引用的問題。
- 垃圾回收算法:標記清除,標記無用對象,然後進行清除回收,效率不高,無法解決碎片問題;標記整理,標記無用對象,讓所有的存活對象往一邊移動,然後清除邊界外的內存;複製算法,劃分兩個相同大小的區域,當一塊用完的時候,將獲得對象複製到另一塊上,然後把已使用的空間一次性的清理掉。
標記清除
標記整理
- 回收算法
4、雙親委派模型
- 如果一個類加載器收到了類加載的請求,它自己並不會去嘗試加載這個類,而是交由自己的父類加載器去加載,一直遞歸到頂層。只有父類無法加載到該類時,自己纔會去嘗試加載這個類。
package com.vim.modules.web.controller;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception{
//啓動類加載器,sun.boot.class.path
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//擴展類加載器,java.ext.dirs
urls = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
//應用類加載器
urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
for(URL url : urls){
System.out.println(url);
}
}
}
- 類加載階段分爲加載、連接、初始化過程,而加載階段,需要通過類的全限定名去獲取類的二進制字節流。對於任何一個類,都需要由它的類加載器和這個類本身一同確立在JVM中的唯一性。每一個類加載器都有一個獨立的類名稱空間。
- 雙親委派的好處是,使類有了層次劃分,父類已經加載的類,子類就不需要加載了,同時java核心庫中定義的class不會被任意替換,保證了安全性。
- 自定義類加載器
package com.vim.modules.web.controller;
public class CustomClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
//加載class字節流
return null;
}
}
- 破壞雙親委派模式,原因是有時候需要使用當前類加載器的子類加載器去加載,但是雙親委派模式單向的,可以使用Thread類的setContextClassLoader方法去設置上下文類加載器,沒有設置的話默認是系統類加載器。