关于jvm:常量池常量静态变量

29次阅读

共计 9791 个字符,预计需要花费 25 分钟才能阅读完成。

1. 类加载过程

虚拟机把形容类的 class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的数据类型,这就是虚拟机的类加载机制。

当然在 class 文件的生成,则是由编译阶段实现。因而整个过程能够依照以下的流程:

编译 -> 加载 -> 验证(链接) -> 筹备(链接) -> 解析(链接) -> 初始化 -> 执行

上面着重讲一下 类加载的过程:

  1. 加载

加载,是指 Java 虚拟机查找字符流(查找.class 文件),并且依据字符流创立 Java,lang.Class 对象的过程,将类的.class 文件的二进制数据读入内存,放在运行区域的办法区内,而后在堆中创立 java.lang.Class 对象,用来封装类在办法区的数据结构。

  1. 验证

验证阶段作用是保障 Class 文件的字节流蕴含的信息合乎 JVM 标准,不会给 JVM 造成危害。如果验证失败就会抛出一个 java.lang.VerifyError 异样或其子类异样。

  1. 筹备

筹备阶段是正式为类变量设置分配内存,并设置初始值的阶段这些内存都在办法区中调配:
对于该阶段有以下几点须要留神:

  1. 这时候进行内存调配的只包含类变量(Class Variable, 即动态变量,被 static 关键字润饰的变量,只与类无关,因而被称为类变量),实例对象会在对象实例化时随着对象一块调配到 Java 堆中。
  2. 从概念上讲,类变量所应用的内存都应该在办法区中进行调配。不过有一点留神的是,在 JDK 7 之前,Hotspot 应用 永恒代来实现办法区时,实现是完全符合这种逻辑概念的。而在 JDK 7 及之后,把本来放在永恒代中的字符串常量池和动态变量等移到堆中。这个时候类变量会一并随着 Class 对象一并放在 Java 堆中。
  3. 这里所设置的初始值,通常状况下是数据类型默认的“零值”, 如 (0,0L,null,false),比方咱们定义了 public static int value = 11, 那么 value 变量在筹备阶段赋的值是 0, 而不是 11,(初始化阶段才会赋值),非凡状况:比方给 value 变量加上了 final 关键字 public static final int value=11,那么筹备阶段 value 的值就被赋值为 11。
  1. 解析

解析阶段是虚拟机将常量值中的符号援用替换为间接援用的过程,解析动作次要针对类和接口,字段,类办法,接口办法,办法类型,办法句柄,办法的限定符。

符号援用就是一组符号来形容指标,能够是任何字面量。间接援用就是间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄。在程序理论运行时,只有符号援用是不够的,举个例子:在程序执行办法时,零碎须要明确晓得这个办法所在的地位。Java 虚拟机为每个类都筹备了一张办法表来寄存类中所有的办法。当须要调用一个类的办法的时候,只有晓得这个办法在办法表中的偏移量就能够间接调用该办法了。通过解析操作符号援用就能够间接转变为指标办法在类中办法表的地位,从而使得办法能够被调用。

  1. 初始化

初始化阶段是执行初始化办法 ()办法的过程,是类加载的最初一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

阐明:< clinit> () 办法是编译之后主动生成的。

对于 < clinit> () 办法的调用,虚构机会本人确保其在多线程环境中的安全性。因为 < clinit> () 办法是带锁线程平安,所以在多线程环境下进行类初始化的话可能会引起多个过程阻塞,并且这种阻塞很难被发现。

对于初始化阶段,虚拟机严格标准了有且只有 5 种状况下,必须对类进行初始化(只有被动去应用类才会初始化类):

  1. 当遇到 new、getstatic、putstatic 或 invokestatic 这 4 条间接码指令时,比方 new 一个类,读取一个动态字段(未被 final 润饰)、或调用一个类的静态方法时。
    1.1. 当 jvm 执行 new 指令时会初始化类。即当程序创立一个类的实例对象。
    1.2. 当 jvm 执行 getstatic 指令时会初始化类。即程序拜访类的动态变量(不是动态常量,常量会被加载到运行时常量池)。
    1.3. 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的动态变量赋值。
    1.4. 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 应用 java.lang.reflect 包的办法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,须要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户须要定义一个要执行的主类 (蕴含 main 办法的那个类),虚构机会先初始化这个类。
  5. MethodHandle 和 VarHandle 能够看作是轻量级的反射调用机制,而要想应用这 2 个调用,就必须先应用 findStaticVarHandle 来初始化要调用的类。
  6. 当一个接口中定义了 JDK8 新退出的默认办法(被 default 关键字润饰的接口办法)时,如果有这个接口的实现类产生了初始化,那该接口要在其之前被初始化。

