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。
- package com.test.java.string;
- class Test{
- public void f(String s){...};
- public static void main(String[] args){
- String str = "Hello";
- ...
- }
- }
编译成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实例。局部变量表里三个变量统一指向同一个堆内存地址。
- package com.test.java.string;
- class Test{
- public void f(String s){...};
- public static void main(String[] args){
- String str1 = "Hello";
- String str2 = "Hello";
- String str3 = "Hello";
- ...
- }
- }
上图中str1,str2,str3之间可以用==来连接。
但如果是用new关键字来创建字符串,情况就不一样了。
- package com.test.java.string;
- class Test{
- public void f(String s){...};
- public static void main(String[] args){
- String str1 = "Hello";
- String str2 = "Hello";
- String str3 = new String("Hello");
- ...
- }
- }
这时候,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日,下午。
Java中String对象两种赋值方式的区别的更多相关文章
- 关于String的两种赋值方式
String的两种赋值是不同的,String str1=“hello”,指向堆内存中的"hello",而String str2=new String("hello&quo ...
- Java中String类两种实例化的区别(转)
原文:http://blog.csdn.net/wangdajiao/article/details/52087302 一.String类的第一种方式 1.直接赋值 例:String str = &q ...
- 细说java中Map的两种迭代方式
曾经对java中迭代方式总是迷迷糊糊的,今天总算弄懂了.特意的总结了一下.基本是算是理解透彻了. 1.再说Map之前先说下Iterator: Iterator主要用于遍历(即迭代訪问)Collecti ...
- Java中容器的两种初始化方式比较
List,Set,Map的两种初始化赋值方式 List List<Integer> list2 = new ArrayList<Integer>(); for (int i= ...
- java中多线程的两种创建方式
一丶继承Thread类实现多线程 第一步:继承Thread类第二步:重写run()方法第三步:创建继承了Thread类的对象 , 调用start()方法启动. //线程创建方式一 : /* 第一步:继 ...
- JAVA高级架构师基础功:Spring中AOP的两种代理方式:动态代理和CGLIB详解
在spring框架中使用了两种代理方式: 1.JDK自带的动态代理. 2.Spring框架自己提供的CGLIB的方式. 这两种也是Spring框架核心AOP的基础. 在详细讲解上述提到的动态代理和CG ...
- String 的两种实例化方式
String 的两种实例化方式 隐式实例化:直接赋值 public class Demo { public static void main(String[] args) { String s = & ...
- 【转载】JAVA中线程的两种实现方法-实现Runnable接口和继承Thread类
转自: http://blog.csdn.net/sunguangran/article/details/6069317 非常感谢原作者,整理的这么详细. 在java中可有两种方式实现多线程,一种是继 ...
- Java中String对象的不可变性
首先看一个程序 package reverse; public class Reverse { public static void main(String[] args) { String c1=n ...
随机推荐
- 操作数据库出现InvalidOpertionException(内部连接致命错误)
用DataTables时并发访问量较大,单个任务操作(获取数据)时间较长.连接数过多的时候就出现InvalidOpertionException错误.知道哪里有问题那就好办了,对GetDataTabl ...
- Linux服务器评测脚本 中文IO脚本简单易懂
中文版: wget -N --no-check-certificate https://raw.githubusercontent.com/FunctionClub/ZBench/master/ZBe ...
- T-SQL基础(四)之集合运算
三个运算符 T-SQL支持三个集合运算符:UNION.INTERSECT.EXCEPT. 集合运算符查询的一般形式如下: Query1 <set_operator> Query2 -- 这 ...
- 浅谈spring中AOP以及spring中AOP的注解方式
AOP(Aspect Oriented Programming):AOP的专业术语是"面向切面编程" 什么是面向切面编程,我的理解就是:在不修改源代码的情况下增强功能.好了,下面在 ...
- iphone屏幕镜像 屏幕镜像怎么用
iPhone手机投屏到电脑设备上,需要使用到AirPlay镜像功能,但是有些苹果新用户朋友一定还不知道iphone屏幕镜像怎么用吧?你要着急下面为你提供解决方法. 使用工具:iphone.电脑 操作方 ...
- redis中各种数据类型的常用操作方法汇总
在spring中使用jedisTemplate操作,详见https://www.cnblogs.com/EasonJim/p/7803067.html 一.Redis的五大数据类型 1.String( ...
- Loadrunner 脚本开发-利用loadrunner开发Windows Sockets协议脚本
脚本开发-利用loadrunner开发Windows Sockets协议脚本 by:授客 QQ:1033553122 欢迎加入软件性能测试交流QQ群:7156436 实践举例 Socket服务端简单实 ...
- python语言学习---3
第四天 1.set 持有一系列元素,这一点和 list 很像,但是set的元素没有重复,而且是无序的, 这点和 dict 的 key很像. (不信可以输出下试试 ~-~ )另外,其存储的对象必须不可变 ...
- 如何在数据表当中找出被删掉的数据行ID
这个问题是一年前我刚步入IT行业的一个面试题,当时抓破头皮都想不到的问题,但现在回想过去自身不禁感到可笑,不多扯直接写解决方案.如何在数据表当中找出被删掉的数据行ID,意思是:在一堆的数据当中,让你找 ...
- linux 下svn操作
* 前言: linux下的svn相比于gitlab,配置要求第一点:gitlab需要4G的内存,如果使用swap+内存的替代方案,理论上是可行的,但是实际操作中各种坑: 所以,由于条件限制,使 ...