Java中String的两种赋值方式的区别

本文修改于:https://www.zhihu.com/question/29884421/answer/113785601

前言:在java中,String有两种赋值方式,第一种是通过“字面量”赋值,如:String str="hello",第二种是通过new关键字创建新对象,如:String str=new String("hello")。那么这两种赋值的方式到底有什么区别呢,下面给出具体分析。

1.首先回顾Java虚拟机的结构图。

在上面的虚拟机结构图中,中间的五彩区域叫“运行时数据区(Run-time Data Areas)”。也就是虚拟机管理的内存,就是大白话的“内存”。其中后面两个,程序计数器(PC Registers)和本地方法栈(Native Method Stack)与所讲没关系,先忽略。一般讲起来虚拟机内存最主要的就是以下三块:

1)堆(Heap):最大一块空间。存放对象实例和数组。全局共享。

2)栈(Stack):全称 “虚拟机栈(JVM Stacks)”。存放基本型,以及对象引用。线程私有

3)方法区(Method Area):“类”被加载后的信息,常量,静态变量存放于此。全局共享。在HotSpot里也叫“永生代”。但两者不能等同。

2.栈、堆和非堆。



上图中,首先Heap堆分成“新生代”,“老年代”,先不用管它,这是GC垃圾回收时候的事。重要的是Stack栈区里的“局部变量表(Local Variables)”“操作数栈(Operand Stack)”。因为栈是线程私有的,每个方法被执行的时候都会创建一个“栈帧(Stack Frame)”。而每个栈帧里对应的都维护着一个局部变量表和操作数栈基本数据类型对象引用就存在栈里,其实就是存在局部变量表里。而操作数栈是线程实际的操作台。

如下图,做个加法100+98,局部变量表就是存数据的地方,一直不变,到加法做完再把和加进去。操作数栈就很忙了,先把两个数字压进去,再求和,算出来以后再弹出去。



中间这个非堆(Non-Heap)可以粗略地理解为非堆里包含了永生代,而永生代里又包括了方法区。上面说了,每个类加载完之后,类的信息都存在方法区里。和String最相关的是里面的运行时常量池(Run-time Constant Pool)”。它是每个类私有的。后面会讲到,每个class文件里的“常量池”在类被加载器加载之后,就映射存放在这个地方。另外一个是“字符串常量池(String Pool)”。和运行时常量池不是一个概念。字符串常量池是全局共享的。位置就在第二张图里Interned String的位置,可以理解为在永生代里,方法区外面。后面会讲到,String.intern()方法,字符串驻留之后,引用就放在这个String Pool。

3.具体分析

如下面的Test.java文件,在主线程方法main里声明了一个字面量是"Hello"的字符串str。

1 package com.test.java.string;
2 class Test{
3      public void f(String s){...};
4      public static void main(String[] args){
5      String str = "Hello";
6      ...
7        }
8 }

编译成Test.class文件之后,如下图,除了版本、字段、方法、接口等描述信息外,还有一个也叫“常量池(Constant Pool Table)”的东西(淡绿色区块)。但这个常量池和内存里的常量池不一样。class文件里的常量池主要存两个东西:“字面量(Literal)”“符号引用量(Symbolic References)”。其中字面量就包括类中定义的一些常量,因为String是不可变的,由final关键字修饰,所以代码里的“Hello”字符串,就是作为字面量(常量)写在class的常量池里。



运行程序用到Test类的时候,Test.class文件的信息就会被解析到内存的方法区里。class文件里常量池里大部分数据会被加载到“运行时常量池”。但String不是。例子中的"Hello"的一个引用会被存到同样在Non Heap区的字符串常量池(String Pool)里。而“Hello”本体还是和所有对象一样,创建在Heap堆区。http://rednaxelafx.iteye.com/blog/774673文章里,测试的结果是在新生代的Eden区。但因为一直有一个引用驻留在字符串常量池,所以不会被GC清理掉。这个Hello对象会生存到整个线程结束。如下图所示,字符串常量池的具体位置是在过去说的永生代里,方法区的外面。



注意:这只是在Test类被类加载器加载时候的情形。主线程中的str变量这时候都还没有被创建,但 Hello的实例已经在Heap里了,对它的引用也已经在字符串常量池里了。

等主线程开始创建str变量的时候,虚拟机就会到字符串常量池里找,看有没有能equals("Hello")的 String。如果找到了,就在栈区当前栈帧的局部变量表里创建str变量,然后把字符串常量池里对Hello对 象的引用复制给str变量。找不到的话,才会在heap堆重新创建一个对象,然后把引用驻留到字符串常量区。然后再把引用复制栈帧的局部变量表。



如果我们当时定义了很多个值为"Hello"的String,比如像下面代码,有三个变量str1,str2,str3,也不会在 堆上增加String实例。局部变量表里三个变量统一指向同一个堆内存地址。

 1 package com.test.java.string;
 2 class Test{
 3      public void f(String s){...};
 4      public static void main(String[] args){
 5          String str1 = "Hello";
 6          String str2 = "Hello";
 7          String str3 = "Hello";
 8          ...
 9      }
10 }



