今天開始,我們來講下JVM。一個好的工程,除了代碼優化之外,還需要有一個優化的運行環境,我們通過JVM系列來了解JVM和常用的優化手段。
JVM的概念
JVM是Java Virtual Machine的簡稱,意爲Java虛擬機。JVM使用軟件模擬Java 字節碼的指令集
虛擬機
指通過軟件模擬的具有完整硬件系統功能的、運行在一個完全隔離環境中的完整計算機系統
有哪些虛擬機
VMWare
Visual Box
JVM
VMWare或者Visual Box都是使用軟件模擬物理CPU的指令集
Java語言規範
這裏只列舉了部分常見的規範並且簡略介紹。詳細請移步官網。
語法
語法定義:
IfThenStatement:
if(true){do sth;}
ArgumentList:
add(a,b,c,d);
詞法結構:
- \u + 4個16進制數字 表示UTF-16
- 行終結符: CR, or LF, or CR LF.
- 空白符
• 空格 tab \t 換頁 \f 行終結符
- 註釋
- 標示符
- 關鍵字
命名規範
類型與變量
元類型
byte short int long float char
變量初始值
• boolean false
• char \u0000
更多
– Java內存模型
– 類加載鏈接的過程
– public static final abstract的定義
– 異常
– 數組的使用
– …….
JVM規範
JVM規範的定義
Java語言規範定義了什麼是Java語言,Java語言和JVM相對獨立,JVM主要定義二進制class文件和JVM指令集等。
JVM常見規範
– Class文件類型
– 數字的內部表示和存儲
– 運行時數據
– 幀棧
– 虛擬機的啓動
– 虛擬機的指令集
– 類型轉化
– l2i
– 出棧入棧操作
– aload astore
– 運算
– iadd isub
– 流程控制
– ifeq ifne
– 函數調用
invokevirtual invokeinterface invokespecial invokestatic
JVM需要對Java Library 提供以下支持
– 反射 java.lang.reflect
– ClassLoader
– 初始化class和interface
– 安全相關 java.security
– 多線程
– 弱引用
JVM啓動流程
JVM基本結構
PC寄存器
– 每個線程擁有一個PC寄存器
– 在線程創建時 創建
– 指向下一條指令的地址
方法區
– 保存裝載的類信息
類型的常量池
字段,方法信息
方法字節碼
通常和永久區(Perm)關聯在一起
Java堆
– 和程序開發密切相關
– 應用系統對象都保存在Java堆中
– 所有線程共享Java堆
– 對分代GC來說,堆也是分代的,堆空間裏面主要分爲新生代(eden+倖存區from和to)和老年代,from和to在後期會介紹到
– GC的主要工作區間
Java棧
– 線程私有
– 棧由一系列幀組成(因此Java棧也叫做幀棧)
– 幀保存一個方法的局部變量表、操作數棧、常量池指針
局部變量表:包含參數和局部變量
操作數棧:Java沒有寄存器,所有參數傳遞使用操作數棧
每一次方法調用創建一個幀,並壓棧
執行本地方法時,PC的值爲undefined
– 棧上分配概念和實現
我們都知道Java中的對象都是在堆上分配的,而垃圾回收機制會回收堆中不再使用的對象,但是篩選可回收對象,回收對象還有整理內存都需要消耗時間。如果能夠通過逃逸分析確定某些對象不會逃出方法之外,那就可以讓這個對象在棧上分配內存。
– 小對象(一般幾十個bytes),沒有逃逸的情況下,可以分配在棧上
– 直接分配在棧上,可以自動回收,減輕GC壓力
– 大對象或者逃逸對象無法棧上分配
– 在JVM使用 -XX:+DoEscapeAnalysis 來實現棧上分配
逃逸對象是指方法中定義的變量會被外部調用,外部指別的線程或方法。
逃逸分析會有時間消耗,所以性能未必提升多少,並且由於逃逸分析比較耗時,目前的實現都是採用不那麼準確但是時間壓力相對較小的算法來完成逃逸分析,這就可能導致效果不穩定,要慎用。
棧、堆、方法區交互
public class AppMain //運行時, jvm 把AppMain的信息都放入方法區
{
//main 方法本身放入方法區。
public static void main(String[] args) {
//test1是引用,所以放到棧區裏, Sample是自定義對象應該放到堆裏面
Sample test1 = new Sample( " 測試1 " );
Sample test2 = new Sample( " 測試2 " );
test1.printName();
test2.printName();
}
}
//運行時, jvm 把appmain的信息都放入方法區
public class Sample{
//new Sample實例後, name 引用放入棧區裏, name 對象放入堆裏
private String name;
public Sample(String name) { this .name = name; }
//print方法本身放入 方法區裏。
public void printName() { System.out.println(name); }
}
內存模型
– 每一個線程有一個工作內存和主存獨立
– 工作內存存放主存中變量的值的拷貝
當數據從主內存複製到工作存儲時,必須出現兩個動作:第一,由主內存執行的讀(read)操作;第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:第一個,由工作內存執行的存儲(store)操作;第二,由主內存執行的相應的寫(write)操作。
對於普通變量,一個線程中更新的值,不能馬上反應在其他變量中
如果需要在其他線程中立即可見,需要使用 volatile 關鍵字
因爲內存模型的設計,會出現以下幾種特性:
– 可見性
一個線程修改了變量,其他線程可以立即知道。
– 保證可見性的方法
– volatile
– synchronized (unlock之前,寫變量值回主存)
– final(一旦初始化完成,其他線程就可見)
– 有序性
在本線程內,操作都是有序的
在線程外觀察,操作都是無序的。(指令重排 或 主內存同步延時)
指令重排
- 線程內串行語義
• 寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
• 寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
• 讀後寫 a = b;b = 1; 讀一個變量之後,再寫這個變量。
• 以上語句不可重排
• 編譯器不考慮多線程間的語義
• 可重排: a=1;b=2;
-破壞線程間的有序性
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
……
}
}
}
線程A首先執行writer()方法
線程B線程接着執行reader()方法
線程B在int i=a+1 是不一定能看到a已經被賦值爲1
因爲在writer中,兩句話順序可能打亂
-保證有序性的方法
class OrderExample {
int a = 0;
boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if (flag) {
int i = a +1;
……
}
}
}
同步後,即使做了writer重排,因爲互斥的緣故,reader 線程看writer線程也是順序執行的。
-指令重排的基本原則
– 程序順序原則:一個線程內保證語義的串行性
– volatile規則:volatile變量的寫,先發生於讀
– 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
– 傳遞性:A先於B,B先於C 那麼A必然先於C
– 線程的start方法先於它的每一個動作
– 線程的所有操作先於線程的終結(Thread.join())
– 線程的中斷(interrupt())先於被中斷線程的代碼
– 對象的構造函數執行結束先於finalize()方法
編譯和解釋運行的概念
解釋運行
– 解釋執行以解釋方式運行字節碼
– 解釋執行的意思是:讀一句執行一句
編譯運行(JIT)
– 將字節碼編譯成機器碼
– 直接執行機器碼
– 運行時編譯
– 編譯後性能有數量級的提升
新版本的jvm默認都是採用混合執行模式。程序字節碼經過JIT環境變量進行判斷,是否屬於“熱點代碼”(多次調用的方法,或循環等),如是,走JIT編譯爲具體硬件處理器(如sparc、intel)機器碼,如否,則直接由解釋器解釋執行。