JVM內(nèi)存結(jié)構(gòu) VS Java內(nèi)存模型 VS Java對象模型
- 整體方向
- JVM內(nèi)存結(jié)構(gòu)籍铁,和Java虛擬機的運行時區(qū)域有關(guān)。
- Java內(nèi)存模型耻煤,和Java的并發(fā)編程有關(guān)昼伴。
- Java對象模型,和Java對象在虛擬機中表現(xiàn)形式有關(guān)溯壶。
Java對象模型
- Java對象自身的存儲模型
- JVM會給這個類創(chuàng)建一個instanceKlass,保存在方法區(qū)及皂,用來在JVM層表示該Java類。
- 當我們在Java代碼中且改,使用new創(chuàng)建一個對象的時候验烧,JVM會創(chuàng)建一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數(shù)據(jù)又跛。
1.內(nèi)存模型
內(nèi)存模型:在特定的操作協(xié)議下碍拆,對特定的內(nèi)存或高速緩存進行讀寫訪問的過程抽象
Java內(nèi)存模型主要關(guān)注JVM中把把變量值存儲到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細節(jié)
- 所有變量(共享的)都存儲在主內(nèi)存中,每個線程都有自己工作內(nèi)存;工作內(nèi)存中保存該線程使用到的變量的主內(nèi)存副本拷貝
- 線程對變量的所有操作(讀慨蓝、寫)都應(yīng)該在工作內(nèi)存中完成
- 不同線程不能相互訪問工作內(nèi)存感混,交互數(shù)據(jù)要通過主內(nèi)存
為什么需要JMM
JMM即為JAVA 內(nèi)存模型(java memory model)。因為在不同的硬件生產(chǎn)商和不同的操作系統(tǒng)下礼烈,內(nèi)存的訪問邏輯有一定的差異弧满,結(jié)果就是當你的代碼在某個系統(tǒng)環(huán)境下運行良好,并且線程安全此熬,但是換了個系統(tǒng)就出現(xiàn)各種問題庭呜。Java內(nèi)存模型,就是為了屏蔽系統(tǒng)和硬件的差異犀忱,讓一套代碼在不同平臺下能到達相同的訪問結(jié)果
2.內(nèi)存間的交互操作
Java內(nèi)存模型規(guī)定了一些操作來實現(xiàn)內(nèi)存間交互,JVM會保存證它們是原子的
lock:鎖定,把變量標識為線程獨占疟赊,作用于主內(nèi)存變量
unlock:解鎖,把鎖定的變量釋放峡碉,別的線程才能使用近哟,作用于主內(nèi)存變量
read:讀取,把變量值從主內(nèi)存讀取到工作內(nèi)存
load:載入鲫寄,把read讀取到的值放入工作內(nèi)存的變量副本中
use:使用吉执,把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎
assign:賦值,把從執(zhí)行引擎接收到的值賦給工作內(nèi)存里面的變量
store:存儲地来,把工作內(nèi)存中一個變量的值傳遞到主內(nèi)存中
write:寫入戳玫,把store進來的數(shù)據(jù)存放到主內(nèi)的變量中
3.內(nèi)存間交互操作的規(guī)則
- 如果要把一個變量從主內(nèi)存中復制到工作內(nèi)存, 就需要按順序地執(zhí) 行read和load操作 , 如果把變量從工作內(nèi)存中同步回主內(nèi)存中, 就要按順序地執(zhí)行store和write操作. 但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行
- 不允許read和load、 store和write操作之一單獨出現(xiàn)
- 不允許一個線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中
- 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
- 一個新的變量只能在主內(nèi)存中誕生, 不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量未斑。 即就是對一個變量實施use和store操作之前 , 必須先執(zhí)行過了assign和load操作
- 一個變量在同一時刻只允許一條線程對 其進行l(wèi)ock操作 , 但lock操作可以被同一條線程重復執(zhí)行多次,多7. 次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖咕宿。lock和unlock必須成對出現(xiàn)
如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值, 在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值 - 如果一個變量事先沒有被lock操作鎖定 , 則不允許 對它執(zhí)行unlock操作 ; 也不允許去unlock一個被其他線程鎖定的變量
- 對一個變量執(zhí)行unlock操作之前 , 必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)
4.重排序
4.1重排序的代碼案例
/**
* 描述: 演示重排序的現(xiàn)象 “直到達到某個條件才停止”,測試小概率事件
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(3);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
two.start();
one.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
x,y 可以是0,1 1,0 1,1 不可能是0,0 但是輸出結(jié)果
省略...
第133601次(0,1)
第133602次(0,1)
第133603次(0,1)
第133604次(1,0)
第133605次(0,0)
Process finished with exit code 0
會出現(xiàn)x=0,y=0府阀?那是因為重排序發(fā)生了缆镣,4行代碼的執(zhí)行順序的其中一種可能:
y = a;
a = 1;
x = b;
b = 1;
重排序的例子、什么是重排序
在線程1內(nèi)部的兩行代碼的實際執(zhí)行順序和代碼在Java文件中的順序不一致试浙,代碼指令并不是嚴格按照代碼語句順序執(zhí)行的董瞻,它們的順序被改變了,這就是重排序田巴,這里被顛倒的是y=a和b=1這兩行語句钠糊。
4.2 重排序的好處:提高處理速度
- 對比重排序前后的指令
優(yōu)化
重排序明顯提高了處理速度
4.3 重排序的3種情況
- 編譯器優(yōu)化:
包括JVM,JIT編譯器等 - CPU指令重排:
就算編譯器不發(fā)生重排壹哺,CPU也可能對指令進行重排 - 內(nèi)存的
"重排序"
:
線程A的修改線程B卻看不到抄伍,引出可見性問題
程序執(zhí)行一段代碼,寫一個普通的共享變量管宵,其可能先被寫到緩沖區(qū)然后再被寫到主內(nèi)存逝慧,此時指令完成的時間就被推遲了∽牟冢看上去像"重排序"
5.可見性
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
而b其它線程恰好可見
可見性是什么?(通俗易懂)
a b 前面加上volatile 關(guān)鍵字云稚,解決可見性問題
volatile int a = 1;
volatile int b = 2;
為什么會有可見性問題隧饼?
CPU有多級緩存,導致讀的數(shù)據(jù)過期:
- 高速緩存的容量比主內(nèi)存小静陈,但是速度僅次于寄存器燕雁,所以在CPU和主內(nèi)存之間就多了Cache層;
- 線程間的對于共享變量的可見性問題不是直接由多核引起的鲸拥,而是由多緩存引起的拐格;
- 如果所有的核心只有一個緩存,那就不會出現(xiàn)內(nèi)存可見性問題刑赶;
但是每個核心都會將自己需要的數(shù)據(jù)讀到獨占的緩存中捏浊,數(shù)據(jù)修改后也是寫入到緩存中,然后等待刷入到主存中撞叨,所以會導致有些核心讀取的值是一個過期的值金踪。
總結(jié):
所有的共享變量存在于主內(nèi)存
中,每個線程有自己的本地內(nèi)存
牵敷,而且線程讀寫共享數(shù)據(jù)
也是通過工作內(nèi)存交換胡岔,但是存在延遲的情況,所以導致可見性問題的出現(xiàn)枷餐。