《深入理解Java虚拟机》中在描述JVM栈帧的局部变量表时,有这样一段话:

  局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、bytecharshortintfloatlongdouble)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。

  我们在这里不讨论基本类型和returnAddress。单独说一说对象引用的理解。--它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置。

———————————————————————————————————————————

  里面的描述令我很迷惑,这个“句柄”的概念其实也是高频地出现在了Thinking In Java这本经典中,那么在物理内存中,这个句柄到底是什么呢?另外,这里的对象引用难道不是在堆内存中真实对象的地址么,为什么除了指针之外还有别的可能呢?

  在Google搜索了一通之后,发现了一个解释,结合自己的理解,这里记录一下。

———————————————————————————————————————————

  首先,如果这个对象引用就是堆内存中该对象的地址,那么很好理解。接下来重点说句柄和相关的位置。

  句柄是一个唯一的整数,作为对象的身份id,区分不同的对象,和同类中的不同实例。程序可以通过句柄访问对象的部分信息。句柄不代表对象的真实内存地址。

  那么从这里我们可以判断,句柄不是堆内存中的地址,而且通过这个整数,程序可以访问到对象的部分信息。那么这个整数真实访问的是什么呢?其实是一组映射,也就是说,句柄访问了该映射,而这个映射中存有堆内存里真实对象的地址。这里举一个MySQL存储数据的例子来进行类比。比如有个订单表,表里有一个字段userId,存储了系统中注册用户的信息。那么我们拿到一个订单id,首先是去找到订单的这一行数据,而这行数据的userId,找到User表,才能真正查询到用户的信息。这样一来,这个句柄的概念,就比较好理解了,而文中所说的“或者其它与此对象相关的位置”也就找到了解释,不管这里所说的相关位置是什么,肯定是最终能够找到堆内存中具体的对象的,所以作者这样的描述是准确的。

  另外,句柄能够访问到对象的部分信息,也就不难理解了。我们还是拿订单表和用户表的例子来说明,所谓的部分信息,也就是在订单表里,能够找到除了user真实地址之外的信息,比如我们在订单表里不仅存放了userId,也存放了这个用户的类型(App用户,PC用户...),便于我们查询。那么所谓的部分信息,应该是指除了真实的对象地址,也放入了这个对象其他信息。

  真实情况是,一部分是直接地址,另一部分是存储了句柄池的地址,而这个指向的句柄池放了对象的真实地址(堆内存中),也放入了对象类型(class)信息的地址(方法区中)。

———————————————————————————————————————————

  理解了上面的这句话,我们要问两个为什么:1.为什么这个对象引用可以有这么多种可能?2.直接使用对象的地址不是很好吗,Java为什么会有句柄这个东西?

  解释:

  1.JVM虚拟机规范中,只要求根据对象引用的这个值,最终能够找到对象的内存地址,但是没有规定必须使用哪种方式。它只定义了一个接口,不同的JVM可以有不同的实现方式。可以使用对象的内存地址,也可以使用句柄这种通过映射找到内存地址的方式,也可以是别的方式。这里也能够说明为什么原文里的描述还有“或者其他与此对象相关的位置”的说法。

  2.内存地址是很方便的,而且节省了一次寻址的消耗。但是我们想一想,这个地方原文描述的是栈帧中的局部变量表,在多线程的环境下,不同的线程里处理着不同的栈帧,而这些栈帧随着方法执行的开始或者结束在不停地入栈和出栈。假设这个时候,发生了一次垃圾回收,对象在内存中的地址发生了变化,那么JVM就必须到这个动态的环境中,将所有相关的内存地址修改过来,这可是一个很复杂且容易出错的过程......如果用一个映射表,在栈帧中的对象引用拿着这个固定的数字,那么当内存地址发生变化时,JVM只需要修改这个映射表中数字对应的对象地址,这可比第一种方案简单多了!

———————————————————————————————————————————

  这里只是记录自己的理解,甚至是一些猜测。所以,如果你有不同的理解或者你知道更为准确的过程信息,欢迎留言讨论。

posted on 2021-07-19 22:52  长江同学  阅读(246)  评论(3编辑  收藏  举报