尝试用HSDB分析JVM运行时内存理解Java多态实现机制

4 篇文章 0 订阅

测试代码

// 接口
package ziya;
public interface TestParent {
    void show();
}

package ziya;
public class Parent {
    public void say() {
        System.out.println("1#子牙");
    }
}

// 实现类 、 入口
package ziya;
public class TestDuotai extends Parent implements  TestParent {
    public static void main(String[] args) {
        TestParent obj = new TestDuotai();
        obj.show();
    }
    @Override
    public void show() {
        System.out.println("ziya");
    }
    public void say() {
        System.out.println("2#子牙");
    }
}

分析

分析字节码

main方法的字节码是下面重点分析的,见下节invokeinterface指令分析
实现类TestDuotai总共有4个方法:分别是默认的构造方法、main、show、say
此时断点在源代码第七行
实现类main方法的字节码

重点看下main方法的访问标志0x9,在HSDB分析时可以互相验证
实现类main方法的访问标志

重点看下实现类show方法的访问标志0x1,在HSDB分析时可以互相验证
实现类show方法的访问标志

重点看下接口类(在HSDB分析时可以互相验证)
总共有1个方法,
show方法的访问标志0x401,
接口的字节码

invokeinterface指令分析

参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokeinterface

指令格式:
invokeinterface
indexbyte1
indexbyte2
count
0

执行该指令时的操作数栈情况
..., objectref, [arg1, [arg2 ...]] →

The count operand is an unsigned byte that must not be zero. The objectref must be of type reference and must be followed on the operand stack by nargs argument values, where the number, type, and order of the values must be consistent with the descriptor of the resolved interface method. The value of the fourth operand byte must always be zero.
==objectref是什么?是实现了接口TestParent的类TestDuotai的一个对象(后面有HSDB验证)。objectref是怎么跑到操作数栈上面的?应该是上面的new指令产生作用的//TODO
======objectref必须是接口的引用
==操作数的个数一定不会为0。(因为即使上面的show方法没有声明参数,肯定有一个隐含的参数this指针:指向调用该方法的对象)
======this指针是怎么跑到操作数栈上面的,应该是上面的new指令产生作用的//TODO
==objectref后面必须跟着n个参数,这些参数的个数、类型、顺序必须和接口声明的方法参数一致
======invokeinterface 指令的第四个字节总是0。

上面截图看到的具体指令信息
invokeinterface #4 <ziya/TestParent.show> count 1
==可以看到操作数上面的操作数个数为1,即this指针:指向调用该方法的对象


下面一段描述就是多态的实现机制(具体方法调用逻辑)
Let C be the class of objectref. The actual method to be invoked is selected by the following lookup procedure:
	1.If C contains a declaration for an instance method with the same name and descriptor as the resolved method, then it is the method to be invoked.
	2.Otherwise, if C has a superclass, a search for a declaration of an instance method with the same name and descriptor as the resolved method is performed, starting with the direct superclass of C and continuing with the direct superclass of that class, and so forth, until a match is found or no further superclasses exist. If a match is found, then it is the method to be invoked.
	3.Otherwise, if there is exactly one maximally-specific method (§5.4.3.3) in the superinterfaces of C that matches the resolved method's name and descriptor and is not abstract, then it is the method to be invoked.
其实只要看第一段就好了:
实现类包含有一个和接口同样方法签名(方法名称、参数类型、顺序),就会调用该实例方法。后面会用HSDB验证。

分析JVM运行时

运行HSDB

“c:\Program Files\Java\jdk1.8.0_221\bin\java.exe” -cp .;“c:\Program Files\Java\jdk1.8.0_221\lib\sa-jdi.jar” sun.jvm.hotspot.HSDB

我们知道对象的方法调用,其实调用的是方法区中该对象对应的Class元数据的对应方法

先看一下断点时的main线程栈帧信息

(其实只能看到局部变量表)
hsdb> jseval jvm.threads[jvm.threads.length-1].frames[0].locals
{args=Object 0x000000076bfeda48, obj=Object 0x000000076bfeda58}
这里可以看到obj是TestDuotai的实例->结合main方法的字节码,也可以推导出执行invokeinterface指令时的objectref是TestDuotai的实例

hsdb> inspect 0x000000076bfeda58
instance of Oop for ziya/TestDuotai @ 0x000000076bfeda58 @ 0x000000076bfeda58 (size = 16)
_mark: 1
_metadata._compressed_klass: InstanceKlass for ziya/TestDuotai

看一看这个对象占用的16个字节的具体值
hsdb> mem 0x000000076bfeda58 2
0x000000076bfeda58: 0x0000000000000001 
0x000000076bfeda60: 0x00000000f800c405 

0xf800c405是指向该对象的Clazz对象的内存地址,因为默认开启了压缩指针,所以这里转换一下:
>>> hex(0xf800c405<<3)
'0x7c0062028'
可以看到和下一节的public class ziya.TestDuotai @0x00000007c0062028,一致(复习一下压缩指针)

查询实例代码3个类的Clazz对象

在这里插入图片描述
public class ziya.Parent @0x00000007c0061c88
public class ziya.TestDuotai @0x00000007c0062028
public abstract interface ziya.TestParent @0x00000007c0061aa0

分析TestDuotai clazz对象(重点)

通过以上分析,只要能够验证该clazz对象中有和接口一致的show方法,执行invokeinterface时就能找到该方法,并执行,完成多态(父类引用指向子类对象)
public class ziya.TestDuotai @0x00000007c0062028

