String应用简介

前言

String字符串在Java应用中使用非常频繁,只有理解了它在虚拟机中的实现机制,才能写出健壮的应用,本文使用的JDK版本为1.8.0_111。

常量池

Java代码被编译成class文件时,会生成一个常量池(Constant pool)的数据结构,用以保存字面常量和符号引用(类名、方法名、接口名和字段名等)。

很简单的一段代码,通过命令 javap -verbose 查看class文件中 Constant pool 实现:

 

通过反编译出来的字节码可以看出字符串 "test" 在常量池中的定义方式:

在main方法字节码指令中,0 ~ 2行对应代码 String test = "test"; 由两部分组成:ldc #16和 astore_1。

 

1、Test类加载到虚拟机时,"test"字符串在Constant pool中使用符号引用symbol表示,当调用 ldc #16 指令时,如果Constant pool中索引 #16 的symbol还未解析,则调用C++底层的 StringTable::intern 方法生成char数组,并将引用保存在StringTable和常量池中,当下次调用 ldc #16 时,可以直接从Constant pool根据索引 #16获取 "test" 字符串的引用,避免再次到StringTable中查找。

2、astore_1指令将"test"字符串的引用保存在局部变量表中。

常量池的内存分配
在 JDK6、7、8中有不同的实现:

1、JDK6及之前版本中,常量池的内存在永久代PermGen进行分配,所以常量池会受到PermGen内存大小的限制。

2、JDK7中,常量池的内存在Java堆上进行分配,意味着常量池不受固定大小的限制了。

3、JDK8中,虚拟机团队移除了永久代PermGen。

 

字符串初始化

字符串可以通过两种方式进行初始化:字面常量和String对象。

字面常量

通过 "javap -c" 命令查看字节码指令实现:

 

其中ldc指令将int、float和String类型的常量值从常量池中推送到栈顶,所以a和b都指向常量池的"java"字符串。通过指令实现可以发现:变量a、b和c都指向常量池的 "java" 字符串,表达式 "ja" + "va" 在编译期间会把结果值"java"直接赋值给c(编译后指向常量)。

String对象

这种情况下,a == c 成立么?字节码实现如下:

 

 

其中3 ~ 9行指令对应代码 String c = new String("java"); 实现:

1、第3行new指令,在Java堆上为String对象申请内存;

2、第7行ldc指令,尝试从常量池中获取"java"字符串,如果常量池中不存在,则在常量池中新建"java"字符串,并返回;

3、第9行invokespecial指令,调用构造方法,初始化String对象。

其中String对象中使用char数组存储字符串,变量a指向常量池的"java"字符串,变量c指向Java堆的String对象,且该对象的char数组指向常量池的"java"字符串,所以很显然 a != c,如下图所示:

 

通过 "字面量 + String对象" 进行赋值会发生什么?

这种情况下,c == d成立么?字节码实现如下:

 

其中6 ~ 21行指令对应代码 String c = a + b; 实现:

1、第6行new指令,在Java堆上为StringBuilder对象申请内存;

2、第10~14行aload_1指令,装载a字符串hello,调用构造方法,初始化StringBuilder对象;(这里本应是两次append,是不是在init时做了优化?)

3、第18行invokevirtual指令,调用append方法,添加b字符串;

4、第21行invokevirtual指令,调用toString方法,生成String对象。

通过指令实现可以发现,字符串变量的连接动作,在编译阶段会被转化成StringBuilder的append操作,变量c最终指向Java堆上新建String对象,变量d指向常量池的"hello world"字符串,所以 c != d。

不过有种特殊情况,当final修饰的变量发生连接动作时,虚拟机会进行优化,将表达式结果直接赋值给目标变量:

指令实现如下:

 

如果其中有不是final的字符串

 

String.intern()原理

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

在 JDK6 和 JDK7 中结果不一样:

1、JDK6的执行结果:false false
对于这个结果很好理解。在JDK6中,常量池在永久代分配内存,永久代和Java堆的内存是物理隔离的,执行intern方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用,所以需要谨慎使用intern方法,避免常量池中字符串过多,导致性能变慢,甚至发生PermGen内存溢出。

2、JDK7的执行结果:true false
对于这个结果就有点懵了。在JDK7中,常量池已经在Java堆上分配内存,执行intern方法时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回,所以在JDK7中,可以重新考虑使用intern方法,减少String对象所占的内存空间。

对于变量s1,常量池中没有 "StringTest" 字符串,s1.intern() 和 s1都是指向Java对象上的String对象。
对于变量s2,常量池中一开始就已经存在 "java" 字符串,所以 s2.intern() 返回常量池中 "java" 字符串的引用。

 