2. 常量池

2.1. 动态 /class 文件常量池

class 文件是一组以字节为单位的二进制数据流,在 java 代码的编译期间,咱们编写的 java 文件就被编译为.class 文件格式的二进制数据寄存在磁盘中,其中就包含动态常量池。class 文件中存在常量池(非运行时常量池),其在编译阶段就曾经确定。

先简略写一个类举例:

class JavaBean{
    private int value = 1;
    public String s = "abc";
    public final static int f = 0x101;

    public void setValue(int v){
        final int temp = 3;
        this.value = temp + v;
    }

    public int getValue(){return value;}
}

通过 javac 命令编译之后,用 javap -v 命令查看编译后的文件:

class JavaBasicKnowledge.JavaBean
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#29         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#30         // JavaBasicKnowledge/JavaBean.value:I
   #3 = String             #31            // abc
   #4 = Fieldref           #5.#32         // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String;
   #5 = Class              #33            // JavaBasicKnowledge/JavaBean
   #6 = Class              #34            // java/lang/Object
   #7 = Utf8               value
   #8 = Utf8               I
   #9 = Utf8               s
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               f
  #12 = Utf8               ConstantValue
  #13 = Integer            257
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               LJavaBasicKnowledge/JavaBean;
  #21 = Utf8               setValue
  #22 = Utf8               (I)V
  #23 = Utf8               v
  #24 = Utf8               temp
  #25 = Utf8               getValue
  #26 = Utf8               ()I
  #27 = Utf8               SourceFile
  #28 = Utf8               StringConstantPool.java
  #29 = NameAndType        #14:#15        // "<init>":()V
  #30 = NameAndType        #7:#8          // value:I
  #31 = Utf8               abc
  #32 = NameAndType        #9:#10         // s:Ljava/lang/String;
  #33 = Utf8               JavaBasicKnowledge/JavaBean
  #34 = Utf8               java/lang/Object

动态常量池次要寄存两大常量:字面量 符号援用

1. 字面量

字面量靠近 java 语言层面的常量概念,次要包含:

  1. 文本字符串,也就是咱们常常申明的:public String s = “abc”; 中的 ”abc”。

     #9 = Utf8               s
     #3 = String             #31            // abc
     #31 = Utf8              abc
  2. 用 final 润饰的成员变量,包含动态变量、实例变量和局部变量。

     #11 = Utf8               f
     #12 = Utf8               ConstantValue
     #13 = Integer            257

    这里须要阐明的一点,下面说的存在于常量池的字面量,指的是数据的值,也就是 abc0x101(257), 通过上面对常量池的察看可知这两个字面量是的确存在于常量池的。

而对于 根本类型数据(甚至是办法中的局部变量),也就是下面的 private int value = 1; 常量池中只保留了他的的字段描述符 I 和字段的名称 value,他们的字面量不会存在于常量池

2. 符号援用

符号援用次要设波及编译原理方面的概念,包含上面三类常量:

  1. 类和接口的全限定名,也就是 java/lang/String; 这样,将类名中原来的 ”.” 替换为 ”/” 失去的,次要用于在运行时解析失去类的间接援用,像下面

     #5 = Class              #33            // JavaBasicKnowledge/JavaBean
     #33 = Utf8               JavaBasicKnowledge/JavaBean
  2. 字段的名称和描述符,字段也就是类或者接口中申明的变量,包含类级别变量和实例级的变量。

     #4 = Fieldref           #5.#32         // JavaBasicKnowledge/JavaBean.value:I
     #5 = Class              #33            // JavaBasicKnowledge/JavaBean
     #32 = NameAndType       #7:#8          // value:I
    
     #7 = Utf8               value
     #8 = Utf8               I
    
     // 这两个是局部变量,值保留字段名称
     #23 = Utf8               v
     #24 = Utf8               temp

    能够看到,对于办法中的局部变量名,class 文件的常量池仅仅保留字段名。

  3. 办法中的名称和描述符,也即参数类型 + 返回值。

     #21 = Utf8               setValue
     #22 = Utf8               (I)V
    
     #25 = Utf8               getValue
     #26 = Utf8               ()I

