本文主要內容
- JVM組成
- 垃圾回收機制簡介
本文不涉及的內容
- 編譯技術
- JVM中class文件結構
- JVM線程同步機制
- JVM指令集構成
- 類加載流程及其生命周期
- 對象實例化過程
- 對象在堆中的表示方法
這些以后慢慢填吧
JVM與我們的關系
廣義上來講,JVM指的是一種設計規范
從圖中我們可以看到,Java編譯器將.java文件編譯成class文件,交給JVM解析和執行。只要滿足這個要求的我們都可以稱其為Java虛擬機
JVM構成
JVM整體結構可以由下圖展示
其中,當JVM加載類文件時,就會將類的相關信息放在方法區(method area)中,程序執行過程中所有對象會放在堆中
Class loader subsystem
整個class loader system大致如下圖
Class loader分為兩類
- Primordial class loader 這個與JVM實現有關,有且僅有一個
- class loader 對象,不同的classloaders加載的類會放在不同的命名空間中(不同的 classloader 加載同一個類不會被 JVM 認為是同一個類
class loader 的對象和 Class 的對象都在堆中,加載的類型的數據在方法區。
每一個 class loader對象維護一個自己的命名空間,因此一個完整的類名是不足以證明該類的唯一性的-->熱更新的手段之一。
class loader的設計是java實現動態加載類的基礎-->安卓插件化技術
Method Area
在JVM中,加載的類型(類)是存放在方法區的。所有線程都共享方法區
JVM對于每種類型都會存儲一系列的信息:
- 類的全名
- 類的直接父類(除非是接口或java.lang.Object)
- 是類還是接口
- 類的標識符(public, abstract, final)
- 實現的接口的list
- 類的常量池(constant pool)
- 成員變量信息
- 方法信息
- 類變量信息(Class Variables)
- Classloader 引用(加載此類的class loader)
- 一個Class類實例引用
類的常量池是一個有序表,持有所有的常量(字符串,整數,浮點數),以及其他類,作用域,方法的符號引用。是Java程序實現動態鏈接的關鍵部分
方法信息存儲了方法名,方法返回的類型,參數表,方法的修飾符(public, private, protected, static, final, synchronized, native, abstract)
如果方法不是抽象方法,那么還會額外存儲方法的字節碼,異常表,操作數棧大小和本地變量
類變量(Class Variables) 就是存儲了和類相關的變量(static),但有final修飾時情況不同,final static 修飾的變量同時會拷貝一份存儲在任何其他使用它的類的數據里,并且是在編譯期完成的復制(compile time vs runtime)
當然很多JVM在方法區會加入其他的數據結構來加快執行速度
下面舉一個例子:
class Lava {
private int speed = 5;
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面展示的是JVM一種處理方法區的手段。
首先,你將Volcano
這個名字告訴給JVM,JVM找到這個名字對應的類文件Volcano.class
,JVM讀取這個類然后將信息加載到方法區。虛擬機之后調用main函數,即解釋方法區中main方法的字節碼。此時Lava類還沒有被加載,于此同時,虛擬機會維護一個指針指向當前的類常量池(Volcano)
main方法中首先請求新建一個Lava實例,JVM會從Volcano這個類的常量池的第一個元素開始遍歷,找到一個指向Lava
這個類的符號引用。此時JVM會檢查Lava
這個類的信息有沒有加載進方法區。顯然Lava類還沒有加載進來,因此它會去尋找并讀取Lava.class
,將信息提取到方法區中。
之后,JVM會將剛才Volcano
中常量池的符號引用,字符串"Lava",替換為一個指向Lava類信息的指針。那么之后JVM如果再次碰到了使用Lava的情況,就會直接使用這個指針去訪問Lava類的信息。這個過程被稱為constant pool resolution.
直到這里,JVM才開始為新的Lava
對象分配內存。這里剛才指向Volcano類常量池的指針就會指向Lava類常量池上,用來計算一個Lava對象需要的空間,JVM總是能夠在根據方法區中一個類的信息判斷其對象需要的空間
一旦創建好了對象,虛擬機就會對對象中的屬性(speed)設置初始值(包括父類),之后在棧幀上壓入一個該對象的引用(lava),之后的方法調用就是利用這個引用去調用初始化方法(5),之后的flow()方法調用和上面的流程類似
PC & Stack & Stack Frame
JVM是一個棧虛擬機,它并不使用寄存器保存中間值,而是使用棧幀來保存。每當創建一個新的線程,該線程就會擁有一個自己的PC計數器和Java棧,PC用來指示下一條待執行的指令
Java棧是由棧幀(Stack Frame)構成的,每當線程調用一個方法,JVM就會將一個棧幀壓棧,當方法返回后,這個棧幀彈出。所有在棧里的數據都只對持有該棧的線程
棧幀由三個部分組成:
- 本地變量
- 操作數棧
- 棧幀數據
本地變量包含一個方法的參數和本地變量,例如:
class Example3a {
public static int runClassMethod(int i, long l, float f, double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s, boolean b) {
return 0;
}
}
這段程序的棧幀中本地變量如圖所示
注意下runInstanceMethod方法中第一個變量是一個reference類型,其實這就是一個this
引用,對每個實例方法都會隱式地傳入這個引用
最后,所有對象在Java中都是引用傳遞,對象本體存在于堆中,你絕不可能在本地變量,操作數棧中找到真實的對象,你只能拿到對象的引用
操作數棧也是一個棧,與本地變量采用下標訪問不同,操作數棧只有出棧入棧兩種操作
比方說下面幾個指令:
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
的執行過程如下圖所示
棧幀數據(Frame Data)包括了對constant pool resolution
,方法正常返回,異常處理的支持
JVM中有大量的指令集是和常量池相關的,凡是這樣的指令虛擬機都是使用棧幀數據中的指針去獲取對應的信息,之前提到過這些剛開始都是符號,當遇到符號時就會開始執行constant pool resolution
棧幀會持有對方法異常表的引用
圖中同時還出現了一個本地方法棧,JVM并沒有規定本地方法棧中的狀態表示。但是可以看到它是獨立于Java棧幀的。
堆
所有線程共享一個堆。沒有任何辦法在兩個JVM實例之間共享堆,下面是一個值得大家思考的問題
如何實現安卓進程間通信呢?(當然我想有很多人知道是如何實現的,不過這也是思考安卓系統架構緣由的好方式)
垃圾回收算法
現在的回收算法仍舊需要做兩件事情:檢測所有需要回收的對象;騰出對象空間并提供給程序使用
檢測需要定義一組根節點和可達性這個概念,檢測引用則有引用計數和追蹤兩種方式
現在的回收機制是基于Copying Collectors
發展而來的Generational Collectors
Copying Collectors
將所有存活的對象移動到一個新的區域,常見的算法是”stop and copy”算法,堆會被分為兩個區域。只有一塊區域在特定時刻才會被使用,這塊區域會一直被使用直到空間耗盡,此時程序會停止運行,然后堆進行遍歷,存活對象會被拷貝到另一片區域,之后程序恢復執行。如此循環反復
Generational Collectors(當代回收算法)
簡單的stop and copy算法面臨的問題是所有的存活對象每次回收都需要拷貝,改進這種算法的方式基于如下兩個事實:
1.絕大部分對象只有很短的生命周期
2.有些對象會有很長的生命周期
Generational Collectors將對象按照年齡分類同時相比老的對象更頻繁的回收新對象,這種方式中,堆被分成兩個及以上的區域,每個區域代表“一代”對象,最年輕的一代回收的最為頻繁,一旦某個對象經過幾次回收后仍舊沒有被回收,那么它就會變老進入下一代(進入另一個堆塊中),當然現在的回收算法遠比這個復雜,會有更多的堆劃分,更優秀的并發GC支持。比方說這樣:
最后留給大家一個小問題思考下吧
為什么Android自定義View中onDraw方法不推薦創建對象呢?
最后附上例子中Volcano
類和Lava
類的常量池和棧幀
aLIEzTeddeMacBook-Pro:~ JianGuo$ java -version
java version "1.8.0_77"
Java(TM) SE Runtime Environment (build 1.8.0_77-b03)
Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode)
aLIEzTeddeMacBook-Pro:java JianGuo$ javap -v -verbose Volcano.class
Classfile /Users/JianGuo/IdeaProjects/HelloMac/src/main/java/Volcano.class
Last modified Dec 1, 2016; size 311 bytes
MD5 checksum 1083bc1bf517460255a98c86e6290a69
Compiled from "Volcano.java"
public class Volcano
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Lava
#3 = Methodref #2.#15 // Lava."<init>":()V
#4 = Methodref #2.#17 // Lava.flow:()V
#5 = Class #18 // Volcano
#6 = Class #19 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Volcano.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 Lava
#17 = NameAndType #20:#8 // flow:()V
#18 = Utf8 Volcano
#19 = Utf8 java/lang/Object
#20 = Utf8 flow
{
public Volcano();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Lava
3: dup
4: invokespecial #3 // Method Lava."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Lava.flow:()V
12: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 12
}
SourceFile: "Volcano.java"
aLIEzTeddeMacBook-Pro:java JianGuo$ javap -v -verbose Lava.class
Classfile /Users/JianGuo/IdeaProjects/HelloMac/src/main/java/Lava.class
Last modified Dec 1, 2016; size 267 bytes
MD5 checksum 83e64490eb9647ad12e2525489e5e344
Compiled from "Lava.java"
public class Lava
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // Lava.speed:I
#3 = Class #16 // Lava
#4 = Class #17 // java/lang/Object
#5 = Utf8 speed
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 flow
#12 = Utf8 SourceFile
#13 = Utf8 Lava.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = NameAndType #5:#6 // speed:I
#16 = Utf8 Lava
#17 = Utf8 java/lang/Object
{
public Lava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #2 // Field speed:I
9: return
LineNumberTable:
line 4: 0
line 5: 4
void flow();
descriptor: ()V
flags:
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
}
SourceFile: "Lava.java"
Reference
Inside the java virtual machine
Understanding Java Garbage Collection
How many types memory areas allocated by JVM
這是我來到簡書的第一篇文章,如果有錯誤的話請在評論區指出,謝謝閱讀!