jvm运行时数据区

  |   0 评论   |   0 浏览

JVM运行时数据区规范

image.png

jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。

jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。

方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot 虚拟机不同版本的

两种实现。

Hotspot运行时数据区

image.png

分配JVM内存空间

分配堆的大小

–Xms(堆的初始容量) -Xmx(堆的最大容量)

分配方法区的大小

-XX:PermSize 永久代的初始容量 -XX:MaxPermSize 永久代的最大容量
-XX:MetaspaceSize 元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 
-XX:MaxMetaspaceSize 最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 
-XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

分配线程空间的大小

-Xss:为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

方法区

方法区存储内容

存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等。

存储示意图如下,下面的图片显示的是JVM加载类的时候,方法区存储的信息:
image.png

1、类型信息

类型的全限定名

超类的全限定名

直接超接口的全限定名

类型标志(该类是类类型还是接口类型)

类的访问描述符(public、private、default、abstract、final、static)

2、类型的常量池

存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类

型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为

常量池中保存着所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在

动态链接中起到核心作用)。

3、字段信息(该类声明的所有字段)

字段修饰符(public、protect、private、default)

字段的类型

字段名称

4、方法信息

方法信息中包含类的所有方法,每个方法包含以下信息:

方法修饰符

方法返回类型

方法名

方法参数个数、类型、顺序等

方法字节码

操作数栈和该方法在栈帧中的局部变量区大小

异常表

5、类变量(静态变量)

指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。

6、指向类加载器的引用

每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。

7、指向Class实例的引用

类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过

Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。

8、方法表

为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能

调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。

9、运行时常量池(Runtime Constant Pool)

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编

译器生成的各种字面常量和符号引用,这部分内容被类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池

中(典型的如String类的intern()方法)。

永久代和元空间区别

永久代和元空间存储位置和存储内容的区别:

  • 存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;
  • 存储内容不同,元空间存储类的元信息,[静态变量]和[常量池]等并入堆中。相当于永久代的数据被分到

了堆和元空间中。

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?带着这个疑问,最后给大家总结以下几点原因:

  1. 字符串存在永久代中,容易出现性能问题和永久代内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢

出,太大则容易导致老年代溢出。

  1. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  2. Oracle 可能会将HotSpot 与 JRockit 合二为一。

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

方法区异常演示

类加载导致OOM异常

我们现在通过动态生成类来模拟方法区的内存溢出:

