之前已經介紹了getstatic與getfield指令的彙編程式碼執行邏輯,這一篇介紹putstatic指令的執行邏輯,putfield將不再介紹,大家可以自己去研究,相信大家有這個實力。
putstatic指令為指定類的靜態域賦值。位元組碼指令的格式如下:
putstatic indexbyte1 indexbyte2
無符號數indexbyte1和indexbyte2構建為(indexbyte1<<8)|indexbyte2,該索引所指向的執行時常量池項應當是一個欄位的符號引用。
指令的模板定義如下:
def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte );
生成函式為putstatic(),函式的實現如下:
void TemplateTable::putstatic(int byte_no) {
putfield_or_static(byte_no, false);
}
呼叫TemplateTable::putfield_or_static()函式生成的機器指令對應的彙編程式碼如下:
0x00007fffe101ff90: movzwl 0x1(%r13),%edx
0x00007fffe101ff95: mov -0x28(%rbp),%rcx
0x00007fffe101ff99: shl $0x2,%edx
0x00007fffe101ff9c: mov 0x10(%rcx,%rdx,8),%ebx
0x00007fffe101ffa0: shr $0x18,%ebx
0x00007fffe101ffa3: and $0xff,%ebx
// 是否已經對putstatic指令進行了連線,如果已經連線,則跳轉到resolved
0x00007fffe101ffa9: cmp $0xb3,%ebx
0x00007fffe101ffaf: je 0x00007fffe102004e
呼叫TemplateTable::resolve_cache_and_index()函式生成如下彙編程式碼:
// 執行到這裡,說明欄位還沒有連線
0x00007fffe101ffb5: mov $0xb3,%ebx // 呼叫MacroAssembler::call_VM()函式生成如下程式碼,
// 用來執行InterpreterRuntime::resolve_get_put()函式
0x00007fffe101ffba: callq 0x00007fffe101ffc4
0x00007fffe101ffbf: jmpq 0x00007fffe1020042
0x00007fffe101ffc4: mov %rbx,%rsi
0x00007fffe101ffc7: lea 0x8(%rsp),%rax
0x00007fffe101ffcc: mov %r13,-0x38(%rbp)
0x00007fffe101ffd0: mov %r15,%rdi
0x00007fffe101ffd3: mov %rbp,0x200(%r15)
0x00007fffe101ffda: mov %rax,0x1f0(%r15)
0x00007fffe101ffe1: test $0xf,%esp
0x00007fffe101ffe7: je 0x00007fffe101ffff
0x00007fffe101ffed: sub $0x8,%rsp
0x00007fffe101fff1: callq 0x00007ffff66b567c
0x00007fffe101fff6: add $0x8,%rsp
0x00007fffe101fffa: jmpq 0x00007fffe1020004
0x00007fffe101ffff: callq 0x00007ffff66b567c
0x00007fffe1020004: movabs $0x0,%r10
0x00007fffe102000e: mov %r10,0x1f0(%r15)
0x00007fffe1020015: movabs $0x0,%r10
0x00007fffe102001f: mov %r10,0x200(%r15)
0x00007fffe1020026: cmpq $0x0,0x8(%r15)
0x00007fffe102002e: je 0x00007fffe1020039
0x00007fffe1020034: jmpq 0x00007fffe1000420
0x00007fffe1020039: mov -0x38(%rbp),%r13
0x00007fffe102003d: mov -0x30(%rbp),%r14
0x00007fffe1020041: retq 0x00007fffe1020042: movzwl 0x1(%r13),%edx
0x00007fffe1020047: mov -0x28(%rbp),%rcx
0x00007fffe102004b: shl $0x2,%edx
接下來生成的彙編程式碼如下:
// ---- resolved ---- // 執行如下程式碼時,表示欄位已經連線完成 0x00007fffe102004e: mov 0x20(%rcx,%rdx,8),%rbx
0x00007fffe1020053: mov 0x28(%rcx,%rdx,8),%eax
0x00007fffe1020057: mov 0x18(%rcx,%rdx,8),%rcx
0x00007fffe102005c: mov 0x70(%rcx),%rcx
0x00007fffe1020060: mov %eax,%edx
// 將_flags向右移動21位,判斷是否有volatile關鍵字
0x00007fffe1020062: shr $0x15,%edx
0x00007fffe1020065: and $0x1,%edx
// 將_flags向右移動28位,剩下TosState
0x00007fffe1020068: shr $0x1c,%eax // 如果不為btos,則跳轉到notByte
0x00007fffe102006b: and $0xf,%eax
0x00007fffe102006e: jne 0x00007fffe1020083 // btos // 將棧頂的值儲存到%eax中,這個值會寫入到對應的欄位中
0x00007fffe1020074: mov (%rsp),%eax
0x00007fffe1020077: add $0x8,%rsp
// %rcx為_java_mirror,%rbx為_f2,表示域在類中的偏移
0x00007fffe102007b: mov %al,(%rcx,%rbx,1)
0x00007fffe102007e: jmpq 0x00007fffe10201be // 跳轉到Done
// -- notByte --
// 如果不為atos,則跳轉到notObj
0x00007fffe1020083: cmp $0x7,%eax
0x00007fffe1020086: jne 0x00007fffe1020130 // atos
// 將棧頂的值彈出到%rax中,這個值將用來更新對應欄位的值
0x00007fffe102008c: pop %rax
// ...
// 將值更新到對應的欄位上
0x00007fffe1020115: mov %eax,(%rcx,%rbx,1)
// 其中的0x9是CardTableModRefBS::card_shift,shr表示邏輯右移,由於%rcx指向的是
// java.lang.Class例項的首地址,向右移後%rcx就算出了卡表的索引
0x00007fffe1020118: shr $0x9,%rcx
// 地址常量$0x7fffe07ff000表示卡表的基地址
0x00007fffe102011c: movabs $0x7fffe07ff000,%r10
// 將對應的卡表項標記為髒,其中常量0x0就表示是髒卡
0x00007fffe1020126: movb $0x0,(%r10,%rcx,1)
0x00007fffe102012b: jmpq
0x00007fffe10201be // 跳轉到Done
// ---- notObj ----
// 如果不為itos,那麼跳轉到notInt
0x00007fffe1020130: cmp $0x3,%eax
0x00007fffe1020133: jne 0x00007fffe1020148 // itos
0x00007fffe1020139: mov (%rsp),%eax
// 如果不為ctos,則跳轉到notChar
0x00007fffe102013c: add $0x8,%rsp
0x00007fffe1020140: mov %eax,(%rcx,%rbx,1)
0x00007fffe1020143: jmpq 0x00007fffe10201be // 跳轉到Done
0x00007fffe1020148: cmp $0x1,%eax
0x00007fffe102014b: jne 0x00007fffe1020161 // ctos
0x00007fffe1020151: mov (%rsp),%eax
0x00007fffe1020154: add $0x8,%rsp
0x00007fffe1020158: mov %ax,(%rcx,%rbx,1)
0x00007fffe102015c: jmpq 0x00007fffe10201be // 跳轉到Done
0x00007fffe1020161: cmp $0x2,%eax
0x00007fffe1020164: jne 0x00007fffe102017a // stos
0x00007fffe102016a: mov (%rsp),%eax
0x00007fffe102016d: add $0x8,%rsp
0x00007fffe1020171: mov %ax,(%rcx,%rbx,1)
0x00007fffe1020175: jmpq 0x00007fffe10201be // 跳轉到Done
0x00007fffe102017a: cmp $0x4,%eax
0x00007fffe102017d: jne 0x00007fffe1020194 // ltos
0x00007fffe1020183: mov (%rsp),%rax
0x00007fffe1020187: add $0x10,%rsp
0x00007fffe102018b: mov %rax,(%rcx,%rbx,1)
0x00007fffe102018f: jmpq 0x00007fffe10201be // 跳轉到Done
0x00007fffe1020194: cmp $0x5,%eax
0x00007fffe1020197: jne 0x00007fffe10201b0 // ftos
0x00007fffe102019d: vmovss (%rsp),%xmm0
0x00007fffe10201a2: add $0x8,%rsp
0x00007fffe10201a6: vmovss %xmm0,(%rcx,%rbx,1)
0x00007fffe10201ab: jmpq 0x00007fffe10201be // 跳轉到Done // dtos
0x00007fffe10201b0: vmovsd (%rsp),%xmm0
0x00007fffe10201b5: add $0x10,%rsp
0x00007fffe10201b9: vmovsd %xmm0,(%rcx,%rbx,1) // ---- Done ---- 0x00007fffe10201be: test %edx,%edx
0x00007fffe10201c0: je 0x00007fffe10201cb
0x00007fffe10201c6: lock addl $0x0,(%rsp) // ---- notVolatile ----
在如上程式碼中,最值得關注的2個點如下:
(1)更新引用欄位時,通過屏障將對應的卡表項標記為髒,這樣可在GC過程中掃描髒卡就可將活躍物件標記出來而不會造成遺漏;
(2)當欄位有volatile關鍵字修飾時,需要填寫lock指令字首,這個字首在之前介紹x86-64機器指令時沒有介紹過,這裡摘抄一下別人對此指令的介紹:
Intel手冊對 lock 字首的說明如下:
- 確保被修飾指令執行的原子性;
- 禁止該指令與前面和後面的讀寫指令重排序;
- 指令執行完後把寫緩衝區的所有資料重新整理到記憶體中(這樣這個指令之前的其他修改對所有處理器可見)。
在所有的 X86 CPU 上都具有鎖定一個特定記憶體地址的能力,當這個特定記憶體地址被鎖定後,它就可以阻止其他的系統匯流排讀取或修改這個記憶體地址。這種能力是通過 lock 指令字首再加上下面的彙編指令來實現的。當使用 lock 指令字首時,它會使 CPU 宣告一個 lock# 訊號,這樣就能確保在多處理器系統或多執行緒競爭的環境下互斥地使用這個記憶體地址。當指令執行完畢,這個鎖定動作也就會消失。
推薦閱讀:
第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法
第13篇-通過InterpreterCodelet儲存機器指令片段
第20篇-載入與儲存指令之ldc與_fast_aldc指令(2)
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM原始碼剖析系列文章!