java内存优化:使用String.intern()节省java堆内存

一,什么是字符串常量池?

A pool of strings, initially empty, is maintained privately by the class String.一个由String类私有维持的初始化为空的字符串池

 

JDK1.6前,字符串常量池像运行时常量池一样,属于方法区(永久代,Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为“永久代”(Permanent Generation));

 

JDK1.7开始,字符串常量池被从永久代(方法区)中移除,HotSpot VM里,成为记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。是个纯运行时的结构,而且是惰性(lazy)维护的 ,满足java动态解析。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。


二,String的intern()方法有什么作用?

假设,有字符串:a

   JDK1.6——a.intern(),由于字符串常量池位于“永久代”,会把首次遇到的字符串(equals())的实例clone到永久代,并返回该引用,否则直接返回已存在实例的引用。注意:返回的引用都是指向永久代中实例的引用

 

   JDK1.7——a.intern(),字符串常量池从方法区移除,被全局表StringTable(本质HashSet<String>)代替,会把首次遇到的字符串(equals())的引用添加到StringTable,并返回此引用,否则直接返回StringTable中已持有的引用。注意:返回的引用都指向heap堆

 

三,使用 “”(double quotation marks)与 new String("")初始化字符串变量的区别?

    1, 无论是 double quotation marks(" ") 还是 new String("") 的字符串字面量,在编译期都被存储到Class文件常量池,由CONSTANT_String常项(持有index,由index指向的CONSTANT_Utf8常项(持有实际文本内容。在类加载过程加载阶段通过验证阶段文件格式验证动作,将二进制字节流读入方法区存储结构—— 即,将Class文件常量池存储为方法区运行时常量池

 

2,对象的创建需要诸如 ldc、new 等字节码指令的执行。所以,存储在运行时常量池中的字面量,一般不会在类加载时heap堆创建实例并添加引用到StringTable。注意:ldc指令用于将int、float等基本类型及String型字面量从运行时常量池中推送至栈顶

 

3,ldc指令执行时遇到String类型字面量,首先会查看字符串常量池StringTable中有无此字符串对象的引用(equals()),如果有,则返回此引用;否则在heap堆创建新的字符串对象,将引用驻留在StringTable,再返回此引用。此过程类似于,String.intern()的执行过程。所以有:

String s1=new String("he")+new String("llo"); // 执行StringBuilder().append().toString(){ new String() }在堆中创建新的对象,引用赋值s1

s1=s1.intern();   // 仍然返回s1原本的引用,因为StringTable在此之前还没有驻留 “hello”的引用
    String s2="hello";  // "hello"在类加载过程中并未发生解析,Runtime程序执行开始时,StringTable中并没有该实例的引用

 

String s1="hello"; 近似于String s1=new String("hello"); s1=s1.intern();

 

③ String s1=new String("hello");s1=s1.intern();首先执行ldc指令“hello”,创建实例驻留引用在字符串常量池StringTable,再执行new指令创建对象在heap堆中,所以s1=s1.intern();返回的引用不是s1的引用,而是“hello”驻留在StringTable中的引用;

 

4,类加载过程包含加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

 

5,特殊的:

① Static类变量,在类加载过程中的准备阶段被加载到方法区,赋初值(零值),初始化阶段执行<clinit>()方法,被初始化;

 

②如果Static类变量没有被final修饰①,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化;

 

③ final 只有在修饰Static类变量时, 编译器Javac才会为之生成ConstantValue属性,在准备阶段加载到方法区的时候,虚拟机就会根据ConstantValue设置的值,直接初始化;

 

④ 实例变量将会在实例构造器 <init>() 方法中被初始化!……实例构造器<init>()方法和类构造器<clinit>()方法,都是在编译期字节码生成阶段添加到语法树中的(注意,这里的实例构造器不是指默认构造函数,如果用户代码中没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性(public、protected或private)与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成);

 

⑤ 在类编译的字节码生成阶段,还做了一些代码替换工作用于优化程序的实现逻辑!……所以有

(1)String s1=new String("he")+new String("llo"); // 执行StringBuilder().append().toString(){ new String() }在堆中创建新的对象,引用赋值s1;

 

(2)String s1="he"+"llo"; // 编译阶段,被优化为“hello”,同时与“he”和“llo”以字面量形式存储在常量池,执行阶段等效于

String s1="hello";

 

(3)String s1=new String("he")+"llo"; // 执行StringBuilder().append().toString(){ new String() }在堆中创建新的对象,引用赋值s1;

 

(4)String s=new String("he");String s1=s+"llo"; // 执行StringBuilder().append().toString(){ new String() }在堆中创建新的对象,引用赋值s1;

 

(5)String s="he";String s1=s+"llo"; // 执行StringBuilder().append().toString(){ new String() }在堆中创建新的对象,引用赋值s1;

 

最后:我们可以通过 String.intern(),重用字符串常量池StringTable的引用,释放String在heap堆中新创建的实例的引用,使该实例进入到GC回收期,回收引用指向的堆中内存,以此优化堆内存

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章