hsdb> inspect 0x00000007c0062028
Type is InstanceKlass (size of 440)
juint Klass::_super_check_offset: 56
#public abstract interface ziya.TestParent @0x00000007c0061aa0,这个字段_secondary_super_cache指向接口
Klass* Klass::_secondary_super_cache: Klass @ 0x00000007c0061aa0
Array<Klass*>* Klass::_secondary_supers: Array<Klass*> @ 0x000000001bd4fe40
Klass* Klass::_primary_supers[0]: Klass @ 0x00000007c0000f28
oop Klass::_java_mirror: Oop for java/lang/Class @ 0x000000076bfe9678 Oop for java/lang/Class @ 0x000000076bfe9678
jint Klass::_modifier_flags: 1
# public class ziya.Parent @0x00000007c0061c88,这个字段_super指向父类
Klass* Klass::_super: Klass @ 0x00000007c0061c88
省略。。。
Klass* InstanceKlass::_array_klasses: Klass @ null
# 重点,方法集合的地址是0x000000001bd4fff0
Array<Method*>* InstanceKlass::_methods: Array<Method*> @ 0x000000001bd4fff0
Array<Method*>* InstanceKlass::_default_methods: Array<Method*> @ null
省略。。。
hsdb> 

重点,方法集合的地址是0x000000001bd4fff0

hsdb> mem 0x000000001bd4fff0 10
#看内存地址0x000000001bd4fff0 开始的10*8=80字节的信息
#只要看前面40个字节的值就可以了
#猜测:第1个8个字节,值为4,保存的是方法集合的个数
#猜测:第2个8个字节,值为0x000000001bd50060 ,指向构造方法
#猜测:第3个8个字节,值为0x000000001bd50120 ,指向main方法
#猜测:第4个8个字节,值为0x000000001bd501c8 ,指向show方法
#猜测:第5个8个字节,值为0x000000001bd50270 ,指向say方法
# 方法的顺序的猜测,是和上面的TestDuotai类生成的字节码相对应的
0x000000001bd4fff0: 0x0000000000000004 
0x000000001bd4fff8: 0x000000001bd50060 
0x000000001bd50000: 0x000000001bd50120 
0x000000001bd50008: 0x000000001bd501c8 
0x000000001bd50010: 0x000000001bd50270 
0x000000001bd50018: 0x8000000000000000 
0x000000001bd50020: 0x000000001bd4fc80 
0x000000001bd50028: 0x0000000000000000 
0x000000001bd50030: 0x000e000500000009 
0x000000001bd50038: 0x0000000c000b0005 
验证0x000000001bd50120 ,指向main方法

使用HSDB看下main线程栈的内存情况,箭头所指向的内存地址,
因为此时(断点处)main线程已经执行完obj.show()方法,可以确定这个方法就是main方法的内存地址了
main线程的内存情况
看下0x000000001bd50120内存情况

hsdb> inspect 0x000000001bd50120
Type is Method (size of 88)
ConstMethod* Method::_constMethod: ConstMethod @ 0x000000001bd500b8
MethodData* Method::_method_data: MethodData @ null
MethodCounters* Method::_method_counters: MethodCounters @ 0x000000001bd50748
#这里通过方法的访问标识符,可以看到访问标识为9,和字节码中的一致,侧面验证是main方法
AccessFlags Method::_access_flags: 9
int Method::_vtable_index: -2
u2 Method::_method_size: 11
u1 Method::_intrinsic_id: 0
nmethod* Method::_code: nmethod @ null
address Method::_i2i_entry: address @ 0x000000001bd50150
AdapterHandlerEntry* Method::_adapter: AdapterHandlerEntry @ 0x00000000029d5e70
address Method::_from_compiled_entry: address @ 0x000000001bd50160
address Method::_from_interpreted_entry: address @ 0x000000001bd50170
猜测0x000000001bd501c8是show方法内存地址
hsdb> inspect 0x000000001bd501c8
Type is Method (size of 88)
ConstMethod* Method::_constMethod: ConstMethod @ 0x000000001bd50178
MethodData* Method::_method_data: MethodData @ null
MethodCounters* Method::_method_counters: MethodCounters @ 0x000000001bd507a8
AccessFlags Method::_access_flags: 1
int Method::_vtable_index: 6
u2 Method::_method_size: 11
u1 Method::_intrinsic_id: 0
nmethod* Method::_code: nmethod @ null
address Method::_i2i_entry: address @ 0x000000001bd501f8
AdapterHandlerEntry* Method::_adapter: AdapterHandlerEntry @ 0x00000000029d5e70
address Method::_from_compiled_entry: address @ 0x000000001bd50208
address Method::_from_interpreted_entry: address @ 0x000000001bd50218

TODO以及思考

1.虽然可以通过方法的索引和访问标识推断,但是要验证内存地址0x000000001bd501c8保存的就是show方法相关代码,最好是能解析出字符串"show",这个就需要看JVM底层ConstMethod相关的源码
2.示例代码中的接口引用指向子类实现,是通过new实现的(TestParent obj = new TestDuotai),如果把手动new改为通过Java反射(Class.newInstance),再结合JVM类加载相关知识,就可以实现很多框架都用到的SPI机制。关于SPI可以参考我的前面博客:https://blog.csdn.net/shiyueshis/article/details/115433661。通过这一点,Java的多态机制是很重要的一个知识点。

参考

微信公众号:硬核子牙

补充

补充1:找到验证show方法内存地址的方法

1.在show方法上面断点
在show方法上断点
2.使用HSDB查看main线程栈信息(箭头所指处)
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值