Java类加载阶段的一些细节
注意,本文所说的加载,是类加载这个过程的第一个阶段,而不是指的类加载的全部过程
加载(Loading)
从不同的来源将类文件(.class)转化为二进制流加载到内存中,来源可以是本地磁盘、jar包中的类、甚至可以从网络上动态下载类。然后会通过这个二进制流将数据转化成一个代表类的Class对象,并且在元空间中存储该类的信息
这里有一个容易混淆的概念,方法区,永久代以及元空间的关系。方法区只是一个规范,而永久代和元空间是对这套规范的实现,永久代是JDK8之前的实现方式,而元空间是JDK8及之后的实现方式。永久代和元空间的区别在于,永久代在JVM的堆内存中,而元空间在本地内存中,即操作系统中可用内存大小,其大小不受
-Xms
和-Xmx
的限制,减少了OOM发生的频率。
静态常量池
编译期确定,存储在.class文件中,可以通过javap -v -c XXX.class
查看
动态常量池
又称运行时常量池,是JVM在加载类时,将静态常量池加载到元空间后的产物,这意味着它是位于元空间的。动态常量池中的数据与静态常量池中的数据基本相同,但是有着更加灵活的特性,包括可实时动态向其中添加新的对象。
动态常量池还与动态解析相关,其中存放的符号引用会在解析成功后替换为直接引用,若解析失败,则会被JVM标记,防止下一次再次尝试解析
字符串常量池
字符串常量池在不同的JDK版本中有细微差异,如下图:
上图中的String Object为实际的对象,s1-s3是引用
我们来看以下代码:
1 | public class Main { |
上述代码来自:https://generalthink.github.io/2020/08/26/analysis-string-intern/
这段代码的输出如下:
- JDK6 : false false
- JDK8 : true false
- JDK12: true true
WHY?
在开始分析前,一定要牢记一下几点,否则会很容易迷失:
- 无论哪个JDK版本,字符串常量池中始终存放的是字符串对象的引用,而不是字符串对象本身。
- JVM在执行时,每遇到一个字符串字面量,都会在某个内存区域内(JDK7之前是在永久代中,JDK7及其之后是在堆中),为每个字面量都创建一个字符串对象,并且将这个对象的引用加入字符串常量池
我们用x
表示引用,*x
表示该引用指向的对象本身,以免引起混淆
intern
方法的作用:对于调用sa.intern()
,若字符串常量池中存在一个字符串对象的引用sb
,满足sa.equals(sb)
,那么直接返回sb
,否则将sa
加入字符串常量池中(此处的“加入”行为随着JDK版本不同而不同,后面会详细解释),并返回sa
-
JDK6:字符串常量池位于永久代中,与堆处于不同的内存区域,
intern
方法此时的行为是:对于调用sa.intern()
,若在字符串常量池中存在一个sb
,满足sa.equals(sb)
,那么直接返回sb
,否则将*sa
复制到永久代(注意不是复制到永久代中的字符串常量池,字符串常量池只存储引用,并不存储对象本身),记这个副本为*sc
,然后返回sc
。现在再来看上述程序的行为,虽然*s1被创建出来后,其中的字符串的值为
”think123”
,但是由于”think123”
并不是编译时期就能确定的字面量,而是运行时通过StringBuilder
拼出来的,所以在永久代中并没有值为”think123”
的字符串对象(当然字符串常量池中也没有值为”think123”
的字符串对象的引用),所以在调用s1.intern()
时,会把*s1
拷贝到永久代中,记这个副本为*s1’
,然后把s1’
添加到字符串常量池中,并返回s1’
,那么自然s1’
并不等于s1
,所以输出false
。后面的s2
也是同样的道理 -
JDK7:JDK7中的字符串常量池不再位于永久代中,而是位于堆中,也就意味着intern的行为将发生改变,此时intern的行为是:对于调用
sa.intern()
,若在字符串常量池中存在一个sb
,满足sa.equals(sb)
,那么直接返回sb
,否则将sa
加入字符串常量池中,并返回sa
。可以看到相比JDK6,少了一个复制的步骤,这是因为此时字符串对象和字符串常量池都在堆中,没有必要进行复制,减少了内存占用,加快了执行时间。再来分析一下上述程序,首先创建了一个值为
”think123”
的字符串对象*s1
,此时s1
并没有被加入字符串常量池中。接着调用s1.intern()
,发现s1
不在字符串常量池中,那么会将s1
加入字符串常量池,然后返回s1
,s1 == s1
成立,故输出true
。那为什么对于s2
输出的是false
呢?不是应该与s1
的情况一样吗?这其实是因为在Java的sun.misc.Version
中,存在这个”java”
字符串字面量!这个类是Java内部的,会在很早就加载进来,那么自然就会创建它的字符串对象并将其引用加入字符串常量池了,所以与我们自己创建的”java”
字符串对象不是同一个,自然就返回false
-
JDK12:在JDK12中,
”java”
这个字面量被从sun.misc.Version
中移除了,所以对于s2
的情况也会输出true