JVM學習小結(2)-類加載機制&&字節碼執行引擎&&Java內存模型:

類加載機制:

類加載生命期:加載(Loading),驗證(Verification),準備(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸載(Unloading)
	初始化:
		1.遇到new,getstatic,putstatic,invokestatic指令,類沒有進行初始化,先觸發初始化
		2.java反射機制
		3.初始化一個類,父類沒有初始化,需要先觸發父類的初始化
		4.JVM啓動的時候,用戶需要指定一個執行的主類(main所在的位置),JVM優先初始化
		5.JDK1.7動態語言支持
		注:
			1.只有直接定義靜態字段(static int i=1;)的類纔會被初始化,而(static{sysout("Hello")})並不會觸發初始化
			2.類在初始化的時候必須要求其父類初始化,但是接口不需要,只有真正使用到父類接口,父類接口才會初始化
	加載:
		1.通過一個類的全限定名來獲取定義此類的二進制字節流//因此可從jar,war,網絡獲取類結構
		2.將這個字節流所代表的靜態結構轉化爲方法區的運行時數據結構
		3.在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區的訪問入口
		注:類加載的過程可以通過系統的類加載器來完成,也可以自定義類加載器控制字節流獲取方式
		加載階段完成之後,類的字節流文件按照JVM的格式(格式由JVM定)儲存在方法區中,然後在內存中實例化一個java.lang.Class對象對於HotSpot來說,該對象存放在方法區中,這個對象將作爲程序訪問的方法區數據類型的接口
	驗證:
		確保Class文件的字節流中包含的信息不會損害JVM
		1.格式驗證:是否以魔數開頭,主次版本號,常量池中是否有不支持常量的類型,編碼情況,Class文件是否具有附加信息.....
		2.元數據驗證:檢查是否符合Java語言規範
		3.字節碼驗證:驗證類型轉換
		4.符號引用驗證:字符串限定符,類字段,方法的限定符
	準備:
		爲類變量(靜態變量)分配內存,設置初值(默認0/false/null),實例變量的初始化跟隨對象一起分配到Java堆中
	解析:將符號引用(通過一組符號描述引用目標,符號引用的實現與內存佈局無關)替換爲直接引用(直接指向目標的指針/句柄)
		解析動作主要針對類或接口,字段,類方法,接口方法,方法類型,方法句柄,和調用限定符7類符號引用進行,分別對應常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_MethodType_info,CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7中類型
	字段解析,類方法解析,接口方法解析
	初始化(類加載過程最後一步//初始化階段的過程就是執行類構造器<climit>()方法的過程):
		<climit>()方法是由編譯器自動收集類中所有變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生,編譯器收集的順序是由語句在源文件中出現順序決定
		例:
			public class Test {
				static{
					i = 0;//靜態代碼塊中的i可以被賦值,但不能被訪問(非法向前引用)正常編譯
					System.out.println(i);//報 illegal forward reference(非法向前引用)
				}
				static int i = 1;//如果要訪問i,應該將i在靜態代碼塊前面進行聲明
			}
		<climit>()方法與類的構造方法(<init>()方法)不同,它不需要顯示地調用父類構造器,虛擬機保證在子類的<climit>()方法執行之前,父類的<climit>()方法已經執行完畢,因此在JVM中第一個被執行的<climit>()方法的類肯定是java.lang.Object
		由於<climit>()先執行,所以靜態語句塊優先加載
		例:
			static class parent{
				public static int a=1;
				static{
					a=2;
				}
			}
			static class sub extends parent{
				public static int b=a;//此時b會被初始化爲2
			}
		注:<climit>()對於類或接口來說,不是必須的,當類中沒有靜態語句塊,沒有對類變量賦值操作,編譯器將不會生成<climit>方法
		接口中不能使用靜態語句塊,但可以有變量賦值操作,說明接口和類都一樣會生成<climit>(),接口與類不同的是,接口中不需要先執行父類的<climit>(),只有使用父接口才會去初始化
		JVM會保證一個類的<climit>()在多線程的環境中被正確加鎖,同步,多個線程同時初始化一個類,將只會有一個線程去執行類的<climit>(),其他線程阻塞,直到線程活動執行完<climit>()完畢,就此會造成線程阻塞問題(並且很隱蔽)
	類加載器
		雙親委派模型
			前序:
				啓動類加載器(Bootstrap ClassLoader)//C++語言實現,JVM自身一部分,主要作用是將放在<JAVA_HOME>\lib目錄中/被-Xbootclasspath參數指定的路徑的類庫(要被JVM識別)加載到JVM內存中
				擴展類加載器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實現,用於加載<JAVA_HOME>\lib\ext目錄中/java.ext.dirs系統變量所指定的路徑中所有類庫
				應用程序加載器:加載用戶類路徑上指定類庫
				最後加載器爲自定義加載器
				上述加載器除Bootstrap ClassLoader統稱爲其他類加載器//由java語言實現,獨立於虛擬機外部,繼承於抽象類java.lang.ClassLoader
			雙親委派模型(強制性約束模型):一個類收到加載請求首先會委派父類加載器完成,直到請求傳送到頂層啓動類加載器中,只有當父類反饋無法完成加載請求,子類加載器纔會嘗試加載,保證程序的穩定運行
		OSGi環境下,類加載器不再是樹狀模型,而是網狀,他會採用同級之間的類進行加載

字節碼執行引擎

執行引擎:使用解釋器執行,通過及時編譯器產生本地代碼執行
運行棧幀結構:
棧幀:支持虛擬機進行方法調用和方法執行的數據結構
棧幀儲存的東西:
	方法的局部變量表:
		存放參數和方法內部定義的局部變量,編譯Class文件,在方法的Code屬性的max_locals確定了局部變量表的最大內容
		方法執行的過程,JVM使用局部變量完成參數值到參數變量列表的傳遞過程
		非靜態方法,局部變量表中第0個索引的slot默認適用於傳遞方法所屬的對象實例的引用(this),其餘參數按照參數表的順序排列//爲節省空間,slot可以重用
		slot(Variable Slot,變量槽):
			局部變量表容量的最小單位(未說明具體佔用空間大小),每個slot都可以存放一個boolean,byte,char,short,int,float,reference,returnAddress類型數據
			一個slot可以存放32位以內的數據類型
			reference表示對對象數據的引用:可以通過此引用直接或間接地查找到對象所屬數據類型在方法區中的儲存類型信息
			64位的數據類型,虛擬機會以最高位對齊的方式爲其分配兩個連續的slot空間
			Java中64位數據(reference可能是32位,也可能是64位)的只有long和double, JVM通過索引定位的方式使用局部變量表,索引範圍從0開始,64位數據,則會同時使用n,n+1兩個slot
			Slot重用:當方法體中的變量作用域未覆蓋整個方法體,且pc計數器超出這個變量的作用域範圍,爲節省空間,Slot將會複用,但是複用也會影響到GC(當發現solt沒有被其他其他變量複用,GC Roots還保持這引用之間的關聯,那麼,即使離開了變量的作用域範圍,GC卻不會回收內存),所以不使用的變量要手動設置null,避免GC錯誤
			注:局部變量必須賦初值,於類變量不一樣
	操作數棧(操作棧):
		在編譯時確定了棧最大深度,在方法的Code屬性的max_stacks確定(棧深度永遠不會超出該深度)
		操作數棧的每一個元素可以是任意數據類型,包含long/double
		32位所佔的棧容量爲1,64位爲2
	動態鏈接:
		方法返回地址:
			在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行, 方法推出的過程,實際上就是等於把當前棧出棧,因此退出時的操作:恢復上層方法的局部變量表和操作數棧,把返回值壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向調用指令後面的一條指令
		方法退出的方式:
			正常完成出口:調用者的PC計數器的值可以作爲返回地址,棧幀中會保留這個計數器的值
			異常出口:返回值需要通過異常處理器來確定,棧幀中一般不會保留此消息
		注:方法重載是靜態分派的典型應用(編譯時確定對象),方法重寫是動態分派的典型應用(運行時確定對象)
	方法調用:Class文件不包含傳統編譯的連接操作,一切方法調用直接是調用在Class文件的符號引用,而非方法實際運行時的內存佈局的入口地址
		1.解析:類加載解析階段,將其中一部分符號引用轉化爲直接引用,符合編譯期可知,運行期不變---靜態方法,私有方法(二者的隱蔽性,不能被重寫,即不可實現多態)
		JVM內置方法調用字節碼指令:
			invokestatic:調用靜態方法;
			invokespecial:調用實例構造器<init>方法,私有方法,父類方法;
			invokevirtual:調用所有虛方法
			invokeinterface:調用接口方法,運行時確定實現接口的對象
			(分派邏輯由用戶設定引導)invokedynamic:運行時動態解析調用出限定符所引用的方法
			注: invokestatic,invokespecial一定是調用靜態/私有/構造/父類方法,類加載的時候就會將符號引用轉化爲直接引用(上述方法爲非虛方法, 除上述方法,final方法其餘方法都爲虛方法),final方法不能被覆蓋,就沒有多態的說法
		2.分派:
			靜分派:在編譯時就確定引用所指的對象; 典型應用就是重載
			動態分派:與多態相關---重寫
		3.JVM動態分派的實現:
			建立虛表結構:當子類沒有對父類的方法進行重寫是,子類虛方法表裏的入口地址和父類的入口地址一致,如果子類重寫父類方法,子類方法表中的地址將會替換爲子類重寫後方法的入口地址
		4.java.lang.invoke:
			java中不能將函數名作爲參數(函數指針),只能通過;當Method Handle提出後,就可以將函數名作爲參數
	JDK1.7提供的動態確定目標方法機制:MethodHandle;
		MethodHandle與反射的區別:
			反射實在java代碼級別模擬方法調用,MethodHandle是在字節碼級別
			MethodHandle的一部分方法對應的字節碼指令執行的權限校驗行爲,Reflection API中無需關心
			Reflection是爲Java語言服務,MethodHandle可以設計爲服務於所有的JVM,當然也包含Java語言
		invokedynamic指令(靜態解析--解析私有方法,靜態方法兩大類)
			每一處含有invokedymic指令的位置稱作是動態調用點(Dynamic Call Site),這條指令的第一個參數不再是代表方法符號引用的CONSTANT_Methodref_info常量,而是CONSTANT_MEthodrefDynamic_info常量,該常量中可以得到:Bootsrap method引導方法,有固定的參數,返回值爲java.lang.invoke.CallSite,代表真正要執行的方法調用; Method Type(方法類型);方法名稱
			靜態分派--方法重載的過程中調用重載,形參會根據語義選擇合適的方法
			動態分派--多態性體現(重寫):由於invokevirtual在運行時確定接受者不同的類型,然後會將常量池中類方法符號解析到不同的直接引用上面,invokevirtual搜索接收者的方法時通過虛方法表(如果子類重寫父類方法,表中的函數入口優先指向子類的方法,反之指向父類,接口也一致)

Java內存模型:

主內存:Java內存模型規定所有的變量都保存在主內存中
工作內存:每個線程都有自己的工作內存,保存了該線程使用到的變量的主內存副本拷貝
主內存與工作內存的關係:
	線程對變量的所有操作都必須在自己的工作內存中進行,不能直接讀寫主內存中的變量
	不同線程之間無法直接訪問對方工作內存中的變量
	線程間變量值的傳遞均需要通過主內存來完成
內存交互操作(變量與主內存中間的操作,下列操作都是原子性):
	lock:將變量標識爲一條線程獨佔狀態,作用於主內存的變量
	unlock:將鎖定狀態的變量釋放,釋放後的變量纔可以被其他線程鎖定
	read:將變量值從主內存傳輸到線程的工作內存中
	load:作用於工作內存將讀取後的變量放入工作內存的副本中
	use:將工作內存中的變量傳遞給執行引擎,每當JVM遇到一個需要使用的變量值的字節碼指令就會執行這個操作
	assign(賦值):將一個執行引擎接收到的值賦給工作內存的變量,每JVM遇到變量賦值的字節碼指令執行這個操作//作用於工作內存
	store(儲存):將工作內存中的一個變量值傳送到主內存中,以便write使用//作用於工作內存
	write:將store操作送過來的變量放入主內存的變量中
	注:
		read-load,store-write必須順序執行,不需要連續執行(read a,read a,load b,load b),不可單獨出現
		不允許一個線程丟棄它最近的assign的操作,即變量在工作內存中改變了之後必須把該變化同步到主內存中
		不允許一個線程無原因地把數據從線程的工作內存同步回主內存中
		一個新變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化的變量
		一個變量在同一時刻只允許一條線程對其lock操作,但lock操作可以被同一條線程執行多次,多次執行lock後,只有執行相同次數的unclock,變量才能解鎖
		如果對一個變量執行lock操作,將會清空工作內存 中此變量的值,在執行引擎使用這個變量前,需要重新執行load/assign來初始化變量
		如果一個變量沒被lock鎖定,則不允許對它執行unclock,unclock也一致
		對變量執行unclock操作之前,必須把此變量同步到主內存中(執行store/write)
volatile(最輕量級同步機制)
	變量被聲明爲volatile,具有以下特性:
		保證可見性,不保證原子性
		 a.當寫一個volatile變量時,JMM會把該線程本地內存中的變量強制刷新到主內存中去;
		 b.這個寫會操作會導致其他線程中的緩存無效。
		(2)禁止指令重排 
		 	重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段。重排序需要遵守一定規則:
			a.重排序操作不會對存在數據依賴關係的操作進行重排序。
		        比如:a=1;b=a; 這個指令序列,由於第二個操作依賴於第一個操作,所以在編譯時和處理器運行時這兩個操作不會被重排序。
			b.重排序是爲了優化性能,但是不管怎麼重排序,單線程下程序的執行結果不能被改變
		      比如:a=1;b=2;c=a+b這三個操作,第一步(a=1)和第二步(b=2)由於不存在數據依賴關係, 所以可能會發生重排序,但是c=a+b這個操作是不會被重排序的,因爲需要保證最終的結果一定是c=a+b=3。
		 (3) 使用volatile關鍵字修飾共享變量便可以禁止這種重排序。若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序,volatile禁止指令重排序也有一些規則:
	   		a.當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
	   		b.在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
			即執行到volatile變量時,其前面的所有語句都執行完,後面所有語句都未執行。且前面語句的結果對volatile變
		量及其後面語句可見。
		重排序在單線程下一定能保證結果的正確性,但是在多線程環境下,可能發生重排序,影響結果,下例中的1和2由於不存在數據依賴關係,則有可能會被重排序,先執行status=true再執行a=2。而此時線程B會順利到達4處,而線程A中a=2這個操作還未被執行,所以b=a+1的結果也有可能依然等於2。
		加volatile後,變量會生成內存屏障(即lock鎖住變量):
			 I.它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內
			存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
			II. 它會強制將對緩存的修改操作立即寫入主存;
			III. 如果是寫操作,它會導致其他CPU中對應的緩存行無效。
			//保證此變量對所有線程的可見性,當一條線程修改了這個變量的值,其他線程也會知道,Java的運算並非原子操作,導致volatile變量的運算在併發下是不安全的,即volatile只能保證可見性,所以通常還是需要synchronized/java.util.concurrent中的原子來保證原子性
		注:long double 64位數據不具有原子性,但是jvm爲其讀寫操作進行了原子性處理,無需爲long double 添加volatile
原子性:
		定義: 即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
		原子性是拒絕多線程操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。簡而言之,在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
		a. 基本類型的讀取和賦值操作,且賦值必須是數字賦值給變量,變量之間的相互賦值不是原子性操作。
		b.所有引用reference的賦值操作
		c.java.concurrent.Atomic.* 包中所有類的一切操作
可見性:
		定義:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
		在多線程環境下,一個線程對共享變量的操作對其他線程是不可見的。Java提供了volatile來保證可見性,當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會立即被更新到主內存中,其他線程讀取共享變量時,會直接從主內存中讀取。當然,synchronize和Lock都可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
有序性:
		定義:即程序執行的順序按照代碼的先後順序執行。
		synchronized:保證可見性,對一個變量執行unclock之前,必須先把此變量同步回主內存中
		final:被final修飾的變量在構造器中初始化完成,但構造器沒有把this的引用傳遞出去,在其他線程中可以看見final字段的值
		有序性:在當前線程內,所有的操作都是有序的,但從當前線程中觀察另一個線程,所有的操作都是無序的
		JVM的線程實現基於操作系統,windows,linux使用一對一的線程模型; 線程調度模式使用搶佔式,自動完成
	程序次序規則:
		一個線程內,按照代碼順序(控制流順序及邏輯順序),前面的代碼比後面的代碼先行發生
	管理鎖定規則:
		一個unclock操作先行發生於後面對於同一個鎖的lock操作
	volatile變量規則:
		對於一個volatile變量的寫操作先行發生於後面對這個變量的讀操作//先write進主內存,另一線程再read進工作內存
	線程啓動規則:
		Thread對象的start()方法先行發生於此線程的每一個動作
	線程終止規則:
		線程中所有操作都先行發生於對此線程的終止檢測,通過Thread.join()方法結束,Thread().isAlive的返回值手段檢測線程已經終止執行
	線程中斷規則:
		對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷時間的發生,可以通過Thread.interrupt()方法檢測是否有中斷髮生
	對象終結規則:
		一個對象的初始化完成先行發生於它的finalize()的開始
	傳遞性:
		如果操作A先行發生於操作B,操作B先行發生於操作C,說明操作A先行發生於操作C
	解決線程安全:
		1.互斥同步:阻塞式同步;保證共享數據同一時刻只能被一條線程使用,使用重量級synchronize操作,在對線程狀態喚醒,睡眠需要到內核中進行, 導致狀態轉移操作的時間比代碼一般時間較長; 使用ReentrantLock實現同步,相比於synchronize他多增加: 線程等待鎖時間過長自動中斷,多個線程等待同一個鎖,獲得鎖的順序按照申請鎖的順序而定,可以鎖住多個條件; 但是在性能上還是使用synchronize
		2.非阻塞式同步: 共享數據發生衝突,不需要將線程掛起,而是採取補償措施,對修改的數據進行修正
		3.可重入代碼: 執行線程中判斷執行結果是否滿足預期,不滿足執行其他代碼
		4.線程本地儲存:消費者-工廠模式, 阻塞隊列
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章