Attila Szegedis 在他讲述 JVM 知识的文档中一直强调,清楚知道内存中存储的数据量是非常重要的。我一开始感到十分惊讶,因为一般情况下,在企业开发中并不是经常需要关注对象的大小。他对此给出了 Twitter 的一个例子。

先思考一个内存占用的问题:字符串 "Hello World" 会占用多少字节内存?

答案:在 32 位虚拟机上是 62 字节,在 64 位虚拟机上是 86 字节。

分别为 8/16 (字符串的对象头) + 11 * 2 (字符) + [8/16 (字符数组的对象头) + 4 (数组长度),加上字节对齐所需的填充,共为 16/24 字节] + 4 (偏移) + 4 (偏移长度) + 4 (哈希码) + 4/8 (指向字符数组的引用)【在 64 位虚拟机上,String 对象的内存占用会因为字节对齐而填充为 40 字节】

使用 String intern() 方法

intern 的目的在于复用字符串对象以节省内存。

在明确知道一个字符串会出现多次时才使用 intern(),并且只用它来节省内存。

如何确定 intern 的效率

最好的方法是对整个堆执行一次堆转储。堆转储也会在发生 OutOfMemoryError 时执行。

在 MAT (内存分析工具,译者注)中打开转储文件,然后选择 java.lang.String,依次点击"Java Basics"、"Group By Value"。

根据堆的大小,上面的操作可能耗费比较长的时间。最后可以看到类型这样的结果。按 "Retained Heap" 或者是 "Objects" 列进行排序,可以发现一些有趣的东西:

从这快照中我们可以看到,空的字符串占用了大量的内存!两百万个空字符串对象占用了总共 130 MB 的空间。另外可以看到一部分被加载的 JavaScript 脚本,一些作为键的字符串,它们被用于定位。另外,还有一些与业务逻辑相关的字符串。

这些与业务逻辑相关的字符串是最容易进行 intern 操作的,因为我们清楚地知道它们是在什么地方被加载进内存的。对于其他字符串,可以通过 "Merge shortest Path to GC Root" 选项来找到它们被存储的位置,这个信息也许能够帮助我们找到该使用 intern 的地方。

intern 的利弊

既然 intern() 方法有这些好处,为什么不经常使用呢?原因在于它会降低代码效率。下面给出一个例子:

代码中使用了字符串数组来维护到字符串对象的强引用,另外我们还打印了数组的第一个元素来避免数组由于代码优化而将数组给销毁了。接着从数据库加载 10 个不同的字符串,但在这里我使用了 new String() 来创建一个临时的字符串,这和从数据库里读是一样的。最后我们调用了系统的 GC() 方法,这样就能排除其他不相关对象的影响,保证结果的正确。 在 64 位,8 G 内存,i5-2520M 处理器的 Windows 系统上运行上面的代码, 环境为 JDK 1.6.0_27,指定虚拟机参数 -XX:+PrintGCDetails -Xmx6G -Xmn3G 记录垃圾回收日志。结果如下:

没有使用 intern() 方法的结果:

使用了 intern() 方法的结果:

可以看到结果差别十分的大。在使用 intern() 方法的时候,程序耗时多了 3 秒,但节省了很大一块内存。使用 intern() 方法的程序占用了 253472K(250M) 内存,而不使用的占用了 2397635K (2.4G)。从这些可以看出使用 intern 的利弊。

 

 

 

String in JDK6,7 and 8

String.intern() in Java 6

在JAVA6中字符串池最大的问题是他的位置-永久代.永久代具有固定尺寸并且在运行时不能被扩展.它能使用参数 -XX:MaxPermSize=96m。 据我所知, 永久代默认的大小位于32M至96M间依赖平台.你能增大尺寸.但是字符串池的尺寸依然是固定的.这个限制需要我我们小心的使用String.intern-你最好对不能控制的字符不要使用intern这方法. 这是为什么在JAVA6中大部分使用手动管理map来实现字符串池

String.intern() in Java 7

Oracle 工程师在java7中对字符串池作了一个极其重要的决定-把字符串池移动到堆中.意

字符串池是使用一个拥有固定容量的hashmap,
默认的池大小是1009.(出现在上面提及的bug 报告的源码中).是一个常量在JAVA6早期版本中,随后在java6_30至java6_41中开始为可配置的.而在java 7中一开始就是可以配置的(至少在java7_02中是可以配置的).你需要指定参数 -XX:StringTableSize=N, N是字符串池map的大小. 确宝他是一个为更好的性能预先准备的数字.(不要使用1,000,000 作为-XX:StringTaleSize 的值 - 它不是质数;使用1,000,003代替)

String.intern() in Java 8

java8依然接受 -XX:StringTableSize. 提供可以与JAVA7媲美的性能. 唯一不同的是默认的池大小增加到25-50K

