Java 8 中的常量池、字符串池、包装类对象池

Updated on with 0 views and 0 comments

引言

一直对常量池、字符串池、包装类对象池的认识不是那么深刻,下面巩固一下。

常量池分为静态常量池动态常量池
静态常量池在Class文件中(占用Class文件空间最大的数据项目之一),运行时常量池在方法区中,JDK1.8方法区已经被元数据区替代。
字符串常量池在1.7时被移动到了堆中。

String str = new String("Hello world") 创建了 2 个对象,一个驻留在字符串池,一个分配在 Java 堆,str 指向堆上的实例。
String.intern() 能在运行时向字符串池添加常量。
部分包装类实现了池化技术,-128~127 以内的对象可以重用。

字面量是用于表示源代码中一个固定值的表示方法 例如 十进制的1,字符串“1”,十六进制的0x0A等。在Java 程序员的圈子里,常量不单单指 final 变量,任何具有不变性的东西我们将它称为常量也不会带来什么歧义。

什么是常量池(静态常量池/Class常量池)

在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。而这些在JVM解释执行程序的时候是非常重要的。那么编译器将源程序编译成class文件后,会用一部分字节分类存储这些不变的代码,而这些字节我们就称为常量池(静态常量池/class文件常量池)。

静态/运行时常量池有什么关系
  1. 静态常量池是在编译时每个Class都有的,里面主要存放编译器生成的各种字面量和符号引用。
  2. 运行时常量池是在类加载完成后,将每个Class常量池的符号引用转到运行时常量池中,也就能表示每个Class都有一个运行时常量池,在度过解析环节后将符号引用转为直接引用。相较于Class文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法。

举个例子

 public static void main(String[] args) {
        String str1 = "abc";
        String str2 = new String("def");
        String str3 = "abc";
        String str4 = str2.intern();
        String str5 = "def";
        System.out.println(str1 == str3);//true
        System.out.println(str2 == str4);//false
        System.out.println(str4 == str5);//true
    }

首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值,最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中。(然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。[不解,待验证])

包装类对象池

包装类的对象池(也有称常量池)和JVM的静态/运行时常量池没有任何关系。静态/运行时常量池有点类似于符号表的概念,与对象池相差甚远。

包装类的对象池是池化技术的应用,并非是虚拟机层面的东西,而是 Java 在类封装里实现的。打开 Integer 的源代码,找到 cache

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

IntegerCache 是 Integer 在内部维护的一个静态内部类,用于对象缓存。通过源码我们知道,Integer 对象池在底层实际上就是一个变量名为 cache 的数组,里面包含了 -128 ~ 127 的 Integer 对象实例。

使用对象池的方法就是通过 Integer.valueOf() 返回 cache 中的对象,像 Integer i = 10 这种自动装箱实际上也是调用 Integer.valueOf() 完成的。
如果使用的是 new 构造器,则会跳过 valueOf(),所以不会使用对象池中的实例。

字符串常量池

在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 区(Permanent Generation,永久代)。Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,容量是固定的,默认在 32 M 到 96 M 间,我们可以通过 -XX:MaxPermSize = N 来配置永久代的大小,但是在运行过程中它仍然还是固定大小的。
在 JDK 1.7 的版本中,字符串池移到Java Heap。在 JDK 1.8 中永久代的说法被废弃,元空间成为方法区的替代品。

字符串池的实现——StringTable

由于字符串池是虚拟机层面的技术,所以在 String 的类定义中并没有类似 IntegerCache 这样的对象池,String 类中提及缓存/池的概念只有intern() 这个方法。

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

可以看到,intern() 是一个native 的方法,那么说明它本身并不是由 Java 语言实现的,而是通过 jni (Java Native Interface)调用了其他语言(如C/C++)实现的一些外部方法。

大体实现:Java 调用 c++ 实现的 StringTable 的 intern() 方法。StringTable 的 intern() 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容,默认大小是1009。

字符串池(String pool)实际上是一个 HashTable。Java 中 HashMap 和 HashTable 的原理大同小异,将字符串池看作哈希表更便于我们套用学习数据结构时的一些知识。比如解决数据冲突时,HashMap 和 HashTable 使用的是开散列(或者说拉链法)。
字符串常量池是一个固定容量的 hashmap,每一个 bucket 包含一系列相同 hash 码的字符串。
待续~


标题:Java 8 中的常量池、字符串池、包装类对象池
作者:liqitian3344
地址:https://liqitian.com/articles/2020/07/09/1594294616590.html