
作者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉淀、分享、成長,讓自己和他人都能有所識訓!😄
一、前言
學習,不知道從哪下手?
當學習一個新知識不知道從哪下手的時候,最有效的辦法是梳理這個知識結構的脈絡資訊,匯總出一整張的思維匯出,接下來就是按照思維導圖的知識結構,一個個學習相應的知識點,并匯總記錄,
就像JVM的學習,可以說它包括了非常多的內容,也是一個龐大的知識體系,例如:類加載、加載器、生命周期、性能優化、調優引數、調優工具、優化方案、記憶體區域、虛擬機堆疊、直接記憶體、記憶體溢位、元空間、垃圾回收、可達性分析、標記清除、回收程序等等,如果沒有梳理的一頭扎進去,東一榔頭西一棒子,很容易造成學習恐懼感,
如圖 24-1 是 JVM 知識框架梳理,后續我們會按照這個結構陸續講解每一塊內容,

二、面試題
謝飛機,小記!,很多知識根本就是背背背,也沒法操作,難學!
謝飛機:大哥,你問我兩個JVM問題,我看看我自己還行不!
面試官:啊?嗯!往死了問還是?
謝飛機:就就就,都行!你看著來!
面試官:啊,那 JVM 加載程序都是什么步驟?
謝飛機:巴拉巴拉,加載、驗證、準備、決議、初始化、使用、卸載!
面試官:嗯,背的挺好!我懷疑你沒操作過! 那加載的時候,JVM 規范規定從第幾位開始是決議常量池,以及資料型別是如何定義的,u1、u2、u4,是怎么個玩意?
謝飛機:握草!算了,告訴我看啥吧!
三、類加載程序描述