2.2. 运行时常量池

运行时常量池是办法区的一部分,所以也是全局奉献的,咱们晓得,jvm 在执行某个类的时候,必须通过 加载、链接(验证、筹备、解析)、初始化 ,在第一步 加载 的时候须要实现:

  • 通过一个类的全限定名来获取此类的二进制字节流。
  • 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构。
  • 在内存中生成一个类对象,代表加载的这个类,这个对象是 java.lang.Class,它作为办法区这个类的各种数据拜访的入口。

类对象 一般对象 是不同的,类对象是在类加载的时候实现的,是 jvm 创立的并且是单例的,作为这个类和外界交互的入口,而一般的对象个别是在调用 new 之后创立。

1. 动态常量池 -> 运行时常量池

下面的第二条:将 class 字节流代表的动态存储构造转化为办法区的运行时数据结构 ,其中就蕴含了 动态常量池进入运行时常量池的过程。

2. 不同类共用一个运行时常量池,雷同字符串优化

这里须要强调一下:不同的类共用一个运行时常量池,同时在进入运行时常量池的过程中,多个 class 文件中常量池雷同的字符串,多个 class 文件中常量池中雷同的字符串只会存在一份在运行时常量池,这也是一种优化。

运行时常量池的作用是存储 java 动态常量池中的符号信息,运行时常量池中保留着一些 class 文件中形容的符号援用,同时在类的解析阶段还会将这些符号援用翻译出间接援用(间接指向实例对象的指针,内存地址),翻译进去的间接援用也是存储在运行时常量池中。

3. 运行时常量池比动态常量池多了动态性

运行时常量池绝对于动态常量池一大特色就是具备动态性,java 标准并不要求常量只能在运行时才产生,也就是说 运行时常量池的内容并不全副来自动态常量池,在运行时能够通过代码生成常量并将其放入运行时常量池中,这种个性被用的最多的就是 String.intern()。

2.3. 字符串常量池

在工作中,String 类是咱们应用频率十分高的一种对象类型。JVM 为了晋升性能和缩小内存开销,防止字符串的反复创立,其保护了一块非凡的内存空间,这就是咱们明天要探讨的外围,即字符串常量池(String Pool)。字符串常量池由 String 类公有的保护。

字符串是常量,字符串池中的每个字符串对象只有惟一的一份,能够被多个援用所指向,防止了反复创立内容雷同的字符串。

1. Java 中创立字符串对象的两种形式

个别有如下两种:

String s0 = "hellow";
String s1 = new String("hellow");

它们的区别在于:

  1. 通过字面值赋值创立的字符串对象寄存在字符串池中。第一种形式申明的字面量 hellow 是在编译期就曾经确定的,它会间接进入动态常量池中;当运行期间在全局字符串常量池中会保留它的一个援用. 实际上最终还是要在堆上创立一个”hellow”对象,这个前面会讲。
  2. 通过关键字 new 进去的字符串对象寄存在堆中。第二种形式形式应用了 new String(),也就是调用了 String 类的构造函数,咱们晓得 new 指令是创立一个类的实例对象并实现加载初始化的,因而这个字符串对象是在运行期能力确定的,创立的字符串对象是在堆内存上。

因而此时调用 System.out.println(s0 == s1); 返回的必定是 flase, 因而 == 符号比拟的是两边元素的地址,s1 和 s0 都存在于堆上,然而地址必定不雷同。咱们来看几个常见的题目:

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false

class 文件里常量池里大部分数据会被加载到“运行时常量池”,包含 String 的字面量;但同时“Hello”字符串的一个援用会被存到同样在“非堆”区域的“字符串常量池”中,而 ”Hello” 本体还是和所有对象一样,创立在 Java 堆中。

当主线程开始创立 s1 时,虚构机会先去字符串池中找是否有 equals(“Hello”)的 String,如果相等就把在字符串池中“Hello”的援用复制给 s1;如果找不到相等的字符串,就会在堆中新建一个对象,同时把援用驻留在字符串池,再把援用赋给 str。

