new String("abc")创建了几个对象

面试官考察点猜想

这种问题,考察你对JVM的理解程度。涉及到常量池、对象内存分配等问题。

涉及背景知识详解

在分析这个问题之前,我们先来了解一下JVM的组成,如图所示。

在JVM1.8中,内存划分为堆、程序计数器、本地方发栈、方法区(元空间)、虚拟机栈。

JVM知识点普及

下面分别解释一下JVM运行时内存的功能。

堆内存空间

堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。

但需要注意的是,这些区域的划分因不同的垃圾收集器而不同。大部分垃圾收集器都是基于分代收集理论设计的,就会采用这种分代模型。而一些新的垃圾收集器不采用分代设计,比如 G1 收集器就是把堆内存拆分为多个大小相等的 Region。

方法区

在 jdk8 之前,HotSopt 虚拟机的方法区又被称为永久代,由于永久代的设计容易导致内存溢出等问题,jdk8 之后就没有永久代了,取而代之的是元空间(MetaSpace)。元空间并没有处于堆内存上,而是直接占用的本地内存,因此元空间的最大大小受本地内存限制。

方法区与堆空间类似,是所有线程共享的。方法区主要是用来存放已被虚拟机加载的类型信息、常量、静态变量等数据。方法区是一个逻辑分区,包含元空间、运行时常量池、字符串常量池,元空间物理上使用的本地内存,运行时常量池和字符串常量池是在堆中开辟的一块特殊内存区域。这样做的好处之一是可以避免运行时动态生成的常量的复制迁移,可以直接使用堆中的引用。

要注意的是,字符串常量池在JVM中只有一个,而运行时常量池是和类型数据绑定的,每个Class一个。

  1. 每个class的字节码文件中都有一个常量池,里面是编译后即知的该class会用到的字面量符号引用,这就是class文件常量池。JVM加载class,会将其类信息,包括class文件常量池置于方法区中。
  2. class类信息及其class文件常量池是字节码的二进制流,它代表的是一个类的静态存储结构,JVM加载类时,需要将其转换为方法区中的java.lang.Class类的对象实例;同时,会将class文件常量池中的内容导入运行时常量池
  3. 运行时常量池中的常量对应的内容只是字面量,比如一个"字符串",它还不是String对象;当Java程序在运行时执行到这个"字符串"字面量时,会去字符串常量池里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在Java堆里创建该字面量对应的String对象,并将其引用置于字符串常量池中,然后返回该引用。
  4. Java的基本数据类型中,除了两个浮点数类型,其他的基本数据类型都在各自内部实现了常量池,但都在[-128~127]这个范围内。

虚拟机栈

每当启动一个新的线程,虚拟机都会在虚拟机栈里为它分配一个线程栈,线程栈与线程同生共死。线程栈以栈帧为单位保存线程的运行状态,虚拟机只会对线程栈执行两种操作:以栈帧为单位的压栈或出栈。每个方法在执行的同时都会创建一个栈帧,每个方法从调用开始到结束,就对应着一个栈帧在线程栈中压栈和出栈的过程。方法可以通过两种方式结束,一种通过 return 正常返回,一种通过抛出异常而终止。方法返回后,虚拟机都会弹出当前栈帧然后释放掉。

当虚拟机调用一个Java方法时.它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。

栈帧由三部分组成:局部变量区、操作数栈、帧数据区。

1)局部变量区:

  • 局部变量区是一个数组结构,主要存放对应方法的参数和局部变量。
  • 如果是实例方法,局部变量表第一个参数是一个 reference 引用类型,存放的是当前对象本身 this。

2)操作数栈:

  • 操作数栈也是一个数组结构,但并不是通过索引来访问的,而是栈的压栈和出栈操作。
  • 操作数栈是虚拟机的工作区,大多数指令都要从这里弹出数据、执行运算、然后把结果压回操作数栈。

3)动态链接:

  • 每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。

  • 在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用。