package club.jvm;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class methodarea {
    public static void main(String[] args) {
        URL url = null;
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
        try {
            url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true) {
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                //这里的club.jvm.classloader.Test 随便指定一个类
                loader.loadClass("club.jvm.classloader.Test");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在jdk1.7下面运行指定永久代(方法区) -XX:PermSize3m -XX:MaxPermSize=3m 会报错

java.lang.OutOfMemoryError: PermGen space

这里的 “ PermGen space ”其实指的就是方法区。由于方法区主要存储类的相关信息,所以对于

动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,

容易出现永久代内存溢出。

在jdk1.8 运行指定元空间(方法区)大小 : -XX:MetaspaceSize=3M -XX:MaxMetaspaceSize=3M

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

字符串OOM异常

以下这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存:

package club.jvm;

import java.util.ArrayList;
import java.util.List;

public class StringOomMock {
    static String base = "string";

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}

运行时候指定参数: -XX:PermSize=8m -XX:MaxPermSize=8m -Xms16m -Xmx16m

jdk1.6会报:

Java.lang.OutOfMemoryError: PermGen space 永久代溢出.

jdk1.7会报

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 堆溢出,

结论是: ==jdk1.7中已经将字符串常量从永久代转移到堆中==。

Jdk1.8也是堆溢出

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,同时会提示 PermSize 和 MaxPermSize 已经无效. 因此可以验证==jdk1.8中已经不存在永久代==的结论。

运行时常量池和字符串常量池

存储内容

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

字面量:
	双引号引起来的字符串值,“kkb”
	定义为final类型的常量的值。
符号引用:
	类或接口的全限定名(包括他的父类和所实现的接口)
	变量或方法的名称
	变量或方法的描述信息
			方法的描述:参数个数、参数类型、方法返回类型等等
			变量的描述信息:变量的返回值
	this

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

存储位置

在JDK1.6及以前,运行时常量池是方法区的一部分。

在JDK1.7及以后,运行时常量池在Java 堆(Heap)中。

运行时和class常量池一样,运行时常量池也是每个类都有一个。但是字符串常量池全只有一个

常量池区别

class常量池(静态常量池)、运行时常量池、字符串常量池区别:

  • class常量池中存储的是符号引用,而运行时常量池存储的是被解析之后的直接引用。
  • class常量池存在于class文件中,运行时常量池和字符串常量池是存在于JVM内存中。
  • 运行时常量池具有动态性,java运行期间也可能将新的常量放入池中(String#intern()),
  • 字符串常量池逻辑上属于运行时常量池的一部分,但是它和运行时常量池的区别在于,字符串常量

池是全局唯一的,而运行时常量池是每个类一个。

字符串常量池如何存储数据

实际上,为了提高匹配速度,即更快的查找某个字符串是否存在于常量池,Java在设计字符串常量池的时候,还搞了一张stringtable, stringtable 有点类似于我们的hashtable,里面保存了字符串的引用。

在jdk6中 StringTable 的长度是固定的,就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长。此时当调用 String.intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降;

在jdk7+, StringTable 的长度可以通过一个参数指定:-XX:StringTableSize=99991

stringtable是类似于hashtable的数据结构,hashtable数据结构如下:
image.png

字符串常量池查找字符串的方式:

  • 根据字符串的 hashcode 找到对应entry。如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串。
  • 如果找得到字符串,返回引用。如果找不到字符串,会把字符串放到常量池,并把引用保存到stringtable里。image.png

字符串常量池介绍

上面我们已经稍微了解过字符串常量池了,它是java为了节省空间而设计的一个内存区域,java中所有的类共享一个字符串常量池。 比如A类中需要一个“hello”的字符串常量,B类也需要同样的字符串常量,他们都是从字符串常量池中获取的字符串,并且获得得到的字符串常量的地址是一样的。

字符串常量池案例分析

  1. 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中。
  2. 使用new String(“”)创建的对象会存储到heap中,是运行期新创建的。
  3. 使用只包含常量的字符串连接符如”aa”+”bb”创建的也是常量,编译期就能确定已经存储到String

Pool中。

  1. 使用包含变量的字符串连接如”aa”+s创建的对象是运行期才创建的,存储到heap中。
  2. 运行期调用String的intern()方法可以向String Pool中动态添加对象。
public class Test {
    @org.junit.Test
    public  void test() {
        String str1 = "abc";
        String str2 = new String("abc");
        System.out.println("结果是false"+(str1 == str2));
        String str3 = new String("abc");
        System.out.println("结果是false" + (str3 == str2));
        String str4 = "a" + "b";
        System.out.println( "结果是true" + (str4 == "ab"));



        final String s = "a";
        String str5 = s + "b";
        System.out.println("结果是true" + (str5 == "ab"));
        String s111 = "a";
        String str15 = s111 + "b";
        System.out.println("结果是false" + (str15 == "ab"));


        String s1 = "a";
        String s2 = "b";
        String str6 = s1 + s2;
        System.out.println((str6 == "ab")+ "结果是false");
        String str7 = "abc".substring(0,2); System.out.println((str7 == "ab")+ "结果是false");
        String str8 = "abc".toUpperCase();
        System.out.println((str8 == "ABC")+ "结果是false");
        String s3 = "abc";
        String s4 = "ab" + getString();
        System.out.println((s3 == s4)+ "结果是是false");
        String s5 = "a";
        String s6 = "abc";
        String s7 = s5 + "bc";
        System.out.println((s6 == s7.intern())+ "结果是true");
    
        System.out.println("========以下====String的intern()方法============");
        String a = "hello";
        String b = new String("hello");
        System.out.print("结果是false");
        System.out.println(a == b);
    
   
        String c = "world";
        System.out.print("结果是true");
        System.out.println(c.intern() == c);
    
       /**
      	*因为有双引号括起来的字符串,所以会把ldc命令,即"mike"会被我们添加到字符串常量池,它
     	 	*的引用是string的char数组的地址,会被我们添加到stringtable中。所以d.intern的时候,
				*返回的其实是string中的char数组的地址,和d的string实例化地址肯定是不一样的。
      	**/
        String d = new String("mike");
        System.out.print("结果是false");
        System.out.println(d.intern() == d);

        /**
         * new String("jo") + new String("hn")实际上会转为stringbuffer的append 然后
         * tosring()出来,实际上是new 一个新的string出来。在这个过程中,并没有双引号括起john,
         * 也就是说并不会执行ldc然后把john的引用添加到stringtable中,所以intern的时候实际就是
         * 把新的string地址(即e的地址)添加到stringtable中并且返回回来。
         */
        String e = new String("jo") + new String("hn");
        System.out.print("结果是true");
				System.out.println(e.intern() == e);
    
      /**
      *或许很多朋友感觉很奇怪,这跟上面的例子2基本一模一样,但是却是false呢?这是因为java在启动的时候,			*会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添			*加到字符串常量池中的”java“的引用,所以肯定跟f的原地址不一样。
      */
        String f = new String("ja") + new String("va");
        System.out.print("结果是false");
        System.out.println(f.intern() == f);
    }

    private  String getString() {
        return "c";
    }
}

分析1

String str1 = "abc"; 
System.out.println(str1 == "abc");
步骤: 
1) 栈中开辟一块空间存放引用str1, 
2) String池中开辟一块空间,存放String常量"abc",
3) 引用str1指向池中String常量"abc", 
4) str1所指代的地址即常量"abc"所在地址,输出为true

