1. 前言
?從學習Java的第一天開始,到如今工作當中,想必大家都耳聞目染了各種Java的優點。其中肯定少不了:Java有虛擬機,java是跨平臺的,一次編譯到處運行。在相當長的一段時間里對此觀點都只是一個很模糊的概念,對自己寫的代碼也有一種吃不透的感覺。猶如一只攔路的大老虎,望而生畏,止步不前。一番思量,一日不解決掉,對技術難以有更深層次的理解,只好硬著頭皮上。
2. 不能跨平臺的原因是怎樣造成的?
2.1 機器語言和匯編
?計算機只認識0和1
這句話大家都聽說過。的確,正所謂大道至簡,0和1足以撐起整個互聯網世界。在早期編程中,都是編寫一條條0和1組成的指令
來開發,要自己處理每一塊數據的存儲分配和輸入輸出。可想而知,滿屏的0和1,程序容易出錯且可讀性很差。
?使用 0和1
組成的機器指令來編程,太過于繁瑣,單單只是記住0和1組成的指令
就令人頭大。完全可以用一種簡易的方式代替記憶,例如做加法運算,而這個加
的操作在機器碼中可能是一個 010010
固定的指令,完全可以用 add
這個單詞來代替記憶,簡化了編程過程,這就是匯編語言。匯編語言的特點是用符號代替了機器指令代碼,而且符號與指令代碼一一對應,基本保留了機器語言的靈活性。而再將add指令
轉為010010機器碼
的程序便是匯編語言編譯器。
2.2 硬件關系
?組裝過電腦的朋友都知道,組裝一臺電腦需要購買:CPU、內存條、硬盤,主板等以及各種外設。對程序而言,一開始存儲在硬盤當中,即便計算機斷電,下次重啟程序依舊存在。CPU 是一個復雜的計算機部件,它內部又包含很多小零件,如下圖所示:
?????圖片摘自C語言中文網
?內存對于 CPU 來僅僅是一個存放指令和數據的地方,并不能在內存中完成計算功能。例如要計算 a = b + c,必須將 a、b、c 都讀取到 CPU 內部才能進行加法運算,寄存器是存儲 CPU 執行所需數據的區域,是 CPU 不可或缺的一部分,所有程序都只能通過操作寄存器,達到控制 CPU 目的,完成計算任務。
2.2 芯片架構
?arm
、X86
兩種芯片架構廣泛應用在 PC 機和移動端嵌入式設備中。前者由arm公司設計,后者由Intel、amd共同設計,雙方交叉授權使用。arm
是精簡指令集架構(RSIC),功耗較低,性能隨之也降了下來。x86
是復雜指令集架構(CISC),功耗較高,性能強。arm架構
的寄存器 比 x86架構
的多不少。寄存器和指令集加架構本身的差異性,也是造成不能跨平臺的原因。近幾十年來,硬件的性能一直都在飛速發展,CPU架構 也經歷了幾次較大的改變。 x86架構
從最早的 16 位到 32 位再到現在的 64 位架構。arm架構
也從 v1 發展到了如今的 v8的64位架構。一般新的架構都會向前兼容幾個版本,保證舊架構上的老代碼,能夠在新架構上運行。但這樣做,卻無法發揮出新架構硬件的性能,無疑是對資源的浪費。在開發中如果涉及到底層庫的使用,則需要考慮兼容不同架構的CPU。例如在使用百度地圖SDK時,會下載不同CPU架構的so文件,還有 X86 架構的,就是為了兼容不同CPU架構的手機。
Android可以通過adb命令來查看cpu信息1、adb shell 2、cat /proc/cpuinfo
2.3 C語言為什么不能夸平臺?
?通常認為 C 語言是編譯型語言。在編譯階段,編譯器直接將源碼
編譯為 對應CPU架構和操作系統上的可執行文件
。
如下圖所示 c 語言代碼編譯為的匯編代碼:
#include <stdio.h>
int main() {
printf("Hello World");
return 0;
}
Windows 部分匯編指令:
ubuntu 部分匯編指令:
雖然讀不太懂匯編指令,比較了一下差異還是不小的。C 語言更多的是偏向底層開發,只要編譯器足夠強大,支持對應平臺的編譯,或者對應平臺提供有C 編譯器
(C 語言的編譯器也是眾多語言中最多的)。程序就能在對應平臺執行,也許 C 語言從來就沒有想過要跨平臺。
代碼與平臺有關性,是不能跨平臺的原因。
3. JVM是如何做到跨平臺的
?講了這么多不能夸平臺的原因,再來理解Java是如何做到跨平臺就容易得多了。JVM 在編譯階段,只將 .java
的源碼,編譯為和平臺無關的 .class
字節碼文件。不同 CPU 架構和操作系統上都會編譯為相同的 calss 文件(最多只是 JDK 版本不同,有些許差異,jdk 都會向前兼容幾個版本)。再由不同平臺上的自行實現JVM。我們只需要搭建相應平臺的運行環境即可,便可做到任意平臺開發編譯,到處運行。
?JVM 在真機基礎之上模擬了一套自己的架構,有自己的指令集、內存管理等。在使用 Eclipse 追溯源碼時,常常會遇到只有 class 文件,而沒有源碼出現下面的頁面:
?圖中紅色框內的便是字節碼指令,運行時通過逐條解釋執行,這也是以前 Java 被指性能底下的詬點。的確,解釋執行的性能確實是和 C 編譯目標代碼比不了,但是在
JDK1.2
時就支持 JIT
及時編譯器。程序運行期間,分析熱點(經常調用)函數,編譯為本地代碼緩存起來,以后直接執行本地代碼。雖然性能還是和編譯型的語言有一定的差異,但 Java 憑借其語言特性以及各種成熟的 Web 解決方案,這點性能差顯得不那么重要,完全能夠接受。JIT 編譯代碼如下:有些JVM是采用純JIT編譯方式實現的,內部沒解釋器,例如JRockit、Maxine VM和Jikes RVM ---RednaxelaFX
4.JVM內存結構
?內存作為程序運行中的臨時存儲介質,本質上不進行任何的區域劃分,為了能夠合理有效的使用回收內存,才將內存劃分出更多的區域。平時聽得較多的就是堆棧內存,堆棧是一種數據結構,也是一種概念模型。不同的語言有自己的實現方式,通常在 Oop
編程中,棧存放函數執行時所需的局部變量,函數執行完即釋放,堆內存存儲對象。
操作系統內存布局
?Windows 上棧內存由系統回收,堆內存由程序員自行回收。因為棧上內存不可控,JVM 只能在操作系統的堆內存上開辟自己的空間。
JVM運行時內存結構
JVM堆
?所有類實例和數組都從堆中分配
,官方JVMS8規范文檔 的確是這樣描述的 The heap is the run-time data area from which memory for all class instances and arrays is allocated
。有一個很常見情況下,函數執行中產生的對象在堆中分配,函數執行結束,不再引用的對象,已經沒有存在的必要了。這些對象在堆中等待下一次GC,而大多對象朝生即死,生命周期極短,等待GC這段時間,也是對資源的浪費。在JDK1.5
時JVM
提供支持逃逸分析技術,通過分析對象作用域,實現了棧上分配、標量替換、同步消除優化等技術。通過函數傳遞對象,稱之為方法逃逸。將對象賦值給其他線程變量,稱之為線程逃逸:
標量替換
?不可再分解的基礎數據類型稱之為標量,例如Java中的八大基礎類型和引用類型。反之、如果某個對象還可繼續分解,則該對象屬于聚合量,Java類就是典型的聚合量。標量替換則是將對象的成員變量分解成原始數據類型,代替對象在棧中分配。
棧上分配
?JDK1.8默認開啟逃逸分析,確定對象不會再被外部引用,通過標量替換將對象分解在棧中分配,棧中的對象隨著棧幀的出棧而銷毀,大大的減少了堆內存的占用和GC的壓力。
public class Main {
public static void main(String[] args) throws Exception {
for(int i = 0 ; i < 1000000;i++){
Child child = new Child();
child.setAge(1);
}
System.out.println("阻塞...");
System.in.read();
}
public static class Child{
private int age;
private String name;
//省略get/set方法
}
}
開啟逃逸分析(1.8默認開啟)
C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
17456 sun.tools.jps.Jps
19680 linked.Main
7608
C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 19680
num #instances #bytes class name
----------------------------------------------
1: 220734 5297616 linked.Main$Child
2: 437 1763680 [I
3: 3099 449536 [C
4: 2392 57408 java.lang.String
5: 488 55696 java.lang.Class
6: 97 41776 [B
7: 835 33400 java.util.TreeMap$Entry
關閉逃逸分析:
C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
2436 sun.tools.jps.Jps
16536 linked.Main
7608
C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 16536
num #instances #bytes class name
----------------------------------------------
1: 1000000 24000000 linked.Main$Child
2: 451 1873120 [I
3: 3099 449536 [C
4: 2392 57408 java.lang.String
5: 488 55696 java.lang.Class
6: 97 41776 [B
7: 835 33400 java.util.TreeMap$Entry
可以看到,關閉逃逸分析總共使用堆內存 22M ,開啟逃逸分析只使用了 5M 左右。節約了不少堆內存空間,減少了 GC 壓力。
開啟逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC
關閉逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC
同步消除
如果逃逸分析確認對象的作用范圍不會超過當前線程,則消除對變量的同步措施。
JVM棧
?JVM棧 是方法執行所需的數據結構,每個線程都擁有一個JVM棧,隨著線程的創建而創建,隨著線程的銷毀而銷毀。JVM棧 以棧幀的單元,存放局部變量、操作數棧、動態鏈接、方法返回信息。具體可以參考
方法區/元數據區
?方法區中存放已被虛擬機加載的類信息,并且每個類只會存在一份,作為使用該類的入口。我們所編寫的代碼類,經過javac
編譯器,編譯存儲為 class 文件,在使用該類時(創建類的實例,調用了類靜態方法類等),如果該類還未加載,會先將該 class 字節流從磁盤或者其他途徑方式,加載存儲到方法區當中,并且創建該類的 class對象 供以后訪問使用。
運行時常量池
?運行時常量池作為方法區的一部分,為每一個類都維護一個常量池,存放著編譯時已知的字面量和各種符號引用。具體可見參考第二章
PC寄存器
?每個JVM線程都有自己的PC(程序計數器)寄存器。在任何時候,每個JVM線程都在執行單個方法的代碼,如果執行的不是native方法,則pc寄存器包含當前正在執行的Java字節碼指令的地址。如果當前執行的native方法,則PC寄存器的值undefined。
本地方法棧
?支持 native
方法調用,隨著線程的創建來分配本地方法棧。
參考:
深入理解Java虛擬機一書