4)方法返回:

  • 方法执行后,有两种方式退出该方法:正常调用完成,执行返回指令。异常调用完成,遇到未捕获异常,不会有方法返回值给调用者。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是相似的,当线程调用Java方法时,会创建一个栈帧并压入虚拟机栈;而调用本地方法时,虚拟机会保持栈不变,不会压入新的栈帧,虚拟机只是简单的动态链接并直接调用指定的本地方法,使用的是某种本地方法栈。比如某个虚拟机实现的本地方法接口是使用C连接模型,那么它的本地方法栈就是C栈。

本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,它可以做任何他想做的事情,本地方法不受虚拟机控制。

程序计数器

每一个运行的线程都会有它的程序计数器(PC寄存器),与线程的生命周期一样。执行某个方法时,PC寄存器的内容总是下一条将被执行的地址,这个地址可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是 undefined。

程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。多线程环境下,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

代码在JVM内存中的体现

当我们通过Object o=new Object()创建一个对象时,在JVM中会分配一块内存用来存储该对象的信息,实现原理如下图所示。

在main方法中,创建了一个局部变量o,当main方法运行时,首先会把main方法压入到栈帧中,接着执行该方法的Object o =new Object()创建对象。

  1. 在局部变量表中创建一个局部变量o
  2. 在堆内存中分配一块内存地址,用来存储object对象。
  3. 变量o指向堆内存中的内存地址。

我们再来看一个例子,声明一个Person对象,在该对象中存在一个常量name、以及一个成员变量age,当运行该类中的main方法时,此时JVM内存中的运行情况如下。

在这个例子中,看到了常量池的出现,看来,还有必要了解一下常量池的知识

JVM中的常量池

在JVM中,常量池主要分为:Class文件常量池运行时常量池,当然还有全局字符串常量池,以及基本类型包装类对象常量池

常量池主要存放两大类常量:字面量和符号引用。

  • 字面量:字面量主要是文本字符串、final 常量值、类名和方法名的常量等。
  • 符号引用:符号引用对java动态连接起着非常重要的作用。主要的符号引用有:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。

Class文件常量池

class文件是一组以8位字节为单位的二进制数据流,在java代码的编译期间,我们编写的.java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池

为了更好的说明,我们通过下面这段代码为例进行讲解。

class ConstantExample{
private int value = 1;
public String s = "abc";
public final static int f = 0x101; public void setValue(int v){
final int temp = 3;
this.value = temp + v;
} public int getValue(){
return value;
}
}

这段代码被编译后,通过javap -v命令查看编译后的字节码。

从下面这个字节码信息中可以看到,执行这个命令之后我们得到了该class文件的版本号、常量池、已经编译后的字节码指令(处于篇幅原因这里省略),下面我们会对照这个class文件来讲解:

example/target/classes/HelloExample.class
Last modified 2021-10-25; size 734 bytes
MD5 checksum fd06c1426f4fdef12aa109ee7f010a45
Compiled from "HelloExample.java"
public class HelloExample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#32 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#33 // HelloExample.value:I
#3 = String #34 // abc
#4 = Fieldref #5.#35 // HelloExample.s:Ljava/lang/String;
#5 = Class #36 // HelloExample
#6 = Class #37 // java/lang/Object
#7 = Utf8 value
#8 = Utf8 I
#9 = Utf8 s
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 LHelloExample;
#21 = Utf8 getValue
#22 = Utf8 ()I
#23 = Utf8 setValue
#24 = Utf8 (I)V
#25 = Utf8 MethodParameters
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 SourceFile
#31 = Utf8 HelloExample.java
#32 = NameAndType #14:#15 // "<init>":()V
#33 = NameAndType #7:#8 // value:I
#34 = Utf8 abc
#35 = NameAndType #9:#10 // s:Ljava/lang/String;
#36 = Utf8 HelloExample
#37 = Utf8 java/lang/Object

字面量

字面量接近于java语言层面的常量概念,主要包括:

  • 文本字符串,也就是我们经常声明的:public String s = "abc";中的"abc"

       #3 = String             #34            // abc
  • 用final修饰的成员变量,包括静态变量实例变量局部变量

      #11 = Utf8               f
    #12 = Utf8 ConstantValue
    #13 = Integer 257