分析2

String str2 = new String("abc"); 
System.out.println(str2 == "abc");
步骤: 
1) 栈中开辟一块空间存放引用str2, 
2) 堆中开辟一块空间存放一个新建的String对象"abc", 
3) 引用str2指向堆中的新建的String对象"abc", 
4) str2所指代的对象地址为堆中地址,而常量"abc"地址在池中,输出为false

分析3

String str2 = new String("abc"); 
String str3 = new String("abc"); 
System.out.println(str3 == str2);
步骤: 
1) 栈中开辟一块空间存放引用str3, 
2) 堆中开辟一块新空间存放另外一个(不同于str2所指)新建的String对象, 
3) 引用str3指向另外新建的那个String对象 
4) str3和str2指向堆中不同的String对象,地址也不相同,输出为false

分析4

String str4 = "a" + "b"; 
System.out.println(str4 == "ab");
步骤: 
1) 栈中开辟一块空间存放引用str4,
2) 根据编译器合并已知量的优化功能,池中开辟一块空间,存放合并后的String常量"ab", 
3) 引用str4指向池中常量"ab",
4) str4所指即池中常量"ab",输出为true

分析5

final String s = "a"; 
String str5 = s + "b";
System.out.println(str5 == "ab");
步骤: 同4

分析6

String s1 = "a"; 
String s2 = "b"; 
String str6 = s1 + s2; 
System.out.println(str6 == "ab");
步骤: 
1) 栈中开辟一块中间存放引用s1,s1指向池中String常量"a", 
2) 栈中开辟一块中间存放引用s2,s2指向池中String常量"b", 
3) 栈中开辟一块中间存放引用str6, 
4) s1 + s2通过StringBuilder的最后一步toString()方法还原一个新的String对象"ab",因此 堆中开辟一块空间存放此对象, 
5) 引用str6指向堆中(s1 + s2)所还原的新String对象, 
6) str6指向的对象在堆中,而常量"ab"在池中,输出为false

分析7

String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");
步骤: 
1) 栈中开辟一块空间存放引用str7, 
2) substring()方法还原一个新的String对象"ab"(不同于str6所指),堆中开辟一块空间存放此 对象, 
3) 引用str7指向堆中的新String对象, 输出false

分析8

String str8 = "abc".toUpperCase(); 
System.out.println(str8 == "ABC");
步骤: 
1) 栈中开辟一块空间存放引用str8, 
2) toUpperCase()方法还原一个新的String对象"ABC",池中并未开辟新的空间存放String常 量"ABC", 
3) 引用str8指向堆中的新String对象,输出false

String的Intern方法详解

intern的作用

intern的作用是把new出来的字符串的引用添加到stringtable中,java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了,如果有返回引用(地址),然后没有把字符串的地址放到stringtable中,并返回字符串的引用(地址)。

String a = new String("haha"); 
System.out.println(a.intern() == a);//false

因为有双引号括起来的字符串,所以会把ldc命令,即"haha"会被我们添加到字符串常量池,它的引用是string的char数组的地址,会被我们添加到stringtable中。所以a.intern的时候,返回的其实是string中的char数组的地址,和a的string实例化地址肯定是不一样的。

String e = new String("jo") + new String("hn"); 
System.out.println(e.intern() == e);//true

new String("jo") + new String("hn")实际上会转为stringbuffer的append 然后tosring()出来,实际上是new 一个新的string出来。在这个过程中,并没有双引号括起john,也就是说并不会执行ldc然后把john的引用添加到stringtable中,所以intern的时候实际就是把新的string地址(即e的地址)添加到stringtable中并且返回回来。

