性能文章>GC复制存活对象,它内存地址变了么?>

GC复制存活对象,它内存地址变了么?原创

515614

前言

前些天与一位朋友技术交流,朋友在招人面试时想到一个问题,JVM垃圾回收时,会复制存活的对象到不同的区域。比如从新生代复制到老年代,在此过程中,被复制的对象的地址是否变了呢?对他提出的这个问题很感兴趣,深入研究了一下,便有了这篇文章。

更新引用是JVM的职责

任何一款JVM的设计,采用任何一种GC算法进行对象的移动操作时,如何更新对象引用都是JVM的基本职责。也就是说,当移动对象时,必然会涉及到对象引用的变更,只不过这部分操作JVM已经帮我们做了。

作为开发者来说,可以将引用理解为存储对象的抽象句柄,而不必担心JVM是如何管理对象存储的。但如果做技术研究,好奇底层的实现,倒是值得深入研究一下。

当对象的实际地址发生变化时,简单来说,JVM会将指向该地址的一个或多个变量所使用的引用地址进行更新,从而达到在“不知不觉”中移动了对象的效果。

JVM规范中只规定了引用类型是指向对象的引用,并没有限制具体的实现。因此,不同虚拟机的实现方式可能不同。通常有两种实现形式:句柄访问和直接指针访问。

句柄访问

先来看一张图,句柄访问的形式是堆空间维护一个句柄池,对象引用中保存的是对象的句柄位置。在堆中的句柄包含对象的实例数据和类型数据的真实地址。
IMG_8613.JPG

这种形式的实现好处很明显,引用中保存的对象句柄地址相对稳定(不变),当GC操作移动对象时只用维护句柄池中存储的信息即可,特别是多个变量都引用同一个句柄池中的句柄时,可以减少更新变量存储的引用,同时确保变量的地址不变。缺点就是多了一次中转,访问效率会有影响。

直接指针访问

直接指针访问省去了中间的句柄池,对象引用中保持的直接是对象地址。
IMG_8614.JPG

这种方式很明显节省了一次指针定位的开销,访问速度快。但是当GC发生对象移动时,变量中保持的引用地址也需要维护,如果多个变量指向一个地址,需要更新多次。Hot Spot虚拟机便是基于这种方式实现的。

如何查看引用地址?

上面聊了对象引用的实现形式,那么在日常开发中是否可以通过打印等形式来查看对象的地址吗?有这样一个说法,通过对象默认的toString方法打印出来的信息中包含对象的引用地址。下面我们通过一个实例来看看:

Bike bike = new Bike();
System.out.println(bike);

当我们执行上述程序时,控制台会打印出如下信息:

com.secbro2.others.Bike@4dc63996

@后面的字符串是什么?是对象的地址吗?这种地址的说法其实在坊间流传了很久。我们先来看Object的toString源码:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

通过源码我们会发现,其实@符合后面并不是对象的地址,而只是hashcode的十六进制展现形式而已。

那么,如何打印对象的内存地址呢?我们需要依赖一个JOL(Java Object Layout)类库,在项目中添加如下Maven依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

然后在程序中通过如下方法使用:

String answer = "42";
System.out.println("The memory address is " + VM.current().addressOf(answer));

会发现打印的内容如下:

The memory address is 31856221536

上面的便是真实的内存地址,虽然能够获取并打印出内存地址,但由于不同环境下的JVM采用了不同的指针压缩操作。因此,我们不要基于此地址来做一些本机内存相关的操作。但上面的打印,明确的证明了toString方法打印出来的信息并不包括对象的内存地址。

鉴于此,基于toString方法打印出来hashCode值只能保证两个对象的hashcode一样,却无法保证两个引用地址指向同一对象。

小结

通过与朋友的一个小交流,深挖一下,竟然发现不少底层的知识点,交流和探索的作用可见一斑。总结来说就是:JVM在GC操作时会自动维护引用地址,变量对应的应用地址是否变化要看采用的是基于句柄池方式还是直接指针指向的方式。同时,当我们通过toString方法打印时,输出的内容并不包含对象地址,只不过是对象hashcode的十六进制而已。

分类:
标签:
请先登录,再评论

image.png

10月前

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得