【String str = new String(“AAA“) 创建了几个对象?】

news2025/1/17 1:03:08

在这里插入图片描述

✅典型解析


创建的对象数应该是1个或者2个。


首先要清楚什么是对象?


Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的,在HotSpot虚机中,存储的形式就是oop-klass model,即ava对象模型。我们在Java代码中,使用new创建一个对象的时候,JVM会创建一instanceOopDesc对象,这个对象中包合了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息,元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。


这才叫对象。其他的,一概都不叫对象。


那么不管怎么样,一次new的过程,都会在堆上创建一个对象,那么就是起码有一个对象了。至于另外一个对象到底有没有要看具体情况了。


另外这一个对象就是常量池中的字符串常量,这个字符串其实是类编译阶段就进到Class常量池的,然后在运行期,字符串常量在第一次被调用(准确的说是ldc指令)的时候,进行解析并在字符串池中创建对应的String实例的。


在运行时常量池中,也并不是会立刻被解析成对象,而是会先以VM_CONSTANT_UnresolveString_info的形式驻留在常量池。在后面,该引用第一次被LDC指令执行到的时候,就尝试在堆上创建字符串对象,并将对象的引用驻留在字符串常量池中。


通过看上面的过程,你也能发现,这个过程的触发条件是我们没办法决定的,问题的题干中也没提到。有可能执行这段代码的时候是第一次LDC指令执行,也许在前面就执行过了。


所以,如果是第一次执行,那么就是会同时创建两个对象。一个字符串常量引用指向的对象,一个我们new出来的对象。


如果不是第一次执行,那么就只会创建我们自己new出来的对象。


至于有人说什么在字符串池内还有在栈上还有一个引用对象,你听听这说法,引用就是引用。别往对象上面扯。


✅什么是Class常量池,和运行时常量池关系是什么?


Class常量池可以理解为是Class文件中的咨源仓库。Cass文件中除了包合类的版本,字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Litera)和符号引用Symbolic References)。


Class是用来保存常量的一个媒个场所,并且是一个中间场所。Class文件中的常量池部分的内容,会在运行期被运行时常量池加载进去。


✅ 查看Class常量池


由于不同的Class文件中包合的常量的个数是不固定的,所以在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录了常量池中常量的个数。


在这里插入图片描述

当然,还有一种比较简单的产看Class文件中常量池的方法,那就是通过 javap 命令,对于以上的HelloWorld.class,可以通过

javap -v HelloWorld.class


查看常量池内容如下:


在这里插入图片描述

从上图可以看到,反编译后的class文件常量池中共有16个常量。而Class文件中常量计数器的数值是0011,将该16进制数字转化为10进制的结果是17。


原因是与Java的语言习惯不同,常量池计数器是从0开始而不是从1开始的,常量池的个数是10进制的17,这就代表了其中有16个常量,索引值的范围为1-16。


✅字符串常量是什么时候进入到字符串常量池的?


字符串常量池中的常量有两种来源,一种是字面量会在编译期先进入到Class常量池,然后再在运行期进去到字符串池,还有一种就是在运行期通过intern将字符串对象手动添加到字符串常量池中。


那么,Class常量池中的常量,是在什么时候被放进到字符串池的呢?


Java 的类加载过程要经历加载 (Loading) 、链接 (Linking) 、初始化nitializing) 等几个步,在链接这人步骤,又分为验证 (Verification) 、准备 (Preparation) 以及解析(Resolution) 等几个步骤。


在Java 虚拟机规范及Java语言规范中都提到过:


《The Java Virtual Machine Specification》 5.4 Linking ::