String f = new String("ja") + new String("va"); 
System.out.println(f.intern() == f);//false

或许很多朋友感觉很奇怪,这跟上面的例子基本一模一样,但是却是false呢?这是因为java在启动的时候,会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添加到字符串常量池中的”java“的引用,所以肯定跟f的原地址不一样。

jdk6中的理解

Jdk6中字符串常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和 字符串池的大小固定的区域。

执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。

Jdk6中使用intern()方法的主要问题就在于字符串常量池被保存在PermGen中:

  • 首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同, 大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;
  • 其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不 等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能 损失。

JDK7+的理解

Jdk7将常量池从PermGen区移到了Java堆区。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。

字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。

执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回。

除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。

intern案例分析

public static void main(String[] args) { 
  	String s = new String("1"); 
  	s.intern(); 
  	String s2 = "1"; 
  	System.out.println(s == s2); 
  
  
  	String s3 = new String("1") + new String("1"); 
  	s3.intern(); 
  	String s4 = "11"; 
  	System.out.println(s3 == s4); 
}

打印结果是

jdk6 下 false false

jdk7 下 false true

然后将 s3.intern(); 语句下调一行,放到 String s4 = "11"; 后面。将

s.intern(); 放到 String s2 = "1"; 后面。是什么结果呢?

public static void main(String[] args) { 
  	String s = new String("1"); 
  	String s2 = "1";
	  s.intern(); 
  	System.out.println(s == s2); 
  
  
  	String s3 = new String("1") + new String("1"); 
  	String s4 = "11"; 
	  s3.intern(); 
  	System.out.println(s3 == s4); 
}

打印结果为:

jdk6 下 false false

jdk7 下 false false

jdk6中的解释

image.png

如上图所示。在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVAHeap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String.intern方法也是没有任何关系的。

jdk7中的解释

在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用intern 是会直接产生 java.lang.OutOfMemoryError:PermGen space 错误的。

在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动,Perm区域太小是一个主要原因,而且jdk8已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在 JAVA 的发展了。正式因为字符串常量池移动到JAVA Heap区域后,再来解释为什么会有上述的打印结果。
image.png

  • 在第一段代码中,先看 s3和s4字符串。 String s3 = new String("1") + new String("1"); ,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap中的 s3引用指向的对象。中间还有2个匿名的 new String("1") 我们不去讨论它们。此时s3引用对象内容是”11″,但此时常量池中是没有 “11”对象的。
  • 接下来 s3.intern(); 这一句代码,是将 s3中的"11"字符串放入String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个"11"的对象,关键点是 jdk7 中常量池不在Perm区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。 也就是说引用地址是相同的。
  • 最后 String s4 = "11"; 这句代码中”11″是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3引用对象的一个引用。所以s4引用就指向和s3一样了。因此最后的比较 s3 == s4 是 true。
  • 再看s和 s2 对象。 String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。 s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
  • 接下来 String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。
    image.png
  • 来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是 s3.intern();的顺序是放在 String s4 = "11"; 后了。这样,首先执行 String s4 = "11"; 声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行 s3.intern(); 时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
  • 第二段代码中的 s 和 s2 代码中, s.intern(); ,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码 String s = new String("1"); 的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
小结

从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:

  • 将String常量池从Perm区移动到了Java Heap区
  • String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。

intern方法的好处

如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。

String s3 = new String("1") + new String("1");

那么,有了这个特性了,intern就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。

这时候,对于那种可能经常使用的字符串,使用intern进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。

static final int MAX = 1000 * 10000;
    static final String[] arr = new String[MAX];

    public static void main(String[] args) throws Exception {
        Integer[] DB_DATA = new Integer[10];
        Random random = new Random(10 * 10000);
        for (int i = 0; i < DB_DATA.length; i++) {
            DB_DATA[i] = random.nextInt();
        }
        long t = System.currentTimeMillis();
        for (int i = 0; i < MAX; i++) {
            arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
        }
        System.out.println((System.currentTimeMillis() - t) + "ms");
        System.gc();
    }

以上程序会有很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过intern显示的将其加入常量池,这样可以减少很多字符串的重复创建。

Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用;

String.intern() 方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。


标题:jvm运行时数据区
作者:码农路上
地址:http://wujingjian.club/articles/2021/04/23/1619177542754.html