上图中str1,str2,str3之间可以用==来连接。

但如果是用new关键字来创建字符串,情况就不一样了。

 1 package com.test.java.string;
 2 class Test{
 3         public void f(String s){...};
 4         public static void main(String[] args){
 5             String str1 = "Hello";
 6             String str2 = "Hello";
 7             String str3 = new String("Hello");
 8             ...
 9         }
10 }                        

这时候,str1和str2还是和之前一样。但str3因为new关键字会在Heap堆申请一块全新的内存,来创建新的对象。虽然字面还是"Hello",但是完全不同的对象,有不同的内存地址。



当然String#intern()方法让我们能手动检查字符串常量池,把有新字面值的字符串地址驻留到常量池里。

最后补充一下,JDK 7开始Hotspot把Interned String从PermGen移到Heap堆,JDK 8又彻底取消了 PermGen。但不管怎样,基本原理还是不变的。

总结:通过以上的分析,可以非常清楚的发现String两种赋值方式的区别,每次阅读都收益颇多。

by Shawn Chen 2018.3.20日,下午。

原文地址:https://www.cnblogs.com/morewindows0/p/8608410.html

时间: 2024-08-05 11:18:20

Java中String的两种赋值方式的区别的相关文章

关于String的两种赋值方式

String的两种赋值是不同的,String str1=“hello”,指向堆内存中的"hello",而String str2=new String("hello"),因为new开辟的新的堆内存,所以二者地址不同,在用==时,显示的是false. 例一: String str1=“Hello”; String str2=“Hello”; String str3=“Hello”; 这时候三者都是指向同一堆内存地址,因为如果对象池中已经有了相同的字符串声明时,就不会再重

细说java中Map的两种迭代方式

以前对java中迭代方式总是迷迷糊糊的,今天总算弄懂了,特意的总结了一下,基本是算是理解透彻了. 1.再说Map之前先说下Iterator: Iterator主要用于遍历(即迭代访问)Collection集合中的元素,Iterator也称为迭代器.它仅仅只有三个方法:hasNext(),next()和remove() hasNext():如果仍有元素可以迭代,则返回 true.(换句话说,如果 next 返回了元素而不是 抛出异常,则返回 true). next():返回迭代的下一个元素. re

Java中异常的两种处理方式

异常处理的两种方式: 声明抛出 throws  声明抛出的位置:是在方法声明的位置上使用throws关键字向上抛出异常. 捕捉 try....catch.. public class ExceptionTest03{     public static void main(String[] args){     //创建文件输入流读取文件     //思考:java编译器是如何知道以下的代码执行过程中可能出现异常,     //java编译器是如何知道这个异常发生的几率比较高呢?    //ja

String类两种实例化方式的区别

1.直接赋值方式:创建的对象存放到字符串对象池中,假如对象存在,则不会再创建 2.new对象方式:每次都会创建一个新的对象 原文地址:https://www.cnblogs.com/a591378955/p/8410748.html

java中线程的两种创建方式

第一种:继承java.lang.Thread类.然后重写run方法 例如我们模拟一个龟兔赛跑 1 package edu.aeon.thread; 2 3 /** 4 * 说明:模拟龟兔赛跑 5 * 6 * @author lzj 7 * 8 */ 9 public class RabbitThread extends Thread { 10 @Override 11 public void run() { 12 for (int i = 1; i <= 100; i++) { 13 Syste

java中多线程的两种创建方式

一丶继承Thread类实现多线程 第一步:继承Thread类第二步:重写run()方法第三步:创建继承了Thread类的对象 , 调用start()方法启动. //线程创建方式一 : /* 第一步:继承Thread类 第二步:重写run()方法 第三步:创建继承了Thread类的对象 , 调用start()方法启动. */ public class TestThread extends Thread{ @Override public void run() { for (int i = 0; i

简述java中抛出异常的两种方式

java编程中经常遇到异常,这时就需要利用java中的异常抛出机制,在java中提供了两种抛出异常的方法:try{}  catch() {}和throw. 一.抛出异常的两种方式 (1) 首先我们来看一下try()  catch(){}这种方式: ? 1 2 3 4 5 6 try{    i=9\0; } catch(exception e) {     system.out.println("除数不能为0"): } 该种方式是将待执行的代码放入try中,如果执行的代码发生异常就会被

述java中抛出异常的两种方式

java编程中经常遇到异常,这时就需要利用java中的异常抛出机制,在java中提供了两种抛出异常的方法:try{}  catch() {}和throw. 一.抛出异常的两种方式 (1) 首先我们来看一下try()  catch(){}这种方式: ? 1 2 3 4 5 6 try{    i=9\0; } catch(exception e) {     system.out.println("除数不能为0"): } 该种方式是将待执行的代码放入try中,如果执行的代码发生异常就会被

java中super 的两种用法

通过用 static 来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于 C 语言中的全局函数和全局变量.但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的. 好了,现在开始讨论 this&super 这两个关键字的意义和用法. 在 Java 中, this 通常指当前对象, super 则指父类的.当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用