JVM 類加載程序分為,加載、鏈接、初始化、使用和卸載這四個階段,在鏈接中又包括:驗證、準備、決議,
- 加載:Java 虛擬機規范對 class 檔案格式進行了嚴格的規則,但對于從哪里加載 class 檔案,卻非常自由,Java 虛擬機實作可以從檔案系統讀取、從JAR(或ZIP)壓縮包中提取 class 檔案,除此之外也可以通過網路下載、資料庫加載,甚至是運行時直接生成的 class 檔案,
- 鏈接:包括了三個階段;
- 驗證,確保被加載類的正確性,驗證位元組流是否符合 class 檔案規范,例魔數 0xCAFEBABE,以及版本號等,
- 準備,為類的靜態變數分配記憶體并設定變數初始值等
- 決議,決議包括決議出常量池資料和屬性表資訊,這里會包括 ConstantPool 結構體以及 AttributeInfo 介面等,
- 初始化:類加載完成的最后一步就是初始化,目的就是為標記常量值的欄位賦值,以及執行
<clinit>方法的程序,JVM虛擬機通過鎖的方式確保 clinit 僅被執行一次 - 使用:程式代碼執行使用階段,
- 卸載:程式代碼退出、例外、結束等,
四、寫個代碼加載下
JVM 之所以不好掌握,主要是因為不好實操,虛擬機是 C++ 寫的,很多 Java 程式員根本就不會去讀,或者讀不懂,那么,也就沒辦法實實在在的體會到,到底是怎么加載的,加載的時候都干了啥,只有看到代碼,我才覺得自己學會了!
所以,我們這里要手動寫一下,JVM 虛擬機的部分代碼,也就是類加載的程序,通過 Java 代碼來實作 Java 虛擬機的部分功能,讓開發 Java 代碼的程式員更容易理解虛擬機的執行程序,
1. 案例工程
interview-24
├── pom.xml
└── src
└── main
│ └── java
│ └── org.itstack.interview.jvm
│ ├── classpath
│ │ ├── impl
│ │ │ ├── CompositeEntry.java
│ │ │ ├── DirEntry.java
│ │ │ ├── WildcardEntry.java
│ │ │ └── ZipEntry.java
│ │ ├── Classpath.java
│ │ └── Entry.java
│ ├── Cmd.java
│ └── Main.java
└── test
└── java
└── org.itstack.interview.jvm.test
└── HelloWorld.java
以上,工程結構就是按照 JVM 虛擬機規范,使用 Java 代碼實作 JVM 中加載 class 檔案部分內容,當然這部分還不包括決議,因為決議部分的代碼非常龐大,我們先從把 .class 檔案加載讀取開始了解,
2. 代碼講解
2.1 定義類路徑介面(Entry)
public interface Entry {
byte[] readClass(String className) throws IOException;
static Entry create(String path) {
//File.pathSeparator;路徑分隔符(win\linux)
if (path.contains(File.pathSeparator)) {
return new CompositeEntry(path);
}
if (path.endsWith("*")) {
return new WildcardEntry(path);
}
if (path.endsWith(".jar") || path.endsWith(".JAR") ||
path.endsWith(".zip") || path.endsWith(".ZIP")) {
return new ZipEntry(path);
}
return new DirEntry(path);
}
}
- 介面中提供了介面方法
readClass和靜態方法create(String path), - jdk1.8 是可以在介面中撰寫靜態方法的,在設計上屬于補全了抽象類的類似功能,這個靜態方法主要是按照不同的路徑地址型別,提供不同的決議方法,包括:CompositeEntry、WildcardEntry、ZipEntry、DirEntry,這四種,接下來分別看每一種的具體實作
2.2 目錄形式路徑(DirEntry)
public class DirEntry implements Entry {
private Path absolutePath;
public DirEntry(String path){
//獲取絕對路徑
this.absolutePath = Paths.get(path).toAbsolutePath();
}
@Override
public byte[] readClass(String className) throws IOException {
return Files.readAllBytes(absolutePath.resolve(className));
}
@Override
public String toString() {
return this.absolutePath.toString();
}
}
- 目錄形式的通過讀取絕對路徑下的檔案,通過
Files.readAllBytes方式獲取位元組碼,
2.3 壓縮包形式路徑(ZipEntry)
public class ZipEntry implements Entry {
private Path absolutePath;
public ZipEntry(String path) {
//獲取絕對路徑
this.absolutePath = Paths.get(path).toAbsolutePath();
}
@Override
public byte[] readClass(String className) throws IOException {
try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
return Files.readAllBytes(zipFs.getPath(className));
}
}
@Override
public String toString() {
return this.absolutePath.toString();
}
}
- 其實壓縮包形式與目錄形式,只有在檔案讀取上有包裝差別而已,
FileSystems.newFileSystem
2.4 混合形式路徑(CompositeEntry)
public class CompositeEntry implements Entry {
private final List<Entry> entryList = new ArrayList<>();
public CompositeEntry(String pathList) {
String[] paths = pathList.split(File.pathSeparator);
for (String path : paths) {
entryList.add(Entry.create(path));
}
}
@Override
public byte[] readClass(String className) throws IOException {
for (Entry entry : entryList) {
try {
return entry.readClass(className);
} catch (Exception ignored) {
//ignored
}
}
throw new IOException("class not found " + className);
}
@Override
public String toString() {
String[] strs = new String[entryList.size()];
for (int i = 0; i < entryList.size(); i++) {
strs[i] = entryList.get(i).toString();
}
return String.join(File.pathSeparator, strs);
}
}
File.pathSeparator,是一個分隔符屬性,win/linux 有不同的型別,所以使用這個方法進行分割路徑,- 分割后的路徑裝到 List 集合中,這個程序屬于拆分路徑,
2.5 通配符型別路徑(WildcardEntry)
public class WildcardEntry extends CompositeEntry {
public WildcardEntry(String path) {
super(toPathList(path));
}
private static String toPathList(String wildcardPath) {
String baseDir = wildcardPath.replace("*", ""); // remove *
try {
return Files.walk(Paths.get(baseDir))
.filter(Files::isRegularFile)
.map(Path::toString)
.filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
.collect(Collectors.joining(File.pathSeparator));
} catch (IOException e) {
return "";
}
}
}
- 這個類屬于混合形式路徑處理類的子類,唯一提供的方法就是把類路徑決議出來,
2.6 類路徑決議(Classpath)
啟動類路徑、擴展類路徑、用戶類路徑,熟悉嗎?是不經常看到這幾句話,那么時候怎么實作的呢?
有了上面我們做的一些基礎類的作業,接下來就是類決議的實際呼叫程序,代碼如下:
public class Classpath {
private Entry bootstrapClasspath; //啟動類路徑
private Entry extensionClasspath; //擴展類路徑
private Entry userClasspath; //用戶類路徑
public Classpath(String jreOption, String cpOption) {
//啟動類&擴展類 "C:\Program Files\Java\jdk1.8.0_161\jre"
bootstrapAndExtensionClasspath(jreOption);
//用戶類 F:\..\org\itstack\demo\test\HelloWorld
parseUserClasspath(cpOption);
}
private void bootstrapAndExtensionClasspath(String jreOption) {
String jreDir = getJreDir(jreOption);
//..jre/lib/*
String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
bootstrapClasspath = new WildcardEntry(jreLibPath);
//..jre/lib/ext/*
String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
extensionClasspath = new WildcardEntry(jreExtPath);
}
private static String getJreDir(String jreOption) {
if (jreOption != null && Files.exists(Paths.get(jreOption))) {
return jreOption;
}
if (Files.exists(Paths.get("./jre"))) {
return "./jre";
}
String jh = System.getenv("JAVA_HOME");
if (jh != null) {
return Paths.get(jh, "jre").toString();
}
throw new RuntimeException("Can not find JRE folder!");
}
private void parseUserClasspath(String cpOption) {
if (cpOption == null) {
cpOption = ".";
}
userClasspath = Entry.create(cpOption);
}
public byte[] readClass(String className) throws Exception {
className = className + ".class";
//[readClass]啟動類路徑
try {
return bootstrapClasspath.readClass(className);
} catch (Exception ignored) {
//ignored
}
//[readClass]擴展類路徑
try {
return extensionClasspath.readClass(className);
} catch (Exception ignored) {
//ignored
}
//[readClass]用戶類路徑
return userClasspath.readClass(className);
}
}
- 啟動類路徑,bootstrapClasspath.readClass(className);
- 擴展類路徑,extensionClasspath.readClass(className);
- 用戶類路徑,userClasspath.readClass(className);
- 這回就看到它們具體在哪使用了吧!有了具體的代碼也就方便理解了
2.7 加載類測驗驗證
private static void startJVM(Cmd cmd) {
Classpath cp = new Classpath(cmd.jre, cmd.classpath);
System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
//獲取className
String className = cmd.getMainClass().replace(".", "/");
try {
byte[] classData = cp.readClass(className);
System.out.println(Arrays.toString(classData));
} catch (Exception e) {
System.out.println("Could not find or load main class " + cmd.getMainClass());
e.printStackTrace();
}
}
這段就是使用 Classpath 類進行類路徑加載,這里我們測驗加載 java.lang.String 類,你可以加載其他的類,或者自己寫的類
- 配置IDEA,program arguments 引數:
-Xjre "C:\Program Files\Java\jdk1.8.0_161\jre" java.lang.String - 另外這里讀取出的 class 檔案資訊,列印的是 byte 型別資訊,
測驗結果
[-54, -2, -70, -66, 0, 0, 0, 52, 2, 28, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0, 15, 8, 0, 61, 8, 0, 85, 8, 0, 88, 8, 0, 89, 8, 0, 112, 8, 0, -81, 8, 0, -75, 8, 0, -47, 8, 0, -45, 1, 0, 0, 1, 0, 3, 40, 41, 73, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3, 40, 41, 90, 1, 0, 4, 40, 41, 91, ...]
這塊部分截取的程式運行列印結果,就是讀取的 class 檔案資訊,只不過暫時還不能看出什么,接下來我們再把它翻譯過來!
五、決議位元組碼檔案
JVM 在把 class 檔案加載完成后,接下來就進入鏈接的程序,這個程序包括了內容的校驗、準備和決議,其實就是把 byte 型別 class 翻譯過來,做相應的操作,
整個這個程序內容相對較多,這里只做部分邏輯的實作和講解,如果讀者感興趣可以閱讀小傅哥的《用Java實作JVM》專欄,
1. 提取部分位元組碼
//取部分位元組碼:java.lang.String
private static byte[] classData = {
-54, -2, -70, -66, 0, 0, 0, 52, 2, 26, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0,
59, 8, 0, 83, 8, 0, 86, 8, 0, 87, 8, 0, 110, 8, 0, -83, 8, 0, -77, 8, 0, -49, 8, 0, -47, 1, 0, 3, 40, 41, 73, 1,
0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41,
76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3,
40, 41, 90, 1, 0, 4, 40, 41, 91, 66, 1, 0, 4, 40, 41, 91, 67, 1, 0, 4, 40, 67, 41, 67, 1, 0, 21, 40, 68, 41, 76,
106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 40, 73, 41, 67, 1, 0, 4};
- java.lang.String 決議出來的位元組碼內容較多,當然包括的內容也多,比如魔數、版本、類、常量、方法等等,所以我們這里只截取部分進行進行決議,
2. 決議魔數并校驗
很多檔案格式都會規定滿足該格式的檔案必須以某幾個固定位元組開頭,這幾個位元組主要起到標識作用,叫作魔數(magic number),
例如;
- PDF檔案以4位元組“%PDF”(0x25、0x50、0x44、0x46)開頭,
- ZIP檔案以2位元組“PK”(0x50、0x4B)開頭
- class檔案以4位元組“0xCAFEBABE”開頭
private static void readAndCheckMagic() {
System.out.println("\r\n------------ 校驗魔數 ------------");
//從class位元組碼中讀取前四位
byte[] magic_byte = new byte[4];
System.arraycopy(classData, 0, magic_byte, 0, 4);
//將4位byte位元組轉成16進制字串
String magic_hex_str = new BigInteger(1, magic_byte).toString(16);
System.out.println("magic_hex_str:" + magic_hex_str);
//byte_magic_str 是16進制的字串,cafebabe,因為java中沒有無符號整型,所以如果想要無符號只能放到更高位中
long magic_unsigned_int32 = Long.parseLong(magic_hex_str, 16);
System.out.println("magic_unsigned_int32:" + magic_unsigned_int32);
//魔數比對,一種通過字串比對,另外一種使用假設的無符號16進制比較,如果使用無符號比較需要將0xCAFEBABE & 0x0FFFFFFFFL與運算
System.out.println("0xCAFEBABE & 0x0FFFFFFFFL:" + (0xCAFEBABE & 0x0FFFFFFFFL));
if (magic_unsigned_int32 == (0xCAFEBABE & 0x0FFFFFFFFL)) {
System.out.println("class位元組碼魔數無符號16進制數值一致校驗通過");
} else {
System.out.println("class位元組碼魔數無符號16進制數值一致校驗拒絕");
}
}
- 讀取位元組碼中的前四位,
-54, -2, -70, -66,將這四位轉換為16進制, - 因為 java 中是沒有無符號整型的,所以只能用更高位存放,
- 決議后就是魔數的對比,看是否與 CAFEBABE 一致,
測驗結果
------------ 校驗魔數 ------------
magic_hex_str:cafebabe
magic_unsigned_int32:3405691582
0xCAFEBABE & 0x0FFFFFFFFL:3405691582
class位元組碼魔數無符號16進制數值一致校驗通過
3. 決議版本號資訊
剛才我們已經讀取了4位魔數資訊,接下來再讀取2位,是版本資訊,
魔數之后是class檔案的次版本號和主版本號,都是u2型別,假設某class檔案的主版本號是M,次版本號是m,那么完整的版本號可以表示成“M.m”的形式,次版本號只在J2SE 1.2之前用過,從1.2開始基本上就沒有什么用了(都是0),主版本號在J2SE 1.2之前是45,從1.2開始,每次有大版本的Java版本發布,都會加1{45、46、47、48、49、50、51、52}
private static void readAndCheckVersion() {
System.out.println("\r\n------------ 校驗版本號 ------------");
//從class位元組碼第4位開始讀取,讀取2位
byte[] minor_byte = new byte[2];
System.arraycopy(classData, 4, minor_byte, 0, 2);
//將2位byte位元組轉成16進制字串
String minor_hex_str = new BigInteger(1, minor_byte).toString(16);
System.out.println("minor_hex_str:" + minor_hex_str);
//minor_unsigned_int32 轉成無符號16進制
int minor_unsigned_int32 = Integer.parseInt(minor_hex_str, 16);
System.out.println("minor_unsigned_int32:" + minor_unsigned_int32);
//從class位元組碼第6位開始讀取,讀取2位
byte[] major_byte = new byte[2];
System.arraycopy(classData, 6, major_byte, 0, 2);
//將2位byte位元組轉成16進制字串
String major_hex_str = new BigInteger(1, major_byte).toString(16);
System.out.println("major_hex_str:" + major_hex_str);
//major_unsigned_int32 轉成無符號16進制
int major_unsigned_int32 = Integer.parseInt(major_hex_str, 16);
System.out.println("major_unsigned_int32:" + major_unsigned_int32);
System.out.println("版本號:" + major_unsigned_int32 + "." + minor_unsigned_int32);
}
- 這里有一個小技巧,class 檔案決議出來是一整片的內容,JVM 需要按照虛擬機規范,一段一段的決議出所有的資訊,
- 同樣這里我們需要把2位byte轉換為16進制資訊,并繼續從第6位繼續讀取2位資訊,組合出來的才是版本資訊,
測驗結果
------------ 校驗版本號 ------------
minor_hex_str:0
minor_unsigned_int32:0
major_hex_str:34
major_unsigned_int32:52
版本號:52.0
4. 決議全部內容對照
按照 JVM 的加載程序,其實遠不止魔數和版本號資訊,還有很多其他內容,這里我們可以把測驗結果展示出來,方便大家有一個學習結果的比對印象,
classpath:org.itstack.demo.jvm.classpath.Classpath@4bf558aa class:java.lang.String args:null
version: 52.0
constants count:540
access flags:0x31
this class:java/lang/String
super class:java/lang/Object
interfaces:[java/io/Serializable, java/lang/Comparable, java/lang/CharSequence]
fields count:5
value [C
hash I
serialVersionUID J
serialPersistentFields [Ljava/io/ObjectStreamField;
CASE_INSENSITIVE_ORDER Ljava/util/Comparator;
methods count: 94
<init> ()V
<init> (Ljava/lang/String;)V
<init> ([C)V
<init> ([CII)V
<init> ([III)V
<init> ([BIII)V
<init> ([BI)V
checkBounds ([BII)V
<init> ([BIILjava/lang/String;)V
<init> ([BIILjava/nio/charset/Charset;)V
<init> ([BLjava/lang/String;)V
<init> ([BLjava/nio/charset/Charset;)V
<init> ([BII)V
<init> ([B)V
<init> (Ljava/lang/StringBuffer;)V
<init> (Ljava/lang/StringBuilder;)V
<init> ([CZ)V
length ()I
isEmpty ()Z
charAt (I)C
codePointAt (I)I
codePointBefore (I)I
codePointCount (II)I
offsetByCodePoints (II)I
getChars ([CI)V
getChars (II[CI)V
getBytes (II[BI)V
getBytes (Ljava/lang/String;)[B
getBytes (Ljava/nio/charset/Charset;)[B
getBytes ()[B
equals (Ljava/lang/Object;)Z
contentEquals (Ljava/lang/StringBuffer;)Z
nonSyncContentEquals (Ljava/lang/AbstractStringBuilder;)Z
contentEquals (Ljava/lang/CharSequence;)Z
equalsIgnoreCase (Ljava/lang/String;)Z
compareTo (Ljava/lang/String;)I
compareToIgnoreCase (Ljava/lang/String;)I
regionMatches (ILjava/lang/String;II)Z
regionMatches (ZILjava/lang/String;II)Z
startsWith (Ljava/lang/String;I)Z
startsWith (Ljava/lang/String;)Z
endsWith (Ljava/lang/String;)Z
hashCode ()I
indexOf (I)I
indexOf (II)I
indexOfSupplementary (II)I
lastIndexOf (I)I
lastIndexOf (II)I
lastIndexOfSupplementary (II)I
indexOf (Ljava/lang/String;)I
indexOf (Ljava/lang/String;I)I
indexOf ([CIILjava/lang/String;I)I
indexOf ([CII[CIII)I
lastIndexOf (Ljava/lang/String;)I
lastIndexOf (Ljava/lang/String;I)I
lastIndexOf ([CIILjava/lang/String;I)I
lastIndexOf ([CII[CIII)I
substring (I)Ljava/lang/String;
substring (II)Ljava/lang/String;
subSequence (II)Ljava/lang/CharSequence;
concat (Ljava/lang/String;)Ljava/lang/String;
replace (CC)Ljava/lang/String;
matches (Ljava/lang/String;)Z
contains (Ljava/lang/CharSequence;)Z
replaceFirst (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
replaceAll (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
replace (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
split (Ljava/lang/String;I)[Ljava/lang/String;
split (Ljava/lang/String;)[Ljava/lang/String;
join (Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String;
join (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;
toLowerCase (Ljava/util/Locale;)Ljava/lang/String;
toLowerCase ()Ljava/lang/String;
toUpperCase (Ljava/util/Locale;)Ljava/lang/String;
toUpperCase ()Ljava/lang/String;
trim ()Ljava/lang/String;
toString ()Ljava/lang/String;
toCharArray ()[C
format (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
format (Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
valueOf (Ljava/lang/Object;)Ljava/lang/String;
valueOf ([C)Ljava/lang/String;
valueOf ([CII)Ljava/lang/String;
copyValueOf ([CII)Ljava/lang/String;
copyValueOf ([C)Ljava/lang/String;
valueOf (Z)Ljava/lang/String;
valueOf (C)Ljava/lang/String;
valueOf (I)Ljava/lang/String;
valueOf (J)Ljava/lang/String;
valueOf (F)Ljava/lang/String;
valueOf (D)Ljava/lang/String;
intern ()Ljava/lang/String;
compareTo (Ljava/lang/Object;)I
<clinit> ()V
Process finished with exit code 0
- 如果大家對這部分驗證、準備、決議,的實作程序感興趣,可以參照這部分用Java實作的JVM原始碼:https://github.com/fuzhengwei/itstack-demo-jvm
六、總結
- 學習 JVM 最大的問題是不好實踐,所以本文以案例實操的方式,學習 JVM 的加載決議程序,也讓更多的對 JVM 感興趣的研發,能更好的接觸到 JVM 并深入的學習,
- 有了以上這段代碼,大家可以參照 JVM 虛擬機規范,在除錯Java版本的JVM,這樣就可以非常容易理解整個JVM的加載程序,都做了什么,
- 如果大家需要文章中一些原圖 xmind 或者原始碼,可以添加作者小傅哥(fustack),或者關注公眾號:bugstack蟲洞堆疊進行獲取,好了,本章節就扯到這,后續還有很多努力,持續原創,感謝大家的支持!
七、系列推薦
- ReentrantLock之公平鎖講解和實作
- 除了JDK、CGLIB,還有3種類代理方式?面試又卡住!
- 面試官,ThreadLocal 你要這么問,我就掛了!
- 手寫執行緒池,對照學習ThreadPoolExecutor執行緒池實作原理!
- 一次代碼評審,差點過不了試用期!
CSDN認證博客專家
ASM
設計模式
面經手冊
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/243573.html
標籤:java
上一篇:初識JVM記憶體區域的劃分
下一篇:numpy.where()函式
