运行时数据区域、Class常量池、运行时常量池、字符串常量池

1、运行时数据区域

在这里插入图片描述

--
线程私有程序计数器、虚拟机栈、本地方法栈
线程共享堆、方法区、直接内存 (非运行时数据区的一部分)

1.1 程序计数器

程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

1.2 Java 虚拟机栈

Java 虚拟机栈是线程私有的,它的生命周期和线程相同。Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有(运行时栈帧结构):

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口信息(不管哪种返回方式都会导致栈帧被弹出)
    • return 语句
    • 抛出异常

1.3 本地方法栈

虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务,本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息

1.4 堆

  • Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。几乎所有的对象实例以及数组都在这里分配内存
  • Java 堆是垃圾收集器管理的主要区域,Java 堆还可以分为下面三部分:
    • 新生代内存(Young Generation)
    • 老生代(Old Generation)
    • 永生代(Permanent Generation)

1.5 方法区

  • 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    更加详细一点的说法是方法区里存放着类的版本字段方法接口常量池

  • 方法区里存储着class文件信息运行时常量池class文件信息包括类信息和class文件常量池(参考Class文件结构反编译解释)。

方法区和永久代的关系

  • 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

1.6 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现

2、Class常量池

参考:class文件常量池和运行时常量池
参考:Java中几种常量池的区分
在这里插入图片描述

  • .java文件被编译成 .class文件后,在类加载时,将.class文件内容提取到方法区的Class文件信息中,Class文件信息包含了Class文件常量池
    -Class文件常量池里存储着字面量和符号引用
    在这里插入图片描述
  • Class文件常量池存放的是 从.class文件中得到的字面量值,符号引用。
  • 运行时常量池就是把所有的Class常量池里的数据汇总到一起,将符号引用替换为直接引用。
    在这里插入图片描述

3、运行时常量池

3.1 运行时常量池的动态添加(1.7之前有,1.8及以后没有)

  • 相较于Class文件常量池运行时常量池更具动态性,在运行期间也可以将新的变量放入运行时常量池中,而不是一定要在编译期间放入常量,这里的常量包括:基本类型的包装类和String(使用String.intern())

3.1 基本类型的包装类

  • java中的基本类型的包装类(Byte、Short、Integer、Long、Character这5种)都实现了常量池技术,默认创建相应类型的缓存,但是,超出此范围仍然会去创建新的对象。 浮点数类型的包装类Float、Double并没有实现常量池技术

    8 种基本类型的包装类和常量池

public void fun07(){
		Integer a = 10;
		Integer b = 10;
		System.out.println(a == b); // true
		Integer c = 200;
		Integer d = 200;
		System.out.println(c == d); // false
		Long e = 200L;
		Long f = 200L;
		System.out.println(e == f); // false
		Long g = 20L;
		Long h = 20L;
		System.out.println(g == h); // true
		Double i = 20.0;
		Double j = 20.0;
		System.out.println(i == j); // false
}

需要注意的是:

  • 使用new,仍然会创建新对象. 比如 Integer i1 = new Integer(40)
  • Integer a = 40在编译的时候会直接将代码封装成Integer a =Integer.valueOf(40),从而使用常量池中的对象

3.1.2 String.intern()

​当调用 String 的 intern() 方法时,若字符串常量池中已经存在这个String对象,则返回字符串常量池中此对象的引用;否则,将此String串加入字符串常量池中,并返回该String对象的引用。简单来说,intern()返回常量池中的某字符串。对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.intren() == t.intern() 才为true。

3.2 class文件常量池、运行时常量池

  • class文件常量池存储的是当.class文件被java虚拟机加载进来后,存放在方法区的一些字面量符号引用

  • 运行时常量池是当.class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,将class文件常量池的一部分符号引用转为直接引用。比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写,所以在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

3.3 字符串常量池、运行时常量池

  • JDK1.7 之前运行时常量池逻辑包含字符串常量池,均存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代

    运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,而且运行时常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到字符串常量池中,这里字符串常量池是包含在运行时常量池里的,但在jdk1.8后,将字符串常量池放到了堆中。

  • JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代

  • JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

4、字符串常量池

为了避免多次创建字符串对象,在jvm中开辟一块空间储存不重复的字符串,这块空间就是字符串常量池

4.1 创建字符串的两种方式

  • 直接使用双引号""声明字符串时, 先去字符串常量池找有没有相同的字符串,如果有,则将常量池的引用返回给变量; 如果没有,会在字符串常量池中创建一个对象,然后返回这个对象的引用

    public static void main(String[] args){
    String name1 = “chenliang”;
    String name2 = “chenliang”;
    System.out.println(name1 == name2); // true
    } ​
    采用字面量的方式创建一个字符串时,JVM首先去字符串常量池中去查找是否存在"chenliang"这个对象,如果不存在,则在字符串常量池中创建"chenliang"对象,然后将该对象的引用地址返回给字符串常量name1,则此时name1会指向字符串常量池中"chenliang"这个对象,若存在,则不创建任何对象,直接将字符串常量池中"chenliang"对象的引用地址返回,赋值给字符串常量。

  • 使用new关键字创建,比如String a = new String(“hello”),这里可能创建两个对象。如果字符串常量池没有,则在这里创建一个对象。另一个是必须创建的,new 关键字必然会在堆中创建一个新对象,最终返回的是new 关键词创建对象的地址

    public static void main(String[] args){
    String name3 = new String(“chenliang”);
    String name4 = new String(“chenliang”);
    System.out.print(nam3 == name4); // false
    } ​
    采用new关键字去新建一个字符串对象时,JVM首先在字符串池中查找是否存在"chenliang"这个字符串对象,若有则不在字符串池中再去创建"chenliang"对象,而是直接在堆中创建"chenliang"字符串对象,然后将堆中该对象的引用地址返回给name3,若字符串池中没有该对象,则会在字符串池中创建一个该对象,再去堆中创建一个"chenliang"对象,并将堆中该对象的引用地址返回给name3。

  • 在jdk1.6及之前,字符串常量池是属于运行时常量池。在jdk1.7 ,字符串常量池从方法区中被单独拿到堆中
    在这里插入图片描述