这里需要说明的一点,上面说的存在于常量池的字面量,指的是数据的,也就是abc0x101(257),通过上面对常量池的观察可知这两个字面量是确实存在于常量池的。

而对于基本类型数据(甚至是方法中的局部变量),也就是上面的private int value = 1;常量池中只保留了他的的字段描述符I字段的名称value,他们的字面量不会存在于常量池:

符号引用

符号引用主要设涉及编译原理方面的概念,包括下面三类常量:

  • 类和接口全限定名,也就是Ljava/lang/String;这样,将类名中原来的"."替换为"/"得到的,主要用于在运行时解析得到类的直接引用.

       #5 = Class              #36            // HelloExample
    #6 = Class #37 // java/lang/Object
  • 字段名称描述符,字段也就是类或者接口中声明的变量,包括类级别变量(static)实例级的变量

       #2 = Fieldref           #5.#33         // HelloExample.value:I
    #7 = Utf8 value
    #8 = Utf8 I

运行时常量

运行时常量池是方法区的一部分,所以也是全局共享的。我们知道,jvm在执行某个类的时候,必须经过加载、连接(验证,准备,解析)、初始化,在第一步的加载阶段,虚拟机需要完成下面3件事情:

  • 通过一个类的“全限定名”来获取此类的二进制字节流
  • 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构
  • 在内存中生成一个类代表这类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口

这里需要说明的一点是,类对象和普通的实例对象是不同的,类对象是在类加载的时候生成的,普通的实例对象一般是在调用new之后创建。

上面第二条,将class字节流代表的静态储存结构转化为方法区的运行时数据结构,其中就包含了class文件常量池进入运行时常量池的过程。这里需要强调一下,不同的类共用一个运行时常量池,同时在进入运行时常量池的过程中,多个class文件中常量池中相同的字符串只会存在一份在运行时常量池中,这也是一种优化。

运行时常量池的作用是存储 Java class文件常量池中的符号信息。运行时常量池 中保存着一些 class 文件中描述的符号引用,同时在类加载的“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)存储在 运行时常量池 中。

运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的较多的是String.intern()(这个方法下面将会详细讲)。

问题解答

理解了上述JVM的背景知识之后,再回到最开始的问题.下面这段代码会创建几个对象?

String str=new String("abc");
  1. 首先,我们看到这个代码中有一个new关键字,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因此这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上
  2. 其次,在String的构造方法中传递了一个字符串abc,由于这里的abc是被final修饰的属性,所以它是一个字符串常量。在首次构建这个对象时,JVM拿字面量"abc"去字符串常量池试图获取其对应String对象的引用。于是在堆中创建了一个"abc"的String对象,并将其引用保存到字符串常量池中,然后返回;

所以,这里正确的回答应该是: 如果abc这个字符串常量不存在,则创建两个对象,分别是abc这个字符串常量,以及new String这个实例对象。

如果abc这字符串常量存在,则只会创建一个对象。

问题总结

关于这道题,其实涉及到的知识点非常多,我并没有非常完整的把JVM的内容整体说完,因为JVM整个体系还是较为庞大的。

所以,建议大家平时如果有时间的情况下,可以系统化的学习一下JVM有关的内容,这块的面试问题还是比较多的。

关注[跟着Mic学架构]公众号,获取更多精品原创

