JVM啓動流程:html
《深刻理解Java虛擬機(第二版)》中的描述是下面這個樣子的:面試
Java中的內存分配:算法
Java程序在運行時,須要在內存中的分配空間。爲了提升運算效率,就對數據進行了不一樣空間的劃分,由於每一片區域都有特定的處理數據方式和內存管理方式。編程
具體劃分爲以下5個內存空間:(很是重要)數組
棧:存放局部變量 堆:存放全部new出來的東西 方法區:被虛擬機加載的類信息、常量、靜態常量等。 程序計數器(和系統相關) 本地方法棧緩存
一、程序計數器:安全
每一個線程擁有一個PC寄存器多線程
在線程建立時建立併發
指向下一條指令的地址
執行本地方法時,PC的值爲undefined
二、方法區:
保存裝載的類信息
類型的常量池
字段,方法信息
方法字節碼
一般和永久區(Perm)關聯在一塊兒
三、堆內存:
和程序開發密切相關
應用系統對象都保存在Java堆中
全部線程共享Java堆
對分代GC來講,堆也是分代的
GC管理的主要區域
如今的GC基本都採用分代收集算法,若是是分代的,那麼堆也是分代的。若是堆是分代的,那堆空間應該是下面這個樣子:
上圖是堆的基本結構,在以後的文章中再進行詳解。
四、棧內存:
線程私有,生命週期和線程相同 棧由一系列幀組成(所以Java棧也叫作幀棧) 幀保存一個方法的局部變量、操做數棧、常量池指針 每一次方法調用建立一個幀,並壓棧
解釋:
Java虛擬機棧描述的是Java方法執行的內存模型:每一個方法被調用的時候都會建立一個棧幀,用於存儲局部變量表、操做棧、動態連接、方法出口等信息。每個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機中從入棧到出棧的過程。
在Java虛擬機規範中,對這個區域規定了兩種異常狀況:
(1)若是線程請求的棧深度太深,超出了虛擬機所容許的深度,就會出現StackOverFlowError(好比無限遞歸。由於每一層棧幀都佔用必定空間,而 Xss 規定了棧的最大空間,超出這個值就會報錯)
(2)虛擬機棧能夠動態擴展,若是擴展到沒法申請足夠的內存空間,會出現OOM
4.1 Java棧之局部變量表:包含參數和局部變量
局部變量表存放了基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。其中64位長度的long和double類型的數據會佔用2個局部變量空間(slot),其他數據類型只佔用1個。局部變量表所需的內存空間在編譯期間完成分配。
例如,我寫出下面這段代碼:
package test03; /** * Created by smyhvae on 2015/8/15. */ public class StackDemo { //靜態方法 public static int runStatic(int i, long l, float f, Object o, byte b) { return 0; } //實例方法 public int runInstance(char c, short s, boolean b) { return 0; } } |
上方代碼中,靜態方法有6個形參,實例方法有3個形參。其對應的局部變量表以下:
上方表格中,靜態方法和實例方法對應的局部變量表基本相似。但有如下區別:實例方法的表中,第一個位置存放的是當前對象的引用。
四、2 Java棧之函數調用組成棧幀:
方法每次被調用的時候都會建立一個棧幀,例以下面這個方法:
public static int runStatic(int i,long l,float f,Object o ,byte b){ return runStatic(i,l,f,o,b); } |
當它每次被調用的時候,都會建立一個幀,方法調用結束後,幀出棧。以下圖所示:
4.3 Java棧之操做數棧
Java沒有寄存器,全部參數傳遞都是使用操做數棧
例以下面這段代碼:
1 2 3 4 5 |
public static int add(int a,int b){ int c=0; c=a+b; return c; } |
壓棧的步驟以下:
0: iconst_0 // 0壓棧
1: istore_2 // 彈出int,存放於局部變量2
2: iload_0 // 把局部變量0壓棧
3: iload_1 // 局部變量1壓棧
4: iadd //彈出2個變量,求和,結果壓棧
5: istore_2 //彈出結果,放於局部變量2
6: iload_2 //局部變量2壓棧
7: ireturn //返回
若是計算100+98的值,那麼操做數棧的變化以下圖所示:
4.4 Java棧之棧上分配:
小對象(通常幾十個bytes),在沒有逃逸的狀況下,能夠直接分配在棧上
直接分配在棧上,能夠自動回收,減輕GC壓力
大對象或者逃逸對象沒法棧上分配
棧、堆、方法區交互:
3、內存模型:
每個線程有一個工做內存。工做內存和主存獨立。工做內存存放主存中變量的值的拷貝。
當數據從主內存複製到工做存儲時,必須出現兩個動做:第一,由主內存執行的讀(read)操做;第二,由工做內存執行的相應的load操做;當數據從工做內存拷貝到主內存時,也出現兩個操做:第一個,由工做內存執行的存儲(store)操做;第二,由主內存執行的相應的寫(write)操做。
每個操做都是原子的,即執行期間不會被中斷
對於普通變量,一個線程中更新的值,不能立刻反應在其餘變量中。若是須要在其餘線程中當即可見,須要使用volatile關鍵字做爲標識。
一、可見性:
一個線程修改了變量,其餘線程能夠當即知道
保證可見性的方法:
volatile
synchronized (unlock以前,寫變量值回主存)
final(一旦初始化完成,其餘線程就可見)
二、有序性:
在本線程內,操做都是有序的
在線程外觀察,操做都是無序的。(指令重排 或 主內存同步延時)
三、指令重排:
指令重排:破壞了線程間的有序性:
指令重排:保證有序性的方法:
指令重排的基本原則:
程序順序原則:一個線程內保證語義的串行性
volatile規則:volatile變量的寫,先發生於讀
鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
傳遞性:A先於B,B先於C 那麼A必然先於C
線程的start方法先於它的每個動做
線程的全部操做先於線程的終結(Thread.join())
線程的中斷(interrupt())先於被中斷線程的代碼
對象的構造函數執行結束先於finalize()方法
4、解釋運行和編譯運行的概念:
解釋運行:
解釋執行以解釋方式運行字節碼
解釋執行的意思是:讀一句執行一句
編譯運行(JIT):
將字節碼編譯成機器碼
直接執行機器碼
運行時編譯
編譯後性能有數量級的提高
編譯運行的性能優於解釋運行
Q:簡單說說 Java 的 JVM 內存結構分爲哪幾個部分?
A:JVM 內存共分爲虛擬機棧、堆、方法區、程序計數器、本地方法棧,運行時常量池(六個部分,分別解釋以下)
· 虛擬機棧:
線程私有的,每一個方法在執行時會建立一個棧幀,用來存儲局部變量表、操做數棧、動態鏈接、方法返回地址等;其中局部變量表用於存放 8 種基本數據類型(boolean、byte、char、short、int、float、long、double)和 reference 類型。每一個方法從調用到執行完畢對應一個棧幀在虛擬機棧中的入棧和出棧。
· 堆:
線程共享的,在虛擬機啓動時建立,用於存放對象實例。
· 方法區:
線程共享的,用於存儲已被虛擬機加載的類信息、常量、靜態變量等。
· 程序計數器:
線程私有的,是當前線程所執行的字節碼行號指示器,每一個線程都有一個獨立的程序計數器,字節碼解釋器工做時經過改變它的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理和線程恢復都依賴於它。
· 本地方法棧:
線程私有的,主要爲虛擬機用到的 native 方法服務,與虛擬機棧相似。
· 運行時常量池(Runtime Constant Pool)
,存放的爲類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區域中分配。
Q: 堆和棧的特色
當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。
堆內存用來存放由new建立的對象和數組。
java中變量在內存中的分配
一、類變量(static修飾的變量):在程序加載時系統就爲它在堆中開闢了內存,堆中的內存地址存放於棧以便於高速訪問。靜態變量的生命週期--一直持續到整個'系統'關閉
二、實例變量:當你使用java關鍵字new的時候,系統在堆中開闢並不必定是連續的空間分配給變量(好比說類實例),而後根據零散的堆內存地址,經過哈希算法換算爲一長串數字以表徵這個變量在堆中的'物理位置'。 實例變量的生命週期--當實例變量的引用丟失後,將被GC(垃圾回收器)列入可回收「名單」中,但並非立刻就釋放堆中內存
三、局部變量:局部變量,由聲明在某方法,或某代碼段裏(好比for循環),執行到它的時候在棧中開闢內存,當局部變量一但脫離做用域,內存當即釋放
Q:你能不能談談,java GC是在何時,對什麼東西,作了什麼事情?
A:
Q:簡單談談你對 Java 虛擬機內存模型 JMM 的認識和理解及併發中的原子性、可見性、有序性的理解?
這是一個很泛很大且頗有水準的面試題,也算是對併發基礎原理實質的一個深度問題,想要在面試中簡短的回答好不是特別容易,本解析也僅供參考,具體理解可本身查閱其餘資料。
Java 內存模型主要目標是定義程序中變量(此處變量特指實例字段、靜態字段等,但不包括局部變量和函數參數,由於這兩種是線程私有沒法共享)在虛擬機中存儲到內存與從內存讀取出來的規則細節,Java 內存模型規定全部變量都存儲在主內存中,每條線程還有本身的工做內存,工做內存保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行而不能直接讀寫主內存中的變量,不一樣線程之間沒法相互直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成(具體以下圖)。
Java 內存模型對主內存與工做內存之間的具體交互協議定義了八種操做,具體以下:
· lock(鎖定):做用於主內存變量,把一個變量標識爲一條線程獨佔狀態。
· unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
· read(讀取):做用於主內存變量,把一個變量從主內存傳輸到線程的工做內存中,以便隨後的 load 動做使用。
· load(載入):做用於工做內存變量,把 read 操做從主內存中獲得的變量值放入工做內存的變量副本中。
· use(使用):做用於工做內存變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量值的字節碼指令時執行此操做。
· assign(賦值):做用於工做內存變量,把一個從執行引擎接收的值賦值給工做內存的變量,每當虛擬機遇到一個須要給變量進行賦值的字節碼指令時執行此操做。
· store(存儲):做用於工做內存變量,把工做內存中一個變量的值傳遞到主內存中,以便後續 write 操做。
· write(寫入):做用於主內存變量,把 store 操做從工做內存中獲得的值放入主內存變量中。
若是要把一個變量從主內存複製到工做內存就必須按順序執行 read 和 load 操做,從工做內存同步回主內存就必須順序執行 store 和 write 操做,可是 JVM 只要求了操做的順序而沒有要求上述操做必須保證連續性,因此實質執行中 read 和 load 間及 store 和 write 間是能夠插入其餘指令的。
Java 內存模型還會對指令進行重排序操做,在執行程序時爲了提升性能編譯器和處理器常常會對指令進行重排序操做,重排序主要分下面幾類:
· 編譯器優化重排序:編譯器在不改變單線程程序語義的前提下能夠從新安排語句的執行順序。
· 指令級並行重排序:現代處理器採用了指令級並行技術來將多條指令重疊執行,若是不存在數據依賴性處理器能夠改變語句對應機器指令的執行順序。
· 內存系統重排序:因爲處理器使用緩存和讀寫緩衝區使得加載和存儲操做看上去多是在亂序執行。
其實 Java JMM 內存模型是圍繞併發編程中原子性、可見性、有序性三個特徵來創建的,關於原子性、可見性、有序性的理解以下:
· 原子性:就是說一個操做不能被打斷,要麼執行完要麼不執行,相似事務操做,Java 基本類型數據的訪問大都是原子操做,long 和 double 類型是 64 位,在 32 位 JVM 中會將 64 位數據的讀寫操做分紅兩次 32 位來處理,因此 long 和 double 在 32 位 JVM 中是非原子操做,也就是說在併發訪問時是線程非安全的,要想保證原子性就得對訪問該數據的地方進行同步操做,譬如 synchronized 等。
· 可見性:就是說當一個線程對共享變量作了修改後其餘線程能夠當即感知到該共享變量的改變,從 Java 內存模型咱們就能看出來多線程訪問共享變量都要通過線程工做內存到主存的複製和主存到線程工做內存的複製操做,因此普通共享變量就沒法保證可見性了;Java 提供了 volatile 修飾符來保證變量的可見性,每次使用 volatile 變量都會主動從主存中刷新,除此以外 synchronized、Lock、final 均可以保證變量的可見性。
· 有序性:就是說 Java 內存模型中的指令重排不會影響單線程的執行順序,可是會影響多線程併發執行的正確性,因此在併發中咱們必需要想辦法保證併發代碼的有序性;在 Java 裏能夠經過 volatile 關鍵字保證必定的有序性,還能夠經過 synchronized、Lock 來保證有序性,由於 synchronized、Lock 保證了每一時刻只有一個線程執行同步代碼至關於單線程執行,因此天然不會有有序性的問題;除此以外 Java 內存模型經過 happens-before 原則若是能推導出來兩個操做的執行順序就能先天保證有序性,不然沒法保證,關於 happens-before 原則能夠查閱相關資料