JVM内存结构和Java内存模型别再傻傻分不清了
JVM內(nèi)存結(jié)構(gòu)和Java內(nèi)存模型都是面試的熱點(diǎn)問題,名字看感覺都差不多,網(wǎng)上有些博客也都把這兩個概念混著用,實(shí)際上他們之間差別還是挺大的。
通俗點(diǎn)說,JVM內(nèi)存結(jié)構(gòu)是與JVM的內(nèi)部存儲結(jié)構(gòu)相關(guān),而Java內(nèi)存模型是與多線程編程相關(guān),本文針對這兩個總是被混用的概念展開講解。
文章目錄
- JVM內(nèi)存結(jié)構(gòu)
- JVM構(gòu)成
- JVM內(nèi)存結(jié)構(gòu)
- 程序計數(shù)器
- 虛擬機(jī)棧
- 本地方法棧
- 堆
- 方法區(qū)
- GC垃圾回收機(jī)制
- 1. 垃圾判別方法
- 引用計數(shù)算法
- 可達(dá)性分析算法
- 2. 垃圾回收算法
- 標(biāo)記清除法
- 標(biāo)記整理法
- 復(fù)制算法
- 3. 分代垃圾回收機(jī)制
- 4. 垃圾回收器
- 5. 四種引用
- 類加載
- 類加載器的分類
- 類加載過程
- LoadClass和forName的區(qū)別
- 雙親委派機(jī)制
- 自定義類加載器
- 反射機(jī)制
- 反射的定義
- 反射的常用場景
- 反射相關(guān)的類
- Class類:
- Filed類
- Method類
- Constructor類
- 案例
- Java內(nèi)存模型
- 什么是Java內(nèi)存模型(JMM)
- 為什么會有Java內(nèi)存模型
- 原子性
- 可見性
- 有序性(重排序)
- volatile
- happens-before規(guī)則
JVM內(nèi)存結(jié)構(gòu)
JVM構(gòu)成
說到JVM內(nèi)存結(jié)構(gòu),就不會只是說內(nèi)存結(jié)構(gòu)的5個分區(qū),而是會延展到整個JVM相關(guān)的問題,所以先了解下JVM的構(gòu)成。
- Java源代碼編譯成Java Class文件后通過類加載器ClassLoader加載到JVM中
- 類存放在方法區(qū)中
- 類創(chuàng)建的對象存放在堆中
- 堆中對象的調(diào)用方法時會使用到虛擬機(jī)棧,本地方法棧,程序計數(shù)器
- 方法執(zhí)行時每行代碼由解釋器逐行執(zhí)行
- 熱點(diǎn)代碼由JIT編譯器即時編譯
- 垃圾回收機(jī)制回收堆中資源
- 和操作系統(tǒng)打交道需要調(diào)用本地方法接口
JVM內(nèi)存結(jié)構(gòu)
程序計數(shù)器
(通過移位寄存器實(shí)現(xiàn))
- 程序計數(shù)器是線程私有的,每個線程單獨(dú)持有一個程序計數(shù)器
- 程序計數(shù)器不會內(nèi)存溢出
虛擬機(jī)棧
-
棧:線程運(yùn)行需要的內(nèi)存空間
-
棧幀:每一個方法運(yùn)行需要的內(nèi)存(包括參數(shù),局部變量,返回地址等信息)
-
每個線程只有一 個活動棧幀(棧頂?shù)臈?#xff09;,對應(yīng)著正在執(zhí)行的代碼
-
常見問題解析
-
垃圾回收是否涉及棧內(nèi)存:不涉及,垃圾回收只涉及堆內(nèi)存
-
棧內(nèi)存分配越大越好嗎:內(nèi)存一定時,棧內(nèi)存越大,線程數(shù)就越少,所以不應(yīng)該過大
-
方法內(nèi)的局部變量是否是線程安全的:
- 普通局部變量是安全的
- 靜態(tài)的局部變量是不安全的
- 對象類型的局部變量被返回了是不安全的
- 基本數(shù)據(jù)類型局部變量被返回時安全的
- 參數(shù)傳入對象類型變量是不安全的
- 參數(shù)傳入基本數(shù)據(jù)類型變量時安全的
-
棧內(nèi)存溢出(StackOverflowError)
-
棧幀過多
- 如遞歸調(diào)用沒有正確設(shè)置結(jié)束條件
-
棧幀過大
- json數(shù)據(jù)轉(zhuǎn)換 對象嵌套對象 (用戶類有部門類屬性,部門類由用戶類屬性)
-
線程運(yùn)行診斷
-
CPU占用過高(定位問題)
- ‘top’命令獲取進(jìn)程編號,查找占用高的進(jìn)程
- ‘ps H -eo pid,tid,%cpu | grep 進(jìn)程號’ 命令獲取線程的進(jìn)程id,線程id,cpu占用
- 將查看到的占用高的線程的線程號轉(zhuǎn)化成16進(jìn)制的數(shù) :如6626->19E2
- ‘ jstack 進(jìn)程id ’獲取進(jìn)程棧信息, 查找‘nid=0X19E2’的線程
- 問題線程的最開始‘#數(shù)字’表示出現(xiàn)問題的行數(shù),回到代碼查看
-
程序運(yùn)行很長時間沒有結(jié)果(死鎖問題)
- ‘ jstack 進(jìn)程id ’獲取進(jìn)程棧信息
- 查看最后20行左右有無‘Fount one Java-level deadlock’
- 查看下面的死鎖的詳細(xì)信息描述和問題定位
- 回到代碼中定位代碼進(jìn)行解決
-
-
-
本地方法棧
- 本地方法棧為虛擬機(jī)使用到的 Native 方法服務(wù)
- Native 方法是 Java 通過 JNI 直接調(diào)用本地 C/C++ 庫,可以認(rèn)為是 Native 方法相當(dāng)于 C/C++ 暴露給 Java 的一個接口
- 如notify,hashcode,wait等都是native方法
堆
-
通過new關(guān)鍵字創(chuàng)建的對象都會使用堆內(nèi)存
-
堆是線程共享的
-
堆中有垃圾回收機(jī)制
-
堆內(nèi)存溢出(OutOfMemoryError)
- 死循環(huán)創(chuàng)建對象
-
堆內(nèi)存診斷
-
命令行方式
- ‘jps’獲取運(yùn)行進(jìn)程號
- ‘jmap -heap 進(jìn)程號’查看當(dāng)前時刻的堆內(nèi)存信息
-
jconsole
- 命令行輸入jconsole打開可視化的界面連接上進(jìn)程
- 可視化的檢測連續(xù)的堆內(nèi)存信息
-
jvisualvm
- 命令行輸入jvisualvm打開可視化界面選擇進(jìn)程
- 可視化的查看堆內(nèi)存信息
-
方法區(qū)
- 方法區(qū)只是一種概念上的規(guī)范,具體的實(shí)現(xiàn)各種虛擬機(jī)和不同版本不相同
- HotSpot1.6 使用永久代作為方法區(qū)的實(shí)現(xiàn)
- HotSpot1.8使用本地內(nèi)存的元空間作為方法區(qū)的實(shí)現(xiàn)(但StringTable還是放在堆中)
- 常見問題
-
StringTable特性
-
常量池中的字符串僅是字符,第一次使用時才變?yōu)閷ο?/p>
-
利用串池機(jī)制,避免重復(fù)創(chuàng)建字符串
-
字符串常量拼接原理是StringBuilder(1.8)
-
字符串常量拼接原理是編譯器優(yōu)化
-
StringTable在1.6中存放在永久代,在1.8中存放在堆空間
-
intern方法主動將串池中沒有的字符串對象放入串池
-
1.8中:嘗試放入串池,如果有就不放入,只返回一個引用;如果沒有就放入串池,同時返回常量池中對象引用
-
1.6中:嘗試放入串池,如果有就不放入,只返回一個引用;如果沒有就復(fù)制一個放進(jìn)去(本身不放入),同時返回常量池中的對象引用
-
字符串常量池分析(1.8環(huán)境)
String s1 = "a"; String s2 = "b"; String s3 = "a"+"b"; String s4 = s1+s2; String s5 = "ab"; String s6 = s4.intern();System.out.println(s3==s4);// s3在常量池中,s4在堆上(intern嘗試s4放入常量池,因?yàn)閍b存在了就拒絕放入返回ab引用給s6,s4還是堆上的) System.out.println(s3==s5);// s3在常量池中,s4也在常量池中(字符串編譯期優(yōu)化) System.out.println(s3==s6);// s3在常量池中,s6是s4的intern返回常量池中ab的引用,所以也在常量池中String x2 = new String("c")+new String("d"); String x1 = "cd"; x2.intern();System.out.println(x1==x2);//x2調(diào)用intern嘗試放入常量池,但常量池中已經(jīng)有cd了,所以只是返回一個cd的引用,而x2還是堆上的引用
-
-
-
JVM調(diào)優(yōu)三大參數(shù)(如: java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar)
- -Xss:規(guī)定了每個線程虛擬機(jī)棧的大小(影響并發(fā)線程數(shù)大小)
- -Xms:堆大小的初始值(超過初始值會擴(kuò)容到最大值)
- -Xmx:堆大小的最大值(通常初始值和最大值一樣,因?yàn)閿U(kuò)容會導(dǎo)致內(nèi)存抖動,影響程序運(yùn)行穩(wěn)定性)
-
JVM內(nèi)存結(jié)構(gòu)中堆和棧的區(qū)別
- 管理方式:棧自動釋放,堆需要GC
- 空間大小:棧比堆小
- 碎片:棧產(chǎn)生的碎片遠(yuǎn)少于堆
- 分配方式:棧支持靜態(tài)分配和動態(tài)分配,堆只支持動態(tài)分配
- 效率:棧的效率比堆高
-
GC垃圾回收機(jī)制
1. 垃圾判別方法
引用計數(shù)算法
-
判斷對象的引用數(shù)量來決定對象是否可以被回收
-
每個對象實(shí)例都有一個引用計數(shù)器,被引用則+1,完成引用則-1
-
優(yōu)點(diǎn):執(zhí)行效率高,程序執(zhí)行受影響小
-
缺點(diǎn):無法檢測出循環(huán)引用的情況,導(dǎo)致內(nèi)存泄露
可達(dá)性分析算法
-
Java虛擬機(jī)中的垃圾回收器采用可達(dá)性分析來探索所有存活對象
-
掃描堆中的對象,看是否能沿著GC Root對象為起點(diǎn)的引用鏈找到該對象,找不到則可以回收
-
哪些對象可以作為GC Root
-
通過System Class Loader或者Boot Class Loader加載的class對象,通過自定義類加載器加載的class不一定是GC Root
-
虛擬機(jī)棧中的引用的對象
-
本地方法棧中JNI(natice方法)的引用的對象
-
方法區(qū)中的常量引用的對象
-
方法區(qū)中的類靜態(tài)屬性引用的對象
-
處于激活狀態(tài)的線程
-
正在被用于同步的各種鎖對象
-
GC保留的對象,比如系統(tǒng)類加載器等。
-
2. 垃圾回收算法
標(biāo)記清除法
- 標(biāo)記沒有被GC Root引用的對象
- 清除被標(biāo)記位置的內(nèi)存
- 優(yōu)點(diǎn):處理速度快
- 缺點(diǎn):造成空間不連續(xù),產(chǎn)生內(nèi)存碎片
標(biāo)記整理法
- 標(biāo)記沒有被GC Root引用的對象
- 整理被引用的對象
- 優(yōu)點(diǎn):空間連續(xù),沒有內(nèi)存碎片
- 缺點(diǎn):整理導(dǎo)致效率較低
復(fù)制算法
- 分配同等大小的內(nèi)存空間
- 標(biāo)記被GC Root引用的對象
- 將引用的對象連續(xù)的復(fù)制到新的內(nèi)存空間
- 清除原來的內(nèi)存空間
- 交換FROM空間和TO空間
- 優(yōu)點(diǎn):空間連續(xù),沒有內(nèi)存碎片
- 缺點(diǎn):占用雙倍的內(nèi)存空間
3. 分代垃圾回收機(jī)制
-
分代垃圾回收流程
- 對象首先分配在伊甸園區(qū)域
- 新生代空間不足時,觸發(fā)Minor GC,伊甸園和from存活的對象使用【復(fù)制算法】復(fù)制到to中,存活的對象年齡加一,并且交換from區(qū)和to區(qū)
- Minor GC會引發(fā)Stop the world(STW)現(xiàn)象,暫停其他用戶的線程。垃圾回收結(jié)束后,用戶線程才恢復(fù)運(yùn)行
- 當(dāng)對象壽命超過閾值時,會晉升至老年代,最大壽命是15(4位二進(jìn)制)
- 當(dāng)老年代空間不足,會先嘗試觸發(fā)Minor GC,如果之后空間仍不足,會觸發(fā)Full GC(STW時間更長,老年代可能使用標(biāo)簽清除或標(biāo)記整理算法)
- 當(dāng)存放大對象新生代放不下而老年代可以放下,大文件會直接晉升到老年代
- 當(dāng)存放大對象新生代和老年代都放不下時,拋出OOM異常
-
默認(rèn)堆內(nèi)存分配
- 新生代占1/3,老年代占2/3
- -XX:NewRatio:老年代和年輕代內(nèi)存大小的比例
- 新生代中按8 1 1進(jìn)行分配,兩個幸存區(qū)大小需要保持一致
- -XX:SurvivorRatio: Eden和Survivor的比值,默認(rèn)是8(8:1)
-
GC相關(guān)VM參數(shù)
4. 垃圾回收器
-
安全點(diǎn)(SafePoint)
-
分析過程中對象引用關(guān)系不會發(fā)生改變的點(diǎn)
-
產(chǎn)生安全點(diǎn)的地方:
- 方法調(diào)用
- 循環(huán)跳轉(zhuǎn)
- 異常跳轉(zhuǎn)
-
安全點(diǎn)的數(shù)量應(yīng)該設(shè)置適中
-
-
串行(SerialGC)
- 單線程的垃圾回收器
- 堆內(nèi)存較小,CPU核數(shù)少,適合個人電腦
- SerialGC收集器 (-XX:+UseSerialGC 復(fù)制算法) Client模式下默認(rèn)的年輕代收集器
- SerialGC Old收集器 (-XX:+UseSerialOldGC 標(biāo)記-整理算法)Client模式下默認(rèn)的老年代收集器
-
吞吐量優(yōu)先(ParallelGC)
- 多線程的垃圾回收器
- 堆內(nèi)存較大,多核CPU,適合服務(wù)器
- 盡可能讓單位時間內(nèi)STW暫停時間最短(吞吐量=運(yùn)行代碼時間/(運(yùn)行代碼時間+垃圾回收時間))
- 并行的執(zhí)行
- ParallelGC收集器(-XX:+UseParallelGC 復(fù)制算法) Server模式下默認(rèn)的年輕代垃圾回收器
- ParallelGC Old收集器(-XX:+UseParallelOldGC 復(fù)制算法)
-
響應(yīng)時間優(yōu)先(CMS -XX:+UseConcMarkSweepGC 標(biāo)記清除算法)
-
多線程的垃圾回收器
-
堆內(nèi)存較大,多核CPU,Server模式下默認(rèn)的老年代垃圾回收器
-
盡可能讓單次STW暫停時間最短
-
部分時期內(nèi)可以并發(fā)執(zhí)行
-
執(zhí)行流程
- 初始標(biāo)記:stop-the-world
- 并發(fā)標(biāo)記:并發(fā)追溯標(biāo)記,程序不會停頓
- 并發(fā)預(yù)清理:查找執(zhí)行并發(fā)標(biāo)記階段從年輕代晉升到老年代的對象
- 重新標(biāo)記:暫停虛擬機(jī),掃描CMS堆中的剩余對象
- 并發(fā)清理:清理垃圾對象,程序不會停頓
- 并發(fā)重置:重置CMS收集器的數(shù)據(jù)結(jié)構(gòu)
-
-
G1(-XX:+UseG1GC 復(fù)制+標(biāo)記清除算法)
- G1l垃圾回收器簡介
- 定義:Garbage First (2017 jdk9 默認(rèn))
- 特點(diǎn)
- 并發(fā)和并行
- 分代收集
- 空間整合
- 可預(yù)測的停頓
- 使用場景
- 同時注重吞吐量和低延遲,默認(rèn)暫停目標(biāo)是200ms
- 超大堆內(nèi)存,會將整個堆劃分為多個大小相等的Region(新生代和老年代不再物理隔離了)
- 整體上是標(biāo)記整理算法,兩個區(qū)域之間是復(fù)制算法
-
垃圾回收階段
-
新生代垃圾收集
- 會發(fā)生STW
-
新生代垃圾收集+并發(fā)標(biāo)記
- 在Young GC時會進(jìn)行GC Root的初始標(biāo)記
- 老年代占用堆內(nèi)存空間比例達(dá)到閾值時,進(jìn)行并發(fā)標(biāo)記(不會STW)
-
混合收集,對新生代,幸存區(qū)和老年代都進(jìn)行收集
- 最終標(biāo)記,會STW
- 拷貝存活,會STW
- 三種階段循環(huán)交替
-
-
Full GC
-
SerialGC
- 新生代內(nèi)存不足發(fā)生的垃圾收集:minor GC
- 老年代內(nèi)存不足發(fā)生的垃圾收集:full GC
-
ParallelGC
- 新生代內(nèi)存不足發(fā)生的垃圾收集:minor GC
- 老年代內(nèi)存不足發(fā)生的垃圾收集:full GC
-
CMS
-
新生代內(nèi)存不足發(fā)生的垃圾收集:minor GC
-
老年代內(nèi)存不足
- 并發(fā)收集成功:并發(fā)的垃圾收集
- 并發(fā)收集失敗:串行的full GC
-
-
G1
-
新生代內(nèi)存不足發(fā)生的垃圾收集:minor GC
-
老年代內(nèi)存不足,達(dá)到閾值時進(jìn)入并發(fā)標(biāo)記和混合收集階段
- 如果回收速度>新產(chǎn)生垃圾的速度 :并發(fā)垃圾收集
- 如果回收速度<新產(chǎn)生垃圾的速度:串行的full GC
-
-
5. 四種引用
-
強(qiáng)引用
- 最常見的對象:通過new關(guān)鍵字創(chuàng)建,通過GC Root能找到的對象。
- 當(dāng)所有的GC Root都不通過【強(qiáng)引用】引用該對象時,對象才能被垃圾回收
-
軟引用
-
僅有【軟引用】引用該對象時,在垃圾回收后,內(nèi)存仍不足時會再次發(fā)起垃圾回收,回收軟引用對象
-
可以配合引用隊列來釋放軟引用自身
-
創(chuàng)建一個軟引用:SoftReference ref = new SoftReference<>(new Object());
-
軟引用被回收后,仍然還保留一個null,如將軟引用加入集合,回收后遍歷集合仍然還存在一個null
- 解決:使用引用隊列,軟引用關(guān)聯(lián)的對象被回收時,軟引用自身會被加入到引用隊列中,通過queue.poll()取得對象進(jìn)行刪除
- 創(chuàng)建一個而引用隊列:ReferenceQueue queue = new ReferenceQueue<>();
- 創(chuàng)建加入了引用隊列的軟引用:SoftReference ref = new SoftReference<>(new Object(),queue);
-
-
弱引用
- 僅有【弱引用】引用該對象時,在垃圾回收時,無論內(nèi)存是否充足,都會回收弱引用對象
- 可以配合引用隊列來釋放弱引用自身
- 創(chuàng)建一個弱引用:WeakReference ref = new WeakReference<>(new Object());
- 引用隊列使用同軟引用
-
虛引用
- 必須配合引用隊列使用,主要配合ByteBuffer使用,被引用對象回收時,會將【虛引用】入隊,由Reference Hanler線程調(diào)用虛引用相關(guān)方法釋放【直接內(nèi)存】(unsafe類中方法)
-
終結(jié)器引用
- 無需手動編碼,但其內(nèi)部配合引用隊列使用,在垃圾回收時,終結(jié)器引用隊列入隊(引用對象暫未回收),再由Finalizer線程通過終結(jié)器引用找到被引用對象并調(diào)用他的finalize方法,第二次gc時回收被引用對象
類加載
類加載器的分類
類加載過程
-
加載
- 通過ClassLoader加載Class文件字節(jié)碼,生成Class對象
-
鏈接
-
校驗(yàn):檢查加載的的Class的正確性和安全性
-
準(zhǔn)備:為類變量分配存儲空間并設(shè)置類變量初始值
-
解析:JVM將常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用
-
-
初始化
- 執(zhí)行類變量賦值和靜態(tài)代碼塊
LoadClass和forName的區(qū)別
- Class.ForName得到的class是已經(jīng)初始化完成的
- ClassLoader.loadClass得到的class是還沒有鏈接的
雙親委派機(jī)制
- 什么是雙親委派機(jī)制
- 當(dāng)某個類加載器需要加載某個.class文件時,它首先把這個任務(wù)委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
- 為什么要使用雙親委派機(jī)制
- 防止重復(fù)加載同一個.class文件,通過委托去向上級問,加載過了就不用加載了。
- 保證核心.class文件不會被串改,即使篡改也不會加載,即使加載也不會是同一個對象,因?yàn)椴煌虞d器加載同一個.class文件也不是同一個class對象,從而保證了class執(zhí)行安全
自定義類加載器
-
需求場景
- 想要加載非classpath的隨意路徑的類文件
- 通過接口來使用實(shí)現(xiàn),希望解耦合
-
步驟
- 繼承Classloader父類
- 遵循雙親委派機(jī)制,重寫findClass方法(不能重寫loadClass,重寫了就不符合雙親委派了)
- 讀取類的字節(jié)碼
- 調(diào)用父類的defineClass方法加載類
- 使用者調(diào)用類加載的loadClass方法
-
案例演示
創(chuàng)建自定義類加載器
public class MyClassLoader extends ClassLoader {private String path;private String classLoaderName;public MyClassLoader(String path, String classLoaderName) {this.path = path;this.classLoaderName = classLoaderName;}//用于尋找類文件@Overridepublic Class findClass(String name) {byte[] b = loadClassData(name);return defineClass(name, b, 0, b.length);}//用于加載類文件private byte[] loadClassData(String name) {name = path + name + ".class";try (InputStream in = new FileInputStream(new File(name));ByteArrayOutputStream out = new ByteArrayOutputStream();) {int i = 0;while ((i = in.read()) != -1) {out.write(i);}return out.toByteArray();} catch (Exception e) {e.printStackTrace();}return null;} }調(diào)用自定義類加載器加載類
public class MyClassLoaderChecker {public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {MyClassLoader m = new MyClassLoader("C:\\Users\\73787\\Desktop\\","myClassLoader");Class<?> c = m.loadClass("Robot");System.out.println(c.getClassLoader());c.newInstance();} }反射機(jī)制
反射的定義
JAVA反射機(jī)制是在運(yùn)行狀態(tài)中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調(diào)用它的任意方法和屬性;這種動態(tài)獲取信息以及動態(tài)調(diào)用對象方法的功能稱為java語言的反射機(jī)制。
反射的常用場景
第三方應(yīng)用開發(fā)過程中,會需要某個類的某個成員變量、方法或是屬性是私有的或者只對系統(tǒng)應(yīng)用開放,就可以通過Java的反射機(jī)制來獲取所需的私有成員或者方法
反射相關(guān)的類
Class類:
代表類的實(shí)體,在運(yùn)行的Java應(yīng)用程序中表示類和接口
- 獲得類的方法
- 獲得類中屬性的方法
- 獲得類中方法的方法
- 獲取類中構(gòu)造器的方法
Filed類
Filed代表類的成員變量(屬性)
Method類
Constructor類
案例
定義一個Robot類
public class Robot {//私有屬性private String name;//公有方法public void sayHi(String hello){System.out.println(hello+" "+name);}//私有方法private String thorwHello(String tag){return "hello "+tag;} }編寫一個反射應(yīng)用類,針對私有的屬性和方法必須設(shè)置setAccessible(true)才能進(jìn)行訪問
public class ReflectSample {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {//加載類Class<?> rc = Class.forName("leetcode.Robot");//獲取類實(shí)例Robot r = (Robot)rc.newInstance();//打印類名System.out.println(rc.getName());//加載一個私有方法Method getHello = rc.getDeclaredMethod("thorwHello",String.class);getHello.setAccessible(true);Object bob = getHello.invoke(r, "bob");System.out.println(bob);//加載一個公有方法Method sayHi = rc.getMethod("sayHi",String.class);Object welcome = sayHi.invoke(r,"welcome");//加載一個私有屬性Field name = rc.getDeclaredField("name");name.setAccessible(true);name.set(r,"tom");sayHi.invoke(r,"welcome");} }Java內(nèi)存模型
什么是Java內(nèi)存模型(JMM)
- 通俗來說,JMM是一套多線程讀寫共享數(shù)據(jù)時,對數(shù)據(jù)的可見性,有序性和原子性的規(guī)則
為什么會有Java內(nèi)存模型
JVM實(shí)現(xiàn)不同會造成“翻譯”的效果不同,不同CPU平臺的機(jī)器指令有千差萬別,無法保證同一份代碼并發(fā)下的效果一致。所以需要一套統(tǒng)一的規(guī)范來約束JVM的翻譯過程,保證并發(fā)效果一致性
原子性
- 什么是原子性
- 原子性指一系列的操作,要么全部執(zhí)行成功,要么全部不執(zhí)行,不會出現(xiàn)執(zhí)行一半的情況,是不可分的。
- 原子性怎么實(shí)現(xiàn)
- 使用synchronized或Lock加鎖實(shí)現(xiàn),保證任一時刻只有一個線程訪問該代碼塊
- 使用原子操作
- Java中的原子操作有哪些
- 除long和double之外的基本類型的賦值操作(64位值,當(dāng)成兩次32位的進(jìn)行操作)
- 所有引用reference的賦值操作
- java.concurrent.Atomic.*包中所有類的原子操作
- 創(chuàng)建對象的過程是否是原子操作(常應(yīng)用于雙重檢查+volatile創(chuàng)建單例場景)
- 創(chuàng)建對象實(shí)際上有3個步驟,并不是原子性的
- 創(chuàng)建一個空對象
- 調(diào)用構(gòu)造方法
- 創(chuàng)建好的實(shí)例賦值給引用
- 創(chuàng)建對象實(shí)際上有3個步驟,并不是原子性的
可見性
- 什么是可見性問題
- 可見性指的是當(dāng)一個線程修改了某個共享變量的值,其他線程是否能夠馬上得知這個修改的值。
- 為什么會有可見性問題、
- 對于單線程程序來說,可見性是不存在的,因?yàn)槲覀冊谌魏我粋€操作中修改了某個變量的值,后續(xù)的操作中都能讀取這個變量值,并且是修改過的新值。
- 對于多線程程序而言。由于線程對共享變量的操作都是線程拷貝到各自的工作內(nèi)存進(jìn)行操作后才寫回到主內(nèi)存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內(nèi)存時,另外一個線程B又對主內(nèi)存中同一個共享變量x進(jìn)行操作,但此時A線程工作內(nèi)存中共享變量x對線程B來說并不可見,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問題
- 如何解決可見性問題
- 解決方法1:加volatile關(guān)鍵字保證可見性。當(dāng)一個共享變量被volatile修飾時,它會保證修改的值立即被其他的線程看到,即修改的值立即更新到主存中,當(dāng)其他線程需要讀取時,它會去內(nèi)存中讀取新值
- 解決方法2:使用synchronized和Lock保證可見性。因?yàn)樗鼈兛梢员WC任一時刻只有一個線程能訪問共享資源,并在其釋放鎖之前將修改的變量刷新到內(nèi)存中
- 案例
循環(huán)創(chuàng)建兩類線程,一個線程用于做值的交換,一個線程用于打印值
比較直觀的三種結(jié)果
- 打印線程先執(zhí)行:b = 2, a = 1
- 交換線程先執(zhí)行:b = 3, a = 3
- 交換線程執(zhí)行到一半就切出去打印了,只執(zhí)行了a=3賦值操作:b = 2 , a =3
實(shí)際上除了很容易想到的三種情況外還有一種特殊情況:b = 3 , a = 1
- 這種情況就是可見性問題
- a的值在線程1(執(zhí)行交換線程)的本地緩存中進(jìn)行了更新,但是并沒有同步到共享緩存,而b的值成功的更新到了共享緩存,導(dǎo)致線程2(執(zhí)行打印線程)從共享緩存中獲取到的數(shù)據(jù)并不是實(shí)時的最新數(shù)據(jù)
-
有序性(重排序)
- 什么是重排序
- 在線程內(nèi)部的兩行代碼的實(shí)際執(zhí)行順序和代碼在Java文件中的邏輯順序不一致,代碼指令并不是嚴(yán)格按照代碼語句順序執(zhí)行的,他們的順序被改變了,這就是重排序。
- 重排序的意義
- JVM能根據(jù)處理器特性(CPU多級緩存系統(tǒng)、多核處理器等)適當(dāng)?shù)膶C(jī)器指令進(jìn)行重排序,使機(jī)器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器性能。
- 案例計算: a = 3; b = 2; a = a + 1; 重排序優(yōu)化前的instructionsload a set to 3 store 3load b set to 2 store bload a set to 4 store a經(jīng)過重排序處理后load a set to 3 set to 4 store aload b set to 2 store b上述少了兩個指令,優(yōu)化了性能
- 重排序的3種情況
- 編譯器優(yōu)化( JVM,JIT編輯器等): 編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序
- 指令級并行的重排序:現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序: 由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
volatile
-
什么是volatile
- volatile是一種同步機(jī)制,比synchronized或者Lock相關(guān)類更輕量級,因?yàn)槭褂胿olacile并不會發(fā)生上下文切換等開銷很大的行為
- volatile是無鎖的,并且只能修飾單個屬性
-
什么時候適合用vilatile
- 一個共享變量始終只被各個線程賦值,沒有其他操作
- 作為刷新的觸發(fā)器,引用刷新之后使修改內(nèi)容對其他線程可見(如CopyOnRightArrayList底層動態(tài)數(shù)組通過volatile修飾,保證修改完成后通過引用變化觸發(fā)volatile刷新,使其他線程可見)
-
volatile的作用
- 可見性保障:修改一個volatile修飾變量之后,會立即將修改同步到主內(nèi)存,使用一個volatile修飾的變量之前,會立即從主內(nèi)存中刷新數(shù)據(jù)。保證讀取的數(shù)據(jù)都是最新的,之前的修改都是可見的。
- 有序性保障(禁止指令重排序優(yōu)化):有volatile修飾的變量,賦值后多了一個“內(nèi)存屏障“( 指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)
-
volatile的性能
- volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
happens-before規(guī)則
什么是happens-before規(guī)則:前一個操作的結(jié)果可以被后續(xù)的操作獲取。
- 程序的順序性規(guī)則:在一個線程內(nèi)一段代碼的執(zhí)行結(jié)果是有序的。雖然還會指令重排,但是隨便它怎么排,結(jié)果是按照我們代碼的順序生成的不會變!
- volatile規(guī)則: 就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量,那么這個寫操作的結(jié)果一定對讀的這個線程可見。
- 傳遞性規(guī)則:happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那么A happens-before C。
- 管程鎖定規(guī)則:無論是在單線程環(huán)境還是多線程環(huán)境,對于同一個鎖來說,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結(jié)果!(管程是一種通用的同步原語,synchronized就是管程的實(shí)現(xiàn))
- 線程啟動規(guī)則:在主線程A執(zhí)行過程中,啟動子線程B,那么線程A在啟動子線程B之前對共享變量的修改結(jié)果對線程B可見。
- 線程終止規(guī)則: 在主線程A執(zhí)行過程中,子線程B終止,那么線程B在終止之前對共享變量的修改結(jié)果在線程A中可見。
- 線程中斷規(guī)則: 對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()檢測到是否發(fā)生中斷。
- 對象終結(jié)規(guī)則:一個對象的初始化的完成,也就是構(gòu)造函數(shù)執(zhí)行的結(jié)束一定 happens-before它的finalize()方法。
如果有用,點(diǎn)個贊再走吧
總結(jié)
以上是生活随笔為你收集整理的JVM内存结构和Java内存模型别再傻傻分不清了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 将datatable导出为excel的三
- 下一篇: Aspose.Java实现word转pd