探究Java常量本质及三种常量池(小结)

之前从他人的博文,还有一些书籍中了解到 常量是放在常量池 中,细节的内容无从得知,总觉得面前的东西是一个几乎完全的黑盒,总是觉得不舒服,于是就翻阅《深入理解Java虚拟机》,这本书中对常量的介绍更多地偏重于字节码文件的结构,还有在自动内存管理机制中也介绍了运行时常量池, 查阅资料后脑海中有了一定的认识。

Java中的常量池分为三种形态:静态常量池,字符串常量池以及运行时常量池。

静态常量池

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

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

String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

那这样来看,通过静态常量池,即*.class文件中的常量池 更能够探究常量的含义了

下面看一段代码

public class Main {

  public static void main(String[] args) {
    System.out.println(Father.str);
  }
}

class Father{
  public static String str = "Hello,world";
  static {
    System.out.println("Father static block");
  }
}

输出结果为

再看另一个:

package com.company;

public class Main {

  public static void main(String[] args) {
    System.out.println(Father.str);
  }
}

class Father{
  public static final String str = "Hello,world";
  static {
    System.out.println("Father static block");
  }
}

结果:

只有一个

是不是发现很吃惊啊

我们对第二个演示的代码块进行反编译一下

D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
 public com.company.Main();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 public static void main(java.lang.String[]);
  Code:
    0: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #4         // String Hello,world
    5: invokevirtual #5         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return
}

这里有一个Main()是构造方法 下面的是main方法

0: getstatic # 2 对应的是System.out
3: ldc #4 对应的值 直接是 Hello,world 了 确定的值 没有从Father类中取出

ldc表示将int,float或是String类型的常量值从常量池中推送至栈顶

竟然没有!!! 即使删除Father.class文件 这段代码照样可以运行 它和Father类 没有半毛钱的关系了

实际上,在编译阶段 常量就会被存入到调用这个常量的方法所在的类的常量池当中

从这个例子中 可以看出 这里的str 是一个常量 调用这个常量的方法是main方法 main方法所在的类是Main ,也就是说编译之后str被放在了该类的常量池中

本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化

类的初始化 涉及到类的加载机制 这里暂时写不说 这个留到之后必须要好好说说

字符串常量池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。

字符串常量池的位置的说法不太准确
在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
在JDK7.0版本,字符串常量池被移到了堆中了。

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

 回到运行常量池(runtime constant pool)

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。

而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。

静态常量池中存的是字面量和符号引用,也就是说它们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。

我们看一个例子

import java.util.UUID;

public class Test {
  public static void main(String[] args) {
    System.out.println(TestValue.str);
  }
}

class TestValue{
  public static final String str = UUID.randomUUID().toString();

  static {
    System.out.println("TestValue static code");
  }
}

结果:

从声明本身str都是常量,关键的是这个常量的值能否在编译时期确定下来,显然这里的例子在编译期的时候显然是确定不下来的。需要在运行期才能能够确定下来,这要求目标类要进行初始化

当常量的值并非编译期间可以确定的,那么其值不会被放到调用类的常量池中
这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。
(这个涉及到类的加载机制,后面会写这里做个标记)

反编译探究一下:

Compiled from "Test.java"
class com.leetcodePractise.tstudy.TestValue {
 public static final java.lang.String str;

 com.leetcodePractise.tstudy.TestValue();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 static {};
  Code:
    0: invokestatic #2         // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
    3: invokevirtual #3         // Method java/util/UUID.toString:()Ljava/lang/String;
    6: putstatic   #4         // Field str:Ljava/lang/String;
    9: getstatic   #5         // Field java/lang/System.out:Ljava/io/PrintStream;
   12: ldc      #6         // String TestValue static code
   14: invokevirtual #7         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
   17: return
}

很明显TestValue类会初始化出来

常量介绍完之后 这里记录一下反编译及助记符的笔记

package com.company;

public class Main {

  public static void main(String[] args) {
    System.out.println(Father.str);
    System.out.println(Father.s);
  }
}

class Father{
  public static final String str = "Hello,world";
  public static final short s = 6;
  static {
    System.out.println("Father static block");
  }
}

public class com.company.Main {
 public com.company.Main();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 public static void main(java.lang.String[]);
  Code:
    0: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #4         // String Hello,world
    5: invokevirtual #5         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   11: bipush    6
   13: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   16: return
}

bipush 表示将单字节(-128-127)的常量值推送至栈顶

再加入

package com.company;

public class Main {

  public static void main(String[] args) {
    System.out.println(Father.str);
    System.out.println(Father.s);
    System.out.println(Father.t);
  }
}

class Father{
  public static final String str = "Hello,world";
  public static final short s = 6;
  public static final int t = 128;
  static {
    System.out.println("Father static block");
  }
}

进行反编译

public class com.company.Main {
 public com.company.Main();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 public static void main(java.lang.String[]);
  Code:
    0: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #4         // String Hello,world
    5: invokevirtual #5         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   11: bipush    6
   13: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   16: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   19: sipush    128
   22: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   25: return
}

