面試官:小夥子,你給我講一下java類加載機制和內存模型吧 發佈文章 ### 類加載機制 虛擬

類加載機制

虛擬機把描述類的數據從 Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。java

類的生命週期

加載(Loading)
驗證(Verification)
準備(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸載(Unloading)程序員

類加載的過程

類的加載過程包括了加載,驗證,準備,解析,初始化
類的加載主要分爲如下三步:面試

1. 加載:根據路徑找到對應的.class文件

這一步會使用到類加載器。
加載是類加載的一個階段,注意不要混淆。算法

加載過程完成如下三件事:編程

經過類的徹底限定名稱獲取定義該類的二進制字節流。
將該字節流表示的靜態存儲結構轉換爲方法區的運行時存儲結構。
在內存中生成一個表明該類的 Class對象,做爲方法區中該類各類數據的訪問入口。api

2. 鏈接:

驗證:檢查待加載的class正確性;
準備:給類的靜態變量分配空間,此時靜態變量仍是零值(還沒到初始化的階段)
解析:將常量池的符號引用轉爲直接引用
符號引用:
符號引用是用一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能夠無歧義的定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。
直接引用:
直接引用能夠是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一符號引用在不一樣虛擬機實例上翻譯出來的直接引用一半不會相同,若是有了直接引用,那引用目標一定已經在內存中存在。
注意:實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一塊兒被分配在堆中。應該注意到,實例化不是類加載的一個過程,類加載發生在全部實例化操做以前,而且類加載只進行一次,實例化能夠進行屢次。數組

3. 初始化:對靜態變量和靜態代碼塊執行初始化工做

初始化階段才真正開始執行類中定義的 Java 程序代碼。初始化階段是虛擬機執行類構造器 () 方法的過程。在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。緩存

總結

在Java中,類裝載器把一個類裝入Java虛擬機中,要通過三個步驟來完成:加載、鏈接和初始化,其中連接又能夠分紅校驗、準備和解析三 步,除了解析外,其它步驟是嚴格按照順序完成的,各個步驟的主要工做以下:安全

裝載:查找和導入類或接口的二進制數據;
連接:執行下面的校驗、準備和解析步驟,其中解析步驟是能夠選擇的;
校驗:檢查導入類或接口的二進制數據的正確性;
準備:給類的靜態變量分配並初始化存儲空間;
解析:將符號引用轉成直接引用
初始化:激活類的靜態變量的初始化Java代碼和靜態Java代碼塊網絡

類初始化的時機

建立類的實例。new,反射,反序列化
使用某類的類方法–靜態方法
訪問某類的類變量,或賦值類變量
反射建立某類或接口的Class對象。Class.forName(「Hello」);—注意:loadClass調用ClassLoader.loadClass(name,false)方法,沒有link,天然沒有initialize
初始化某類的子類
直接使用java.exe來運行某個主類。即cmd java 程序會先初始化該類。

類的加載器(ClassLoader)

類加載器雖然只用於實現類的加載動做,可是還起到判別兩個類是否相同的做用。
對於任何一個類,都須要由加載它的類加載器和這個類自己一同確立其在java虛擬機中的惟一性。
一個java程序由若干個.class文件組成,當程序在運行時,會調用該程序的一個入口函數來調用系統的相關功能,而這些功能都被封裝在不一樣的class文件中。

程序在啓動時,並不會一次性加載程序所要用的全部class文件,而是根據程序的須要,經過java的類加載機制來動態加載某個.class文件到內存當中,從而只有class文件被載入到了內存以後,才能被其餘class引用,因此類的加載器就是用來動態加載class文件到內存當中用的。

類加載器如何判斷是一樣的類

java中一個類用 全限定類名標識——包名+類名
jvm中一個類用其 全限定類名+加載器標識——包名+類名+加載器名

類加載器的種類

從虛擬機的角度來分:
一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(HotSpot虛擬機中),是虛擬機自身的一部分;
另外一種就是全部其餘的類加載器,這些類加載器都有Java語言實現,獨立於虛擬機外部,而且所有繼承自java.lang.ClassLoader。
從開發者角度來分:
啓動(Bootstrap)類加載器:負責將Java_Home/lib下面的類庫加載到內存中(好比rt.jar)。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此==不容許直接經過引用進行操做。==加載java核心類

擴展(Extension)類加載器:它負責將Java_Home /lib/ext或者由系統變量 java.ext.dir指定位置中的類庫加載到內存中。開發者能夠直接使用標準擴展類加載器。

應用程序(Application)類加載器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(CLASSPATH)中指定的類庫。開發者能夠直接使用系統類加載器。默認使用

雙親機制

這裏的類加載器不是以繼承的關係來實現,都是以組合關係複用父類加載器的代碼。

定義:
某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。

使用雙親委派機制好處在於java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。

具體的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,若是沒加載到,則把任務轉交給Extension ClassLoader試圖加載,若是也沒加載到,則轉交給App ClassLoader 進行加載,若是它也沒有加載獲得的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。

若是它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。不然將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。

爲何要使用雙親委託這種模型?

雙親委託機制能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。

考慮到安全因素,咱們試想一下,若是不使用這種委託模式,那咱們就能夠隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在很是大的安全隱患,而雙親委託的方式,就能夠避免這種狀況,由於String已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,因此用戶自定義的ClassLoader永遠也沒法加載一個本身寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。

JVM在搜索類的時候,又是如何斷定兩個class是相同的呢

JVM在斷定兩個class是否相同時,不只要判斷兩個類名是否相同,並且要判斷是否由同一個類加載器實例加載的。只有二者同時知足的狀況下,JVM才認爲這兩個class是相同的。就算兩個class是同一份class字節碼,若是被兩個不一樣的ClassLoader實例所加載,JVM也會認爲它們是兩個不一樣class。

JAVA內存模型JMM

Java虛擬機規範試圖定義一種Java內存模型(JMM),來屏蔽掉各類硬件和操做系統的內存訪問差別,讓Java程序在各類平臺上都能達到一致的內存訪問效果。內存模型的做用就是控制一個線程的變量,何時對其餘線程可見。

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程裏面的變量有所不一樣步,它包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數,由於後者是線程私有的,不會共享,固然不存在數據競爭問題。

JMM規定了全部的變量都存儲在主內存(MainMemory)中。每一個線程還有本身的工做內存(WorkingMemory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。
Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。
從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。

內存間交互操做

java內存模型中定義瞭如下8種操做來完成,虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的。

lock鎖定:做用於主內存的變量。它把一個變量標識爲一條線程獨佔的狀態。
unlock解鎖:做用於主內存的變量
read讀取:做用於主內存的變量,把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
load載入:做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做中內存的變量副本中。
use使用:做用於工做內存的變量,它把工做內存中的一個變量的值傳遞給執行引擎。每當虛擬機遇到一個須要使用該變量的值的字節碼指令時會執行這個操做。
assign賦值:做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
store存儲:做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
write:做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。

volatile原理

變量對線程的可見性,比synchronized性能好
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:

1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
不能認爲,使用了volatile關鍵字,就認爲併發安全。在一些運算中,因爲運算並不是原子操做,仍是會出現同步的問題。

2)禁止進行指令重排序。
  普通變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。
  volatile修飾後,會加入內存屏障(指重排序時不能把後面的指令重排序到內存屏障以前的位置)。執行「lock addl $0x0,(%esp)」,這個操做是一個空操做,做用是使得本cpude Cache寫入了內存,該寫入動做也會引發別的cpu或別的內核無效化其cache,這種操做至關於對cache中的變量store 和write操做,使得對volatile變量的修改對其餘cpu當即可見。

內部原理

處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。

可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。

volatile關鍵字

被volatile修飾的共享變量,就具備瞭如下兩點特性:

保證了不一樣線程對該變量操做的內存可見性;
禁止指令重排序;

JMM三大特性

原子性

原子性即一個操做或一系列是不可中斷的。即便是在多個線程的狀況下,操做一旦開始,就不會被其餘線程干擾。

好比,對於一個靜態變量int x兩條線程同時對其賦值,線程A賦值爲1,而線程B賦值爲2,無論線程如何運行,最終x的值要麼是1,要麼是2,線程A和線程B間的操做是沒有干擾的,這就是原子性操做,不可被中斷的。

由jmm來直接保證的原子性變量操做包括read,load,assign,use,store,write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的,(double和long例外)。此外,synchronized塊之間的代碼也具備原子性

可見性

可見性指的是,當一個線程修改了共享變量的值後,其餘線程可以當即得知這個修改。volatile變量、synchronized,final三個關鍵字修飾的變量均可保證原子性。

有序性

在Java內存模型中有序性可概括爲這樣一句話:若是在本線程內觀察,全部操做都是有序的,若是在一個線程中觀察另外一個線程,全部操做都是無序的。

有序性是指對於單線程的執行代碼,執行是按順序依次進行的。但在多線程環境中,則可能出現亂序現象,由於在編譯過程會出現「指令重排」,重排後的指令與原指令的順序未必一致。所以,上面概括的前半句指的是線程內保證串行語義執行,後半句則指「指令重排」現象和「工做內存與主內存同步延遲」現象。

java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排的語義,而synchronized則是由一個變量在同一時刻只容許一條線程對其進行lock操做這條規則得到的。

指令重排

CPU和編譯器爲了提高程序執行的效率,會按照必定的規則容許進行指令優化。但代碼邏輯之間是存在必定的前後順序,併發執行時按照不一樣的執行邏輯會獲得不一樣的結果。

volatile關鍵詞修飾的變量,會禁止指令重排的操做,從而在必定程度上避免了多線程中的問題

volatile不能保證原子性,它只是對單個volatile變量的讀/寫具備原子性,可是對於相似i++這樣的複合操做就沒法保證了。

剛提到synchronized,能說說它們之間的區別嗎

volatile本質是在告訴JVM當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。
volatile僅能使用在變量級別;synchronized則可使用在變量、方法和類級別的;
volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量的修改可見性和原子性;
volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。

ABA問題

好比說線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。若是鏈表的頭在變化了兩次後恢復了原值,可是不表明鏈表就沒有變化

要解決"ABA問題",咱們須要增長一個版本號,在更新變量值的時候不該該只更新一個變量值,而應該更新兩個值,分別是變量值和版本號

原子變量

原子變量不使用鎖或其餘同步機制來保護對其值的併發訪問。全部操做都是基於CAS原子操做的。他保證了多線程在同一時間操做一個原子變量而不會產生數據不一致的錯誤,而且他的性能優於使用同步機制保護的普通變量,譬如說在多線程環境 中統計次數就可使用原子變量。

多線程的使用場景

有時候使用多線程並非爲了提升效率,而是使得CPU可以同時處理多個事件。

爲了避免阻塞主線程,啓動其餘線程來作好事的事情,好比APP中耗時操做都不在UI中作.
實現更快的應用程序,即主線程專門監聽用戶請求,子線程用來處理用戶請求,以得到大的吞吐量.感受這種狀況下,多線程的效率未必高。 這種狀況下的多線程是爲了避免必等待,能夠並行處理多條數據。好比JavaWeb的就是主線程專門監聽用戶的HTTP請求,而後啓動子線程去處理用戶的HTTP請求。
某種雖然優先級很低的服務,可是卻要不定時去作。
好比Jvm的垃圾回收。
某種任務,雖然耗時,可是不耗CPU的操做時,開啓多個線程,效率會有顯著提升。
好比讀取文件,而後處理。磁盤IO是個很耗費時間,可是不耗CPU計算的工做。 因此能夠一個線程讀取數據,一個線程處理數據。確定比一個線程讀取數據,而後處理效率高。由於兩個線程的時候充分利用了CPU等待磁盤IO的空閒時間。

最後

歡迎你們關注個人公衆號:前程有光,金三銀四跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料,文章都會在裏面更新,整理的資料也會放在裏面。

相關文章
相關標籤/搜索