面试题系列:new String("abc")创建了几个对象的更多相关文章

  1. 面试题之String str = new String("abc"); 创建了几个对象

    今天去面试的时候碰到了这个问题:String str = new String("abc"); 创建了几个对象,回来自己研究并查阅资料才发现答错了..网上的争论不少,有的说是两个, ...

  2. java中String s = new String("abc")创建了几个对象?

    答案是两个,现在我们具体的说一下: String s = new String("abc");一.我们要明白两个概念,引用变量和对象,对象一般通过new在堆中创建,s只是一个引用变 ...

  3. String s=new String("abc")创建了几个对象?

    String str=new String("abc");   紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢? 答案应该是1个或者2个. 1个 ...

  4. 【转载】java 中 String s = new String("abc") 创建了几个对象?!

    原文链接点这里,感谢博主分享 答案是两个,现在我们具体的说一下: String s = new String("abc"); 首先我们要明白两个概念,引用变量和对象,对象一般通过n ...

  5. String a=new String("abc")创建了几个对象

    String str=new String("abc");   紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢? 相信大家对这道题并不陌生,答案 ...

  6. new String("123") 创建了几个对象?

    String 对象可谓再熟悉不过了,与此相关的面试题经常会引出内存性能优化的问题,本篇主要以 new String("123") 创建了几个对象为例记录. 一.你能回答正确吗 St ...

  7. 工作10年后,再看String s = new String("xyz") 创建了几个对象?

    这个问题相信每个学习java的同学都不陌生,作为一个经典的面试题,到现在工作这么多年了我真是认为挺操蛋的一个问题,在网上到现在你仍然可以看见很多讨论这个问题的人,其中不乏工作很多年的人都有争论,我认为 ...

  8. String s = new String("aa") 创建了几个对象?

    1 最近几个同学面试的时候出现了这样一个问题 刚听到这个题目的时候的确是不知所措: 经过网上的查找和自己的理解来解释一下这个题目的答案 答案是: 为什么呢??? 1 实现我们都知道创建实例有两种方法 ...

  9. String s=new String("abc")产生了几个对象?[权威面试版]

    以下总结是我逛论坛 将零零碎碎的知识整理起来,方便自己记忆和阅读,顺便分享出来给大家学习. 若 String s=new String("abc"); 为第一句代码 则会产生两个对 ...

随机推荐

  1. python库--sklearn--流程图

  2. Dart简易教程 (1)---数据类型 运算符,类转换换

    从下面开始学习DART编程 以下是一个简单的示例: main(){ var number = 42; print(number);}程序说明,dart是一个强大的脚本类语言,可以不预先定义变量类型 , ...

  3. k8s标签label

    1.给节点设置标签 一遍pod部署选择 kubectl label node 节点名 disktype=ssd kubectl label node master1 disktype=ssd 效果 [ ...

  4. PHP的那些魔术方法(二)

    上文中介绍了非常常用并且也是面试时的热门魔术方法,而这篇文章中的所介绍的或许并不是那么常用,但绝对是加分项.当你能准确地说出这些方法及作用的时候,相信对方更能对你刮目相看. __sleep()与__w ...

  5. Python项目生成requirements.txt文件之pipreqs的使用

    生成requirements.txt时使用pip freeze > requirements.txt会将环境下所有的安装包都进行生成,再进行安装的时候会全部安装很多没有的包.耗时耗力其实是不可取 ...

  6. 『Python』matplotlib实现GUI效果

    1. 类RadioButtons的使用方法 类似单选框 import numpy as np import matplotlib.pyplot as plt import matplotlib as ...

  7. 定要过python二级 选择题第四套

    1. 2. 3. 4. 5. 6. python用于人工智能 7. 8. 9. 10. 11. 12. 13. 14. 15. 16.

  8. 基于AM335X,如何搭建优良的Linux开发环境(下)

    接着上一篇文章的Linux开发环境搭建,文章中详细讲解了 VMware14.1.1虚拟机安装.基于虚拟机安装Ubuntu14.04.3操作系统.安装Ubuntu14.04.3操作系统.安装虚拟机工具. ...

  9. WPF进阶技巧和实战01-小技巧

    Svg在WPF中的使用 方法1:拷贝svg中的部分代码转换成Geometry(作为Path的Data使用) 在vs或者直接打开svg,看到如下代码: <?xml version="1. ...

  10. Kubernetes集群(RKE)安装ArgoCD排坑

    Photo by Pixabay from Pexels Argo CD是一个声明式的,基于Kubernetes的GitOps持续交付工具.更多的细节参考 ArgoCD官网 的说明,这里记录了一些实践 ...