4.2 String s1 = new String(“rakesh”);这句话创建了几个字符串对象?

将创建 1 或 2 个字符串

String st1 = "rakesh";
String st2 = new String("rakesh");
  • 当我们执行String st1 =“rakesh”时,JVM将在字符串常量池中创建一个对象,st1将引用它
  • 执行第二步的时候,JVM将检查字符串常量池中是否有任何可用的名称为“rakesh”的对象,现在是的,我们已经在字符串常量池中使用了“rakesh”,因此JVM不会在字符串常量池中创建任何对象。因为有new关键词,它将在堆中创建一个对象,st2将指向该对象。

当我们使用new运算符创建String对象时,JVM将首先在SCP(字符串常量池)中检查,该对象是否可用。如果SCP内部没有该对象,JVM将创建两个对象,一个在SCP内部,另一个在SCP外部。但是如果JVM在SCP中找到相同的对象,它只会在SCP外部创建一个对象。
在这里插入图片描述

4.3 字符串拼接

public class StringExample1 {
	public static void main(String[] args) { 
		String s1 = "india";				// ①
		
		String vs2 = "indiais";
		String s2 = s1 + "is";				// ②
		System.out.println(vs2==s2);		// false
		System.out.println(vs2.equals(s2));	// true
		
		String k = "indiagreat";
		String ks = s1.concat("great");		// ③
		System.out.println(ks==k);			// false
		System.out.println(ks.equals(k));	// true
		
		String m = "indiaisindia";
		String ms = s2.concat(s1);			// ④
		System.out.println(ms==m);			// false
		System.out.println(ms.equals(m));	// true
		
		String vs1 = "indiacountry";
		s1 += "country";					// ⑤
		System.out.println(vs1==s1);		// false
		System.out.println(vs1.equals(s1));	// true
	}
}
  • ① "india"会直接在scp(字符串常量池)中创建
  • ② 对于 s1 + “is”,因为编译器无法知道s1是什么,而字符串String是一个不可修改的类,所以这里的 " + " 会被编译成:String s2 = (new StringBuilder(“india”)).append(“is”).toString(),会在堆中new一个 "indiais"字符串。(因为"is"是双引号声明的,所以同样在scp中创建一个"is"对象)

StringBuilder的toString方法,会在堆中重新new一个字符串对象
在这里插入图片描述

  • ③ String ks = s1.concat(“great”)会在堆中new一个"indiagreat"字符串。(因为"great"是双引号声明的,所以同样在scp中创建一个"great"对象)
  • ④ String ms = s2.concat(s1)同理,会在堆中new一个"indiaisindia"字符串
  • ⑤ s1 += “country” 也就是 s1 = s1 + “country”,和②一样,在堆中new一个"indiacountry"字符串

4.4 总结

  • 对于双引号""直接声明的字符串,比如String a = "aa",会直接在scp中创建对象

  • 对于两个双引号""声明的字符串使用 " + " 拼接,jvm在编译时就去掉其中的加号,直接将其编译成一个相连的结果存入字符串常量池,而不是等到运行时再相加,但是两个声明的字符串不会放进字符串常量池(String s = “abc”+ “def”, 是将“abcdef"放入字符串常量 而不把 “abc”与"def"放进常量池)

    String s = “a” + “b”;
    System.out.println(s == “ab”); // true
    即 String s = “a” + “b”;只创建了一个对象,就是ab,存于常量池中。

  • 对于其中有一个不是双引号""声明的字符串,变量相加时,编译器无法得知结果,会用StringBuilder创建新对象,不会将这个临时结果放到scp中,而是最后在堆中new一个新的对象。因此,尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer

    String s1 = “a”;
    String s2 = s1 + “b”; // 堆上新建字符串"ab"
    String s3 = “a” + “b”; // 将"ab"存入字符串常量池
    String s4 = s1.concat(“b”); // 堆上新建字符串"ab"
    System.out.println(s2 == “ab”); // false
    System.out.println(s3 == “ab”); // true
    System.out.println(s4 == “ab”); // false
    System.out.println(s4 == s2); // false

参考:https://blog.csdn.net/zzzgd_666/article/details/87999870
参考:https://my.oschina.net/u/867830/blog/1609952
参考:https://www.jianshu.com/p/8966c51e9728
参考:https://segmentfault.com/a/1190000022743814


版权声明:本文为qq_35091353原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。