sipush表示将一个短整型常量值(-32768~32767)推送至栈顶

再进行更改

package com.company;

public class Main {

  public static void main(String[] args) {
    System.out.println(Father.str);

    System.out.println(Father.t);
  }
}

class Father{
  public static final String str = "Hello,world";

  public static final int t = 1;
  static {
    System.out.println("Father static block");
  }
}
public class com.company.Main {
 public com.company.Main();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 public static void main(java.lang.String[]);
  Code:
    0: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #4         // String Hello,world
    5: invokevirtual #5         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   11: bipush    6
   13: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   16: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   19: sipush    128
   22: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   25: return
}

D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
 public com.company.Main();
  Code:
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."<init>":()V
    4: return

 public static void main(java.lang.String[]);
  Code:
    0: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #4         // String Hello,world
    5: invokevirtual #5         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: getstatic   #2         // Field java/lang/System.out:Ljava/io/PrintStream;
   11: iconst_1
   12: invokevirtual #6         // Method java/io/PrintStream.println:(I)V
   15: return
}

这里变成了 iconst_1

iconst 1表示将int类型1推送至栈顶(iconst_m1-iconst_5)
当大于5的时候 就变为了bipush
m1对应的是-1

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • java String类常量池分析及"equals"和"==”区别详细介绍

    java "equals"和"=="异同 首先简单说一下"equal"和"==" ==操作对于基本数据类型比较的是两个变量的值是否相等, 对于引用型变量表示的是两个变量在堆中存储的地址是否相同, 即栈中的内容是否相同 equals操作表示的两个变量是否是对同一个对象的引用, 即堆中的内容是否相同. 综上,==比较的是2个对象的地址,而equals比较的是2个对象的内容. 再简单介绍一下String类 String类 又称作不可

  • java String源码和String常量池的全面解析

    1. String 介绍,常用方法源码分析 2. String 常量池分析 常用方法 equals trim replace concat split startsWith 和 endsWith substring toUpperCase() 和 toLowerCase() compareTo String 介绍 String类被final所修饰,也就是说String对象是不可变量,并发程序最喜欢不可变量了.String类实现了Serializable, Comparable, CharSequ

  • Java常量池知识点总结

    java常量池是一个经久不衰的话题,也是面试官的最爱,题目花样百出,这次好好总结一下. 理论 先拙劣的表达一下jvm虚拟内存分布: 程序计数器是jvm执行程序的流水线,存放一些跳转指令,这个太高深,不懂. 本地方法栈是jvm调用操作系统方法所使用的栈. 虚拟机栈是jvm执行java代码所使用的栈. 方法区存放了一些常量.静态变量.类信息等,可以理解成class文件在内存中的存放位置. 虚拟机堆是jvm执行java代码所使用的堆. Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池.

  • Java中的字符串常量池详细介绍

    Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池. 工作原理 当代码中出现字

  • Java 常量池的实例详解

    Java 常量池的实例详解 Java的常量池中包含了类.接口.方法.字符串等一系列常量值.常量池在编译期间就已经确定,并保存在*.class文件中 一.对于相同的常量值,常量池中只保存一份拷贝. 而且,当一个字符串由多个字符串常量链接而成时,多个字符串被组成一个字符串常量. 例如: package lxg; public class main { public static void main(String[] args) { String name = "lengxuegang";

  • 浅谈java常量池

    java常量池技术 java中常量池技术说的通俗点就是java级别的缓存技术,方便快捷的创建一个对象.当需要一个对象时,从池中去获取(如果池中没有,就创建一个并放入池中),当下次需要相同变量的时候,不用重新创建,从而节省空间. java八种基本类型的包装类和对象池 java中的基本类型的包装类.其中Byte.Boolean.Short.Character.Integer.Long实现了常量池技术,(除了Boolean,都只对小于128的值才支持) 比如,Integer对象 Integer i1

  • 深入探索Java常量池

    Java的常量池通常分为两种:静态常量池和运行时常量池 静态常量池:class文件中的常量池,class文件中的常量池包括了字符串(数字)字面值,类和方法的信息,占用了class文件的大部分空间. 运行时常量池:JVM在完成加载类之后将class文件中常量池载入到内存中,并保存在方法区中.平时我们所讲的常量池就是指方法区中的运行时常量池.其相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入

  • Java class文件格式之常量池_动力节点Java学院整理

    常量池中各数据项类型详解 常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用.在这11中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用. 这两种数据类型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType类型的数据项(CONSTANT_NameAndType_info)也会引用CONSTANT_Utf8类型的数据项(CONSTANT_Ut

  • Java 中的字符串常量池详解

    Java中的字符串常量池 Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池.

  • Java String 字符串常量池解析

    作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么,我们带着以下三个问题,去理解字符串常量池: 字符串常量池的设计意图是什么? 字符串常量池在哪里? 如何操作字符串常量池? 字符串常量池的设计思想 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能 JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化 为字符串开辟一个字符串

随机推荐