使用-XX:StringTableSize 参数在JAVA7和8中设置字符串池的大小.它是固定的.因为他的实现是一个由桶带链表组成的hashmap.靠近这个数并且设置池的大小等于靠近这个数的质数.他会使String.intern运行在一个常量时间里并且只需要消耗相当小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)

intern 原理

intern() 方法需要传入一个字符串对象(已存在于堆上),然后检查 StringTable 里是不是已经有一个相同的拷贝。StringTable 可以看作是一个 HashSet,它将字符串分配在永久代上。StringTable 存在的唯一目的就是维护所有存活的字符串的一个对象。如果在 StringTable 里找到了能够找到所传入的字符串对象,那就直接返回它,否则,把它加入 StringTable :

 

Java中String的设计的更多相关文章

  1. 在java中String类为什么要设计成final

    在java中String类为什么要设计成final? - 胖胖的回答 - 知乎 https://www.zhihu.com/question/31345592/answer/114126087

  2. 在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?

    最近突然被问到String为什么被设计为不可变,当时有点懵,这个问题一直像bug一样存在,竟然没有发现,没有思考到,在此总结一下. 1.String的不可变String类被final修饰,是不可继承和 ...

  3. 在java中String类为什么要设计成final?

    大神链接:在java中String类为什么要设计成final? - 程序员 - 知乎 我进行了重新排版,并且更换了其中的一个例子,让我们更好理解. String很多实用的特性,比如说“不可变性”,是工 ...

  4. Java中String类为什么被设计为final?

    Java中String类为什么被设计为final   首先,String是引用类型,也就是每个字符串都是一个String实例.通过源码可以看到String底层维护了一个byte数组:private f ...

  5. Java中String、StringBuffer、StringBuilder区别与理解

    一.先比较String.StringBuffer.StringBuilder变量的HashCode值 使用System.out.println(obj.hashcode())输出的时对象的哈希码, 而 ...

  6. java中String的相等比较

    首先贴出测试用例: package test; import org.junit.Test; /** * Created by Administrator on 2015/9/16. * */ pub ...

  7. java中String相等问题

    java中判断两个字符串是否相等的问题   判断两个字符串是否相等的问题.在编程中,通常比较两个字符串是否相同的表达式是"==",但在java中不能这么写.在java中,用的是eq ...

  8. 【转载】 Java中String类型的两种创建方式

    本文转载自 https://www.cnblogs.com/fguozhu/articles/2661055.html Java中String是一个特殊的包装类数据有两种创建形式: String s ...

  9. java中String类为什么不可变?

    在面试中经常遇到这样的问题:1.什么是不可变对象.不可变对象有什么好处.在什么情景下使用它,或者更具体一点,java的String类为什么要设置成不可变类型? 1.不可变对象,顾名思义就是创建后的对象 ...

随机推荐

  1. 8 Most Required Examples Reference For Oracle Forms

    Check the following 8 Links for best Oracle Forms examples with source code (Fmb files), which will ...

  2. 说说GestureDetector.OnGestureListener onScroll函数

    public abstract boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) ...

  3. python模块相关

    aniso8601 pyquery networkx (2.0)                 - Python package for creating and manipulating grap ...

  4. linux中seq命令用法

    NAME seq - print a sequence of numbers SYNOPSIS seq [OPTION]... LAST seq [OPTION]... FIRST LAST seq ...

  5. kill -signal

    1. SIGHUP 启动被终止的进程,可让该PID重新读取配置文件,类似于重启服务 对应的数字为1 9.SIGTERM 以正常的结束进程来终止进程 15.SIGSTOP 暂停一个进程相当于crtl+z

  6. es6 - 模板

    'use strict'; // es5 let name = 'mrs'; let qb = 20; function logs() { return 'goods!'; } let html = ...

  7. HTTP基础(整理)

    前一段时间看了有关这个协议的相关文档,对这个协议有了新的理解,这里整理一下. http是应用层面向对象的协议. 它有以下几个特点: 1.  支持客户服务器模式(这是废话,不支持这个模式怎么工作) 2. ...

  8. linux安装svn客户端subversion及使用方法

    1.下载 [maintain@HM16-213 software]$ wget http://subversion.tigris.org/downloads/subversion-deps-1.6.1 ...

  9. Linux进程间通信(IPC)编程实践(十二)Posix消息队列--基本API的使用

    posix消息队列与system v消息队列的区别: (1)对posix消息队列的读总是返回最高优先级的最早消息,对system v消息队列的读则能够返回随意指定优先级的消息. (2)当往一个空队列放 ...

  10. oracle不同用户间访问表不添加用户名(模式)前缀

    默认的情况下,oracle里面的用户A,要访问用户B的表需要带用户B的前缀,如访问用户B的 user表,需要这样访问 select * from B.user;如果想要不添加用户前缀,需要这样处理: ...