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

简介: GC复制存活对象,它内存地址变了么?

前言

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

更新引用是JVM的职责

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

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

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

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

句柄访问

先来看一张图,句柄访问的形式是堆空间维护一个句柄池,对象引用中保存的是对象的句柄位置。在堆中的句柄包含对象的实例数据和类型数据的真实地址。image.png这种形式的实现好处很明显,引用中保存的对象句柄地址相对稳定(不变),当GC操作移动对象时只用维护句柄池中存储的信息即可,特别是多个变量都引用同一个句柄池中的句柄时,可以减少更新变量存储的引用,同时确保变量的地址不变。缺点就是多了一次中转,访问效率会有影响。

直接指针访问

直接指针访问省去了中间的句柄池,对象引用中保持的直接是对象地址。 image.png这种方式很明显节省了一次指针定位的开销,访问速度快。但是当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的十六进制而已。


目录
相关文章
|
29天前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
41 3
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
140 4
|
3月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
103 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
3月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
42 2
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
339 1
|
28天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
25 3
|
2月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
54 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。

热门文章

最新文章