这几天研究了一下JVM底层原理。其中的内存分配前前后后看了三天,感觉还是没太看透。
先研究到这,做个阶段性的笔记,感兴趣的小伙伴们欢迎大家评论区共同讨论!
查阅了各种博客,长篇大论,例证太多,不清晰。本文主要目的精简浓缩一下,感兴趣的去文中参考的原文链接中自行查看吧~
一、先说说三种常量池——之间执行时的整体执行流程配合
jvm运行加载class文件到内存中(即class常量池 -> 运行时常量池,期间需要到字符串常量池中获取“符号引用 -> 直接引用”的映射关系,然后把符号引用替换为直接引用)
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
- ① class常量池:class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。
- ② 运行时常量池:当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
- ③ 字符串常量池:经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们所说的字符串常量池StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
ps:所以简单来说,运行时常量池就是用来存放class常量池中的内容的。
我们将三者进行一个比较,如图:
二、再说说三种常量池——细节
# class常量池(class constant pool)
一个类一个class文件,一个class文件中包含一个class常量池,其实就是我们编译后的“.class文件”中【constant pool】属性中的信息。
class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。
# 运行时常量池(runtime constant pool)
一个类一个运行时常量池。运行时常量池是每个类/接口的字节码文件中constant_pool table
的运行时实现
当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
# 字符串常量池(string constant pool)
英文名即String Constant Pool,又叫做String Pool,或String Literals Pool,或String Intern Pool,还有叫String Table。
字符串常量池,用于存放字符串常量的运行时的对象的引用。
其中所指的字符串常量,可以是编译期在源码中显式的字符串字面量(string literals)在被加载到内存中使用时创建的String字符串对象,也可以是之后在程序运行时创建的String字符串对象。
加载class文件到内存中,经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们所说的字符串常量池StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
## PS(重点):
字符串常量池的底层实现类是C++的stringTable.cpp类。而stringTable的底层实现为C++语言中的Hashtable(与Java中的HashTable不同,类似于java的HashSet)。
stringTable没在java内存结构里,它是c++写的,放在native memory的。stringTable里不放对象,它里面放的是对象的引用,而堆里放的才是真正的字符串对象。
可以采用《hashmap中的value存储的是引用》同样的方式来验证,链接:java的hashmap中value存放的是引用_HD243608836的博客-CSDN博客
底层实现源码实现:
- jdk8 实现类symbolTable.cpp,路径hotspot/src/share/vm/classfile/symbolTable.cpp,openjdk链接:jdk/symbolTable.cpp at jdk8-b120 · openjdk/jdk · GitHub
- jdk11 实现类stringTable.cpp,路径:/src/hotspot/share/classfile/stringTable.cpp,openjdk链接:jdk/stringTable.cpp at jdk-11+28 · openjdk/jdk · GitHub
重点——如果还有疑问,请查看这两篇文章,保证把字符串常量池了解的非常透彻:
知乎文章:从字符串到常量池,一文看懂String类! - 知乎 (zhihu.com)
当然还可以再更深层次了解一下:jvm从HotSpot VM源码看字符串常量池(StringTable)和intern()方法_HD243608836的博客-CSDN博客
## 存放的位置
- Java 6 及以前,字符串常量池存放在永久代(永久代是方法区概念在jdk6中的实现)。(jdk6时,堆与方法区隔离,互不干扰。甚至方法区有个官方别名,叫Non-heap)
- Java 7 开始 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到 Java 堆内(补充:同时还有static静态常量等等也一并被从方法区中转移出去放到堆中了)。
(这样做的好处是:所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时,仅需要调整堆大小就可以了)
## 关于《JVM 字符串常量池中存储的是对象还是引用呢?》这个问题,网上分为两种说法:
- 一种是JavaGuide为主的,认为字符串池里既存了引用也存了对象;
- 另一种是以R大为主的认为字符串池存储的只是字符串对象的引用。
有个文章专门归总写了一下,感兴趣的可以去看一下。链接:关于字符串池存储的是引用还是对象的争议_@baseException的博客-CSDN博客_字符串常量池放引用还是对象
不过还好,在我千辛万苦,坚持不懈下,这件事终于有头绪了!!
我辛苦做了什么:
用各种搜索引擎搜索各种国内国外论坛、博客。
还翻阅了Oracle官网提供的jdk8的《java语言规范》的“JLS3.10.5. String Literals”对字符串字面量的描述的章节和《JVM虚拟机规范》的“2.5.4. Method Area和2.5.5. Run-Time Constant Pool”章节。(但是我并没有在官方文档中看到有关“字符串常量池”的相关英文词汇,鄙人不禁怀疑:难道是“社会程序员自创的”?有看到的可以评论区留言一下具体词汇位置,万分感谢!!)
JavaGuide在他自己的github上回答了这个争议性问题:
- 承认了自己在这个问题上的观点是错误的,这事还得感谢github网友@popsicle256踊跃的对JavaGuide大佬提出质疑,该问题才得以解决(链接翻到最后就能看见:JVM 垃圾清理 标记-清除算法 常量池移除问题 · Issue #747 · Snailclimb/JavaGuide · GitHub)。
- 并修改了相关的所有文章(两篇文章链接:JavaGuide/memory-area.md at main · Snailclimb/JavaGuide · GitHub与https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/basis/java-basic-questions-02.md)。
R大说的是对的!!(字符串池存储的只是字符串对象的引用)
R大(RednaxelaFX)的说法的原文在知乎的一个问题里(链接:JVM 常量池中存储的是对象还是引用呢? - 知乎 ),
回答原文如下:
如果您说的确实是runtime constant pool(而不是interned string pool / StringTable之类的其他东西)的话,其中的引用类型常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之类)都存的是引用,实际的对象还是存在Java heap上的。
还有评论区中R大的精彩回答:
问:那如果是字符串常量池呢,jdk1.7的话,是存的对象吗?
答:用于管理interned String的那个string pool / StringTable么?那个也只是存引用而不是存对象。
问:哦哦,那这个是在哪里规定的?虚拟机规范里面吗?
答:虚拟机规范里把存储Java对象的地方定义为Java heap,其它地方是不会存有Java对象的实体的(有的话那根据定于也要算Java heap的一部分)
问:传说中的R大出现了,再问一下StringTable本身又存在哪里呢,有人说是方法区,又有人说是native memory?
答:HotSpot VM的StringTable的本体在native memory里。它持有String对象的引用而不是String对象的本体。被引用的String还是在Java heap里。一直到JDK6,这些被intern的String在permgen里,JDK7开始改为放在普通Java heap里。
## 关于《String s = new String("abc")会产生几个String对象?》的问题(答:两个String对象)
String s = new String("abc")创建对象的时候,会创建两个String对象,而且两个对象都在堆中!如下:
①"abc"会先在堆中创建一个String对象String("abc"),然后把引用存储到字符串常量池stringTable本体中(而不是像大部分网友说的:在字符串常量池中创建对象)。(解释:如文章前面描述,字符串常量池实现类是stringTable本体,其中存储的是引用,而不是对象)
②然后会再次在堆中创建new一个String("abc")对象。
具体情况可以通过查看.class字节码文件知晓:
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <abc>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
代码证明:
public class Test {
public static void main(String[] args) {
//创建一下,然后intern一下,
// ① false如果返回的引用和原引用不同,说明字符串常量池中已存在该引用,即new string(...)创建了两个对象。
// ② true如果返回的引用和原引用相同,说明字符串常量池中不存在该引用,即intern后只是向字符串常量池中添加该对象的引用。
String s1 = new String("abc");
String intern1 = s1.intern();
System.out.println(s1==intern1);// false,符合第一种说法,创建了两个对象
String s2 = new String("a")+new String("b"); //① 常量池有"a"和"b"两个对象的引用,但是没有"ab"。② 堆中有String("a")和String("b")和String("ab")和StringBuilder对象。四个不同的对象。
String intern2 = s2.intern();
System.out.println(s2==intern2);// true,符合第二种说法,只创建了一个对象
}
}
重点——如果还有疑问,请查看这两篇文章,保证把字符串常量池了解的非常透彻:
知乎文章:从字符串到常量池,一文看懂String类! - 知乎 (zhihu.com)
当然还可以再更深层次了解一下:jvm从HotSpot VM源码看字符串常量池(StringTable)和intern()方法_HD243608836的博客-CSDN博客
## 说一下String.intern()的问题
JDK6 中,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址(因为jdk6时,堆与方法区隔离(方法区在jdk6中的实现是永久代,而运行时常量池jdk6时属于永久代)
JDK7 起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址(因为jdk7时,方法区概念的实现是元空间(永久代被移除),而运行时常量池从jdk7开始,被放到了堆中了,不再归属于方法区)
代码证明:
public class Test {
public static void main(String[] args) {
//创建一下,然后intern一下,
// ① false如果返回的引用和原引用不同,说明字符串常量池中已存在该引用,即new string(...)创建了两个对象。
// ② true如果返回的引用和原引用相同,说明字符串常量池中不存在该引用,即intern后只是向字符串常量池中添加该对象的引用。
String s1 = new String("abc");
String intern1 = s1.intern();
System.out.println(s1==intern1);// false,符合第一种说法,创建了两个对象
String s2 = new String("a")+new String("b"); //① 常量池有"a"和"b"两个对象的引用,但是没有"ab"。② 堆中有String("a")和String("b")和String("ab")和StringBuilder对象。四个不同的对象。
String intern2 = s2.intern();
System.out.println(s2==intern2);// true,符合第二种说法,只创建了一个对象
}
}
重点——如果还有疑问,请查看这两篇文章,保证把字符串常量池了解的非常透彻:
知乎文章:从字符串到常量池,一文看懂String类! - 知乎 (zhihu.com)
当然还可以再更深层次了解一下:jvm从HotSpot VM源码看字符串常量池(StringTable)和intern()方法_HD243608836的博客-CSDN博客
## 关于《字符串字面量(String literals)何时进入到字符串常量池中?》的问题
(参考链接:Java字符串字面量是何时进入到字符串常量池中的_TomAndersen的博客-CSDN博客)
字符串字面量(String literals),和其他基本类型的字面量或常量不同,并不会在类加载中的解析(resolve) 阶段填充并驻留在字符串常量池中,而是以特殊的形式存储在 运行时常量池(Run-Time Constant Pool) 中。而是只有当此字符串字面量被调用时(如对其执行ldc字节码指令,将其添加到栈顶),HotSpot VM才会对其进行resolve,为其在字符串常量池中创建对应的String实例。
不同jdk版本中,字符串字面量(String literals)的特殊存放形式如下:
- 在JDK1.7的HotSpot VM中,这种还未真正解析(resolve)的String字面量,以JVM_CONSTANT_UnresolvedString的形式存放在运行时常量池中,此时并未为其创建String实例;
- 在JDK1.8的HotSpot VM中,这种未真正解析(resolve)的String字面量,被称为pseudo-string,以JVM_CONSTANT_String的形式存放在运行时常量池中,此时并未为其创建String实例。
三个阶段中,字符串字面量(String literals)状态如下:
- 在编译期,字符串字面量以"CONSTANT_String_info"+"CONSTANT_Utf8_info"的形式存放在class文件的 常量池(Constant Pool) 中;
- 在类加载之后,字符串字面量以"JVM_CONSTANT_UnresolvedString(JDK1.7)"或者"JVM_CONSTANT_String(JDK1.8)"的形式存放在 运行时常量池(Run-time Constant Pool) 中;
- 在首次使用某个字符串字面量时,字符串字面量以真正的String对象的方式存放在 字符串常量池(String Pool) 中。
示例代码:
package cn.tomandersen.javastudy.LeetCode;
public class Test {
public static void main(String[] args) {
String s1 = new String("He") + new String("llo");// 堆上创建"Hello","He","llo"实例,String Pool中创建"He"和"llo"实例
s1.intern();// 将堆上"Hello"的引用存入String Pool
String s2 = "Hello";// 获取String Pool中的"Hello"的引用
System.out.println(s1 == s2);// true
}
}
## 一些其它问题
为什么要有字符串常量池?
- 8种基本类型的6种常量池(Float和Double没有)都是系统协调的(实现为各个基本数据类型的包装类的内部静态类,如IntegerCache、LongCache),而
String
类型的常量池比较特殊。 - 字符串常量池就是由JVM提供的用来复用对象的一个对象池。它位于堆内存中。当我们使用
字面量(
使用双引号来直接创建对象,这种直接声明的方式叫做字面量)
创建字符串时,字符串常量池会将其对象引用进行保存,如果之后创建重复的字面量就会直接返回字符串常量池中的引用。有效地避免资源的重复创建。
String为什么是不可变的?
- 只有字符串不可变,字符串常量池才能发挥作用。对其进行字面量创建字符串对象时,没有便在字符串常量池中创建对象,有的话字符串常量池会返回已有对象的引用。如果字符串是可变的,引用的值就可以随时被修改并影响其他的引用,数据会产生错误。常量池就不能实现其复用功能了。
至于“String为什么是不可变的?为什么要有字符串常量池?”的详细分析请移步博客:
https://blog.csdn.net/HD243608836/article/details/126589892
什么是字面量?什么是符号引用?
(参考:终于搞懂了 Java 8 的内存结构,再也不纠结方法区和常量池了!_业余草-商业新知)
-
字面量
java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:
int a=1; // 这个1便是字面量
String b="iloveu"; // iloveu便是字面量
-
符号引用
由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后再用这个符号引用去获取他的内存地址。
例子:我在com.demo.Solution类中引用了com.test.Quest,那么我会把 com.test.Quest 作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。
## 常量值
常量值又称为字面常量,它是通过数据直接表示的
按照数据类型分类有如下六种:
① 整型常量值:
如 123② 浮点型常量值:
如 3.14、3.14F(这里的实,表示实数。实数定义为与数轴上点相对应的数。实数可以直观地看作有限小数与无限小数)
③ 布尔型常量值(boolean):
如 true、false④ 字符常量值(char):
如 'a'、'2'、'啊'、'\r'(回车)、'\n'(换行)、'\''(单引号字符)、'\\'(反斜杠字符)
(单引号修饰的一个字符或者转义字符。)
(可以是英文字母、数字、标点符号,以及由转义序列来表示的特殊字符)ps:除了以上所述形式的字符常量值之外,Java 还允许使用一种特殊形式的字符常量值来表示一些难以用一般字符表示的字符,这种特殊形式的字符是以开头的字符序列,称为转义字符。
⑤ 字符串常量值(String):
如 "a"、"abc"、"ab\r"(结尾回车)、"ab\\cde"(中间夹杂着反斜杠字符)
(双引号修饰的一个或多个字符或者转义字符)⑥ null常量值:
null常量只有一个值null,表示对象的引用为空。
## 常量
注意:常量不同于常量值,不要混淆。给常量初始化时赋的值,即常量值。
常量与变量之间的关系
常量:Java 语言中,用final修饰的变量表示常量。值一旦给定就无法改变!有类成员常量、静态常量、局部常量三种。例如 final int y = 10;
变量:有类成员变量、静态变量、局部变量三种。例如 int y = 10;
二者对比:
- 常量和变量是 Java 程序中最基础的两个元素。
- 常量的值是不能被修改的,而变量的值在程序运行期间可以被修改。
- 为了与变量区别,常量取名一般都用大写字符,单词之间下划线隔开。
常量有三种类型:成员常量、静态常量、局部常量。
public class HelloWorld {
// 声明并初始化成员常量
final int y = 10;
// 声明并初始化静态常量
public static final double PI = 3.14;public static void main(String[] args) {
// 声明并初始化局部常量
final double x = 3.3;
}
}
四、补充:封装类常量池(基本数据类型的包装类)
除了字符串常量池,Java的基本类型的封装类大部分(8大基本数据类型中的6大类型)也都实现了常量池。包括Byte,Short,Integer,Long,Character,Boolean
注意,浮点数据类型
Float,Double
没有封装类常量池。
封装类的常量池是在各自内部类中实现的,比如IntegerCache
(Integer
的内部类)。
public final class Integer extends Number implements Comparable<Integer> {
...
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[]; //池中的仍然是Integer类型
...
}
}
## 取值范围
要注意的是,这些封装类常量池是有范围的:
-
Byte,Short,Integer,Long : [-128~127]
-
Character : [0~127]
-
Boolean : [True, False]
所以基本数据类型的包装类之间比较时,尽量使用equals()。
因为范围内的是new一个Integer放在池中,范围内的都可以复用,所以==判断为true。
但是范围外的则都是新new的Integer,所以为false。
## 调用方法(指定valueof()方法)
另外强调,注意:
必须调用包装类的valueof()方法才能加入封装类常量池。
正常new的构造方法不会加入封装类常量池。
查看Integer源代码可以验证这一说法:
valueof()方法
正常的构造方法
四、创建对象
五、 方法区(Method Area)存储什么
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:
它用于存储已被虚拟机加载的类信息、方法信息、常量(final)、静态变量(static)、即时编译器编译后的代码缓存(JIT)等。
关于字符串常量池和运行时常量池的位置说明:
JDK版本 | 方法区实现 | 变化 |
---|---|---|
jdk1.6 | 永久代 | 字符串常量池、运行时常量池、静态变量都是在永久代中 |
jdk1.7 | 永久代 | 字符串常量池和静态变量被移动到了堆当中, 运行时常量池还是在永久代中 |
jdk1.8 | 元空间 | 字符串常量池和静态变量(static)仍然在堆当中; 运行时常量池、类型信息、 |
注意:其中“常量(final)”被我划掉了!后面“两个问题”中有解释!
## 两个很重要的问题:
1. 由final修饰的常量存放在哪里?
final 关键字并不影响在内存中的位置,与其无关!!
具体位置请参考下一问题!!!
2. 成员变量、局部变量、类变量分别存储在内存的什么地方?
- 类变量
类变量是用static修饰符修饰,
定义在方法外的变量,随着java进程产生和销毁。
位置:
在jdk7之前,静态变量存放于方法区。在jdk7开始,转移存放在堆中。
-
成员变量
成员变量是定义在类中,但是没有static修饰符修饰的变量,
随着类的实例产生和销毁,是类实例的一部分。位置:
由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
-
局部变量
局部变量是定义在类的方法中的变量。
位置:
在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
另外,援引一段JavaGuide中的原文:Java 内存区域详解 | JavaGuide
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,方法区到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
另外,周志明老师在《深入理解 Java 虚拟机(第 3 版)》
Page 272中原文(注意其中类变量指的是static修饰的成员变量!我后面有解释!!):
【JDK 7之前】,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;
【而在JDK 7及之后】,类变量则会随着Class对象一起存放在Java堆中
进一步解释一下:
Java中的“成员变量”分为两种:
- 实例变量:第一种,是没有static修饰的,这些成员是对象中的成员,称为实例变量(也叫非静态变量)
- 类变量:第二种,是有static修饰的,称为类变量(也叫静态变量)
两种“成员变量”的存放位置:
- 实例变量:
随着对象的建立而存在heap堆中。- 类变量:
jdk6及之前,随着类的加载而存储在方法区中(永久代)。
jdk7开始,随着类的加载而存储在heap堆中(从永久代中转移出去了,转移到堆中了)。ps:
注意:“类加载过程”与“创建对象”是两码事——java在new一个对象的时候
会先查看对象所属的类有没有被加载到内存
,如果没有的话,就会先通过类的全限定名来加载。- 加载并初始化类完成后,
再进行对象的创建工作
。
六、小结HotSpot VM的内存结构
简单总结一下虚拟机规范的内容:
Heap
用于为所有对象和数组分配内存Method Area
用于保存类/接口的结构信息Run-Time Constant Pool
用于保存各种常量
具体详细内容参看:从Java的《jvm虚拟机规范》看HotSpot虚拟机的内存结构和变迁_HD243608836的博客-CSDN博客
以上,就是我这三天多来对jvm内存研究的心血!!
文章中每一个外部链接我都认认真真的读过很多遍。兴趣和毅力真的很重要,大家共同加油吧!!