For example, a Java Virtual Machine implementation may choose to resolve each symbolic reference ina class or interface individually when it is used (azy" or “late” resolution), or to resolve them all atonce when the class is being verified (“eager” or “static” resolution)




《The Java Language Specification》 12.3 Linking of Classes and Interfaces




For example, an implementation may choose to resolve each symbolic reterence in a class or interfaceindividually, only when it is used (lazy or late resolution), or to resolve them all at once while the classis being verified (static resolution), This means that the resolution process may continue, in someimplementations, after a cass or interface has been initialized.


大致意思差不多,就是说,Java 虚拟机的实现可以选择只有在用到类或者接口中的符号引用时才去逐一解析他(延迟解析),或者在验证类的时候就解析每个引用(预先解析)。这意味着在一些虚拟机实现中,把常量放到常量池的步骤可能是延迟处理的。


对于 HotSpot 虚拟机来说,字符串字面量,和其他基本类型的常量不同,并不会在类加载中的解析阶段填充并驻留在字符串常量池中,而是以特殊的形式存储在运行时常量池中。只有当这个字符串字面量被调用时,才会对其进行解析,开始为他在字符串常量池中创建对应的 String 实例。


通过查看 HotSpotJDK 1.8 的 ldc 指令的源代码,也可以验证上面的说法。


ldc 指令表示int、float或String型常量从常量池推送至栈顶


IRT_ENTRY(void,InterpreterRuntime::ldc(JavaThread* thread, bool wide))
//access constant pool
ConstantPool* pool = method(thread)->constants():
int index = wide ? get_index_u2(thread, Bytecodes::_ldc_w) : get_index_u1(thread,Bytecodes :: _ldc);
constantTag tag = pool->tag_at(index);


assert (tag.is_unresolved_klass() || tag.is_klass(),"wrong ldc call");
Klass* klass = pool->klass_at(index,CHECK);
oop java_class = klass->java mirror();	
thread->set_wm_result(java_class);
IRT_END

所以,字符串常量,是在第一次被调用(准确的说是ldc指令)的时候,进行解析并在字符审池中创建对应的String实例的。


✅字符串常量池是如何实现的?


字符串常量池(String Constant Pool) 是Java中一块特殊的内存区域,用于存储字符串常量。


当程序中出现字符串常量时,Java编译器会将其放入字符串常量池中。字符串常量是不可变的,因此可以共享。如果字符串常量池中已存在相同内容的字符串,编译器会直接引用已存在的字符串常量,而不会创建新的对象。


在HotSpot虚拟机中:


在JDK 1.6及之前的版本,字符串常量池通常被实现为方法区的一部分,即永久代(Permanent Generation)用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。


从JDK 1.7开始,字符串常量池的实现方式发生了重大改变。字符串常量池不再位于永久代,而是直接存放在堆(Heap) 中,与其他对象共享堆内存。


之所以要挪到堆内存中,主要原因是因为永久代的 GC 回收效率太低,只有在FulGC的时候才会被执行回收。但是Java中往往会有很多字符串也是朝生夕死的,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

✅字符串常量从哪来的?


字符串常量池中的常量有以下几个来源:

1、字面量常量

在代码中直接使用双引号括起来的字符串字面值(如 String s "Hllis”)会被认为是常量,并且会在编译后进入class文件的常量池,并且在运行阶段,进入字符串常量池。这是最常见的字符串常量来源。

2、intern() 方法


String类提供了一intern()方法,用于将字符电对象手动添加到字符电常量池中。调用intern0方法时,如果字答串常量池中已经存在相同内容的字符串,将会返回常量池中的引用:如果不存在,则会在常量池中创建新的字符串。


✅扩展知识仓


✅字面量和运行时常量池


JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JM中创建的字符串的数量,字符串类维护了一个字符串常量池。


在JVM运行时区域的方法区中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量和符号引用。


了解Class文件结构或者做过Java代码的反编译的朋友可能都知道,在iava代码被 javac 编译之后,文件结构中是包含 部分 Constant pool 的。比以下代码:


public static void main(string[] args) {
	String s ="Hollis";
}

经过编译后,常量池内容如下:


在这里插入图片描述

上面的Class文件中的常量池中,比较重要的几个内容:


在这里插入图片描述

上面几个常量中, s 就是前面提到的符号引用,而 Hollis 就是前面提到的字面量。而Class文件中的常量池部分的内容,会在运行期被运行时常量池加载进去。关于字面量,详情参考Java SE Specifications: Java SE Specifications


✅intern

编译期生成的各种字面量符号引用是运行时常量池中比较重要的一部分来源,但是并不是全部。那么还有一种情况,可以在运行期向运行时常量池中增加常量。那就是 string 的 intern 方法。


当一个 string 实例调用 intern() 方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用:。


intern()有两个作用,第一个是将字符串字面量放入常量池 (如果池没有的话),第二个是返回这个常量的引用。


✅intern的正确用法


不知道,你有没有发现,在 String s3 = new String(“Hollis”).intern(); 中,其实 intern 是多余的?


因为就算不用 intern ,Hollis作为一个字面量也会被加载到Class文件的常量池,进而加入到运行时常量池中为啥还要多此一举呢? 到底什么场景下才需要使用 intern 呢?


在解释这个之前,我们先来看下以下代码:


String s1 ="Hollis";
String s2 = "Chuang";
String s3 = s1 + s2;
String s4 ="Hollis" + "Chuang";

在经过反编译后,得到代码如下:

String s1 = "Hollis";
String s2 = "Chuang";
String s3 = (new StringBuilder()).append(s1).append(s2).toString();
String s4 ="HollisChuang";

可以发现,同样是字符串拼接,s3和s4在经过编译器编译后的实现方式并不一样。s3被转化成 stringBuilder及 append ,而s4被直接拼接成新的字符串。


如果你感兴趣,你还能发现, String s3 = s + s2;经过编译之后,常量池中是有两个字符串常量的分别是Chuang (其实 Hollis 和 Chuang 是 String s1 =“Hollis”,和 String s2 = “ChuanHollis.g”,定义出来的),拼接结果 HollisChuang 并不在常量池中。


究其原因,是因为常量池要保存的是已确定的字面量值。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串池。


如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成 StringBuilder.append这种情况编译器是无法知道其确定值的。只有在运行期才能确定。


那么,有了这个特性了, intern 就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。


这时候,对于那种可能经常使用的字符串,使用 intern 进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。


比如 —— 深入解析String#intern中举的一个例子:


static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
	long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
        //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
    }

	System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

运行的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是一个演示代码,其中有两条语句不一样,一条是使用 intern,一条是未使用 intern。结果如下图:


2160ms

在这里插入图片描述
826ms

在这里插入图片描述
通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。

细心的同学会发现使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。

So,我们明确的知道,会有很多相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过 intern 显示的将其加入常量池,这样可以减少很多字符串的重复创建。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1340517.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue框架引入Axios

首先已经创建好了 Vue 框架&#xff0c;安装好了 node.js。 没有完成的可按照此博客搭建&#xff1a;搭建Vue项目 之后打开终端&#xff0c;使用命令。 1、命令安装 axios 和 vue-axios npm install axios --save npm install vue-axios --save2、package.json 查看版本 在 p…

Binder系列-service_manager.c

1. service_manager的任务 open 驱动告诉驱动&#xff0c;它是“servicemanager”在一个循环里 从驱动读取数据 解析数据 调用&#xff0c;根据code执行注册服务或者获取服务 &#xff08;图来自韦老师的视频&#xff09; 2.代码流程 2.1.open驱动 //framework…

elasticsearch列一:索引模板的使用

概述 近期一直在负责es这块&#xff0c;就想着和大家分享一些使用经验&#xff0c;我们从存储、查询、优化、备份、运维等几个方面来做分享。今天咱们先看下如何更加合理的存储数据。 初见索引模板 记得刚接触es还是18年那会&#xff0c;项目上线后因一些原因导致日志这部分的…

深入理解JVM虚拟机第三十二篇:详解JVM当中本地方法栈

😉😉 欢迎加入我们的学习交流群呀! ✅✅1:这是孙哥suns给大家的福利! ✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring等等很多应用和源码级别的高质量视频和笔记资料,你想学的我们这里都有! 🥭🥭3:QQ群:583783824 📚📚 工作VX:BigTreeJava 拉你…

059:vue中使用 AJAX来读取来自XML文件的信息

第059个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

SpreadJS 集成使用案例

SpreadJS 集成案例 介绍&#xff1a; SpreadJS 基于 HTML5 标准&#xff0c;支持跨平台开发和集成&#xff0c;支持所有主流浏览器&#xff0c;无需预装任何插件或第三方组件&#xff0c;以原生的方式嵌入各类应用&#xff0c;可以与各类后端技术框架相结合。SpreadJS 以 纯前…

Python之路:网络工程师的自动化进阶(第2版)

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 I. 引言 随着网络技术的不断发展&#xff0c;…

H5向微信小程序发送信息(小程序web-view打开H5)

引入weixin-js-sdk npm i weixin-js-sdk 页面引入 // 引入wxjsimport wx from "weixin-js-sdk"; 点击触发方法 methods: {goweap(id){console.log(wx);// H5传递数据 &#xff08;navigateBack&#xff09;wx.miniProgram.navigateBack({delta: 1});wx.min…

混沌工程的核心原则是什么?

在软件开发和系统运维领域&#xff0c;一种新兴的实践逐渐崭露头角&#xff0c;那就是混沌工程。混沌工程的核心理念是通过有计划的、控制的实验引入混沌&#xff0c;以验证和改善系统的稳健性。本文将介绍混沌工程的定义及核心原则是什么! 什么是混沌工程? 混沌工程是一种系统…

【理论】STM32定时器时间计算公式 +【实践】TIM中断1s计时一次

前言&#xff1a;定时器TIM的详细知识点见我的博文&#xff1a;11.TIM定时中断-CSDN博客 STM32定时器时间计算公式 公式解释&#xff1a; ARR&#xff08;TIM_Period&#xff09;&#xff1a;自动重装载值&#xff0c;是定时器溢出前的计数值 PSC&#xff08;TIM_Prescaler&…

k8s的二进制部署(源码包部署)

实验条件&#xff1a; 主机名 IP地址 组件 作用 master01 20.0.0.17 kube-apiserver、kube-controller-manager、kube-scheduler、etcd k8s部署 master02 20.0.0.27 kube-apiserver、kube-controller-manager、kube-scheduler node01 20.0.0.37 kubelet、kube-pro…

工具系列:TimeGPT_(8)使用不规则时间戳进行时间序列预测

文章目录 介绍不规则时间戳的单变量时间预测不规则时间戳的外生变量时间预测 介绍 在处理时间序列数据时&#xff0c;时间戳的频率是一个关键因素&#xff0c;可以对预测结果产生重大影响。像每日、每周或每月这样的常规频率很容易处理。然而&#xff0c;像工作日这样的不规则…

OpenAPI,已支持表单数据格式校验

OpenAPI 路径 开放平台 功能简介 「OpenAPI」- 支持表单数据格式校验。 通过「OpenAPI-新增表单数据」接口&#xff0c;新增数据时&#xff0c;若数据格式不匹配&#xff0c;会导致无法新增。 例如&#xff0c;数字不能新增到日期格式的表单字段。 请参考数据格式传参&a…

IPv4归属地信息查询方法与应用

IPv4地址归属地信息查询是网络管理和安全领域的关键工具。本文将介绍IPv4地址的概念&#xff0c;探讨IPv4归属地信息的重要性&#xff0c;并详细介绍几种查询IPv4归属地信息的方法以及其应用场景。 第一部分&#xff1a;IPv4地址简介 1.1 什么是IPv4地址 IPv4&#xff08;In…

HTML 网页设计 简约风格 注册界面

成品如下 html <!DOCTYPE html> <html><head><meta charset"utf-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Science科幻注册界面</title><link href"…

游戏服务器列表-增量处理

前言 服务器列表比较简单 固定表/开服表&#xff08;一般只会增加及合并),操作频率很低 一般由 服务器ID IP NAME 开服时间 为什么要自己写个&#xff0c; MySQL的增量备份日志&#xff0c;相对太复杂了 看下 一条UPDATE 语句 随便找了表 修改了 10002 level 1 ->2 得用专门…

uniapp原生插件 - android原生插件打包流程 ( 避坑指南一)

【彩带- 避坑知识点】: 当时开发中安卓插件打包成功后&#xff0c;uniapp引用插件aar&#xff0c;用云打包 &#xff0c;总是提示不包含插件。原因是因为module的androidManifest.xml文件没有注册activity。 这一步 很重要&#xff0c;一定要注册。 --------------------------…

Mybatis枚举类型处理和类型处理器

专栏精选 引入Mybatis Mybatis的快速入门 Mybatis的增删改查扩展功能说明 mapper映射的参数和结果 Mybatis复杂类型的结果映射 Mybatis基于注解的结果映射 文章目录 专栏精选摘要引言正文枚举类型映射简单枚举映射枚举顺序映射复杂枚举映射 类型处理器 总结 摘要 在这篇…

【C++】开源:cpp-httplib HTTP协议库配置与使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍cpp-httplib HTTP协议库配置与使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&a…

vmware虚拟机中Nat、桥接模式和仅主机的差别

NAT 在NAT模式下&#xff0c;主机3是Kali和Win两个操作系统的宿主机&#xff0c;那么Kali和Win可以连接到外网&#xff0c;也可以和主机3进行互联&#xff0c;但是主机1和主机2不能连接到Kali和Win。 桥接 在桥接模式下&#xff0c;主机3是Kali和Win两个操作系统的宿主机&…