当用字面量赋值的办法创立字符串时,无论创立多少次,只有字符串的值雷同,它们所指向的都是堆中的同一个对象。

2. 优缺点

字符串池的长处就是防止了雷同内容的字符串的创立,节俭了内存,省去了创立雷同字符串的工夫,同时晋升了性能;

另一方面,字符串池的毛病就是减少了局部工夫老本,即 JVM 在常量池中遍历对象所须要的工夫,不过其工夫老本相比而言比拟低。

如果对空间要求高于工夫要求,且存在大量反复字符串时,能够思考应用常量池存储。
如果对查问速度要求很高,且存储字符串数量很大,反复率很低的状况下,不倡议存储在常量池中。

3. String.intern()

一个初始为空的字符串池,它由类 String 单独保护。当调用 intern 办法时,如果池曾经蕴含一个等于此 String 对象的字符串(用 equals(oject)办法确定),则返回池中的字符串。否则,将此 String 对象增加到池中,并返回此 String 对象的援用。对于任意两个字符串 s 和 t,当且仅当 s.equals(t)为 true 时,s.instan() == t.instan 才为 true。所有字面值字符串和字符串赋值常量表达式都应用 intern 办法进行操作。

2.4. java 根本数据类型包装类及常量池

java 中根本类型的包装类的大部分都实现了常量池技术,这些类是 Byte,Short,Integer,Long,Character,Boolean, 另外两种浮点数类型的包装类则没有实现。另外下面这 5 种整型的包装类也只是在对应值小于等于 127 时才可应用对象池,也即对象不负责创立和治理大于 127 的这些类的对象。

public class StringConstantPool{public static void main(String[] args){
        // 5 种整形的包装类 Byte,Short,Integer,Long,Character 的对象,// 在值小于 127 时能够应用常量池
        Integer i1=127;
        Integer i2=127;
        System.out.println(i1==i2);// 输入 true

        // 值大于 127 时,不会从常量池中取对象
        Integer i3=128;
        Integer i4=128;
        System.out.println(i3==i4);// 输入 false
        //Boolean 类也实现了常量池技术

        Boolean bool1=true;
        Boolean bool2=true;
        System.out.println(bool1==bool2);// 输入 true

        // 浮点类型的包装类没有实现常量池技术
        Double d1=1.0;
        Double d2=1.0;
        System.out.println(d1==d2); // 输入 false

    }
}

在 JDK5.0 之前是不容许间接将根本数据类型的数据间接赋值给其对应地包装类的,如:Integer i = 5; 然而在 JDK5.0 中反对这种写法,因为编译器会主动将下面的代码转换成如下代码:Integer i=Integer.valueOf(5); 这就是 Java 的装箱.JDK5.0 也提供了主动拆箱:Integer i =5; int j = i;

2.5. 常量池和动态变量

上述提到了 动态 /class 文件常量池、运行时常量池、字符串常量池,在刚接触这些概率时,很容易对这三者混同。
但实际上,三者并非是同一维度上能够横向比照的概念,只不过在名词上都带有常量池。

1. 动态常量池和运行时常量池

二者不应该横向比照,而是有工夫上的程序,因为动态常量池在 jvm 加载后,才生成了运行时常量池,算是常量池的不同状态。

但运行时常量池的内容并不全副来自动态常量池,在运行时能够通过代码生成常量并将其放入运行时常量池中,这种个性被用的最多的就是 String.intern()。

在存储形式上,动态常量池存在.class 字节码文件中,即由编译器编译阶段生成。而运行时常量池是在加载阶段后,存储在内存中的。

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

字符串常量池算是运行时常量池的一部分,只不过因为字符串在 java 开发中利用频繁,就独自拎了进去。

但运行时常量池和动态常量池一样,都是每个类 class 各有一份。但字符串常量池又称“全局字符串常量池”,jvm 中只有一个。

字符串常量池的起源大抵有两种:

  • 动态常量池中字符串类型的字面量,当加载成运行时常量池时就继承了。
  • 动静生成的字符串常量 ,如果 String.intern() 的利用。

二者在存储形式上有争议,已大众化的了解如下:

  • 在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区 (也就是办法区) 中,此时常量池中存储的是对象。
  • 在 JDK7.0 版本,只提到字符串常量池被移到了堆中了,默认就是其余运行时常量池还在办法区,此时运行时常量池存储的字符串值就是援用了。(争议:文档只提到字符串常量池移到堆,其实并提到其余运行时常量池是否也移到堆)
  • 在 JDK8.0 中,永恒代(办法区)被元空间取代了。

3. 动态变量

3.1. 动态变量和字符串常量池

动态变量在 jvm 存储地位上,和字符串常量池始终很像。

  • jdk1.6 及以前:有永恒代,动态变量存储在永恒代上。
  • jdk1.7 : 有永恒代,但曾经逐渐在去“永恒代”,类型信息、字段、办法、常量保留在永恒代中,但字符串常量池、动态变量保留在堆中。
  • jdk1.8 : 无永恒代,类型信息、字段、办法、常量保留在本地内存的元空间中,但字符串常量池、动态变量保留在堆中。

jdk 1.7 中,将 字符串常量池 放到了堆空间中。因为永恒代的回收效率很低,在 full gc 的时候才会触发。而 full gc 是老年代空间有余、永恒代空间有余时才会触发,导致 StringTable 回收效率不高。而咱们开发中会有大量的字符串被创立,回收效率低,导致永恒代内存不足。而放到堆中,就能更及时的进行回收。所以,当字符串常量池和动态变量进入堆后,是能够像一般对象一样通过 minor gc、full gc。

3.2. static 和 final

1. ConstantValue 属性

ConstantValue 属性的作用是告诉虚拟机主动为动态变量赋值,只有被 static 润饰的变量才能够应用这项属性。非 static 类型的变量的赋值是在实例结构器办法中进行的;static 类型变量赋值分两种,在类结构器中赋值,或应用 ConstantValue 属性赋值。

在理论的程序中,只有同时被 final 和 static 润饰的字段才有 ConstantValue 属性,且限于根本类型和 String。编译时 Javac 将会为该常量生成 ConstantValue 属性,在类加载的筹备阶段虚拟机便会依据 ConstantValue 为常量设置相应的值,如果该变量没有被 final 润饰,或者并非根本类型及字符串,则抉择在类结构器中进行初始化。

2. 为什么 ConstantValue 的属性值只限于根本类型和 String

因为从常量池中只能援用到根本类型和 String 类型的字面量

3. final、static、static final 润饰的字段赋值的区别

  • static 润饰的字段在加载过程中筹备阶段被初始化,然而这个阶段只会赋值一个默认的值(0 或者 null 而并非定义变量设置的值)初始化阶段在类结构器中才会赋值为变量定义的值。(对于 static 的初始化请看“init”与”clinit”的区别)。
  • final 润饰的字段在运行时被初始化,能够间接赋值,也能够在实例结构器中赋值,赋值后不可批改。
    final 关键字对于变量的存储区域是没有任何影响的。jvm 标准中,类的动态变量存储在办法区,实例变量存储在堆区。也就是说 static 关键字才对变量的存储区域造成影响。
    final 关键字来润饰变量表明了该变量一旦赋值就无奈更改。同时编译器必须保障该变量在应用前被初始化赋值。
    例如你的 static final int c1 这个变量,是一个动态变量,动态变量的初始化能够在动态块中进行。而非 static 变量,能够初始化块中和构造方法中进行。
    如果你在这几个中央没有对 final 变量进行赋值,编译器便会报错。
  • static final 润饰的字段在 javac 编译时生成 comstantValue 属性,在类加载的筹备阶段间接把 constantValue 的值赋给该字段。能够了解为在编译期即把后果放入了常量池中。

在一个类中定义字段时,能够申明为成员变量(如 final),也能够申明为类变量(动态变量),动态变量在装载类时被初始化,而成员变量每次创立实例时都会被初始化一次。一个字段被申明为 static final,示意这个字段在初始化实现后就不可再扭转了,final,类的初始化实现后,在类的实例化进行赋值,每次实例化的值不肯定雷同。加上了 static 的 final,在类只装载一次的状况下,能够是真正意义的“常量”。例如:

private static final int random = new Random().nestInt();// 每次产生类装载时都会赋值一次,且赋的值都不一样 
private final int random = new Random().nestInt();// 每次生成这个类的实例时都会赋值一次,且赋的值都不一样

正文完
 0