文章目录
1、方法区
方法区线程共享,存了以下几部分:
- 类的元信息
- 运行时常量池
- 字符串常量池
类的元信息,即类生命周期的加载阶段的InstanceKlass对象。PS:图中InstanceKlass对象里的常量池、方法等,实际存的只是引用,JVM会把它们摘出来统一安排在一块内存上。
运行时常量池,和类生命周期的连接阶段的操作,把编号变为内存地址:
2、方法区的位置
方法区是一个概念,不同版本的JDK有不同的实现,对JDK7来说,永久代是其对方法区的落地实现(且此时永久代在堆区),对JDK8来说,则给方法区换了一种实现:元空间(元空间在本地内存)
方法区是一个虚拟概念,不同的虚拟机有不同的实现,对于HotSpot:
- JDK7及以前,方法区在堆区的永久代空间里
- JDK8及以后,永久代被移除,用元空间代替,方法区在元空间,而元空间在操作系统的直接内存里,理论上可以一直分配
PS:
使用阿尔萨斯查看:JDK8时,max为-1,即不设上限,但自然不会超过操作系统的内存上限
3、模拟方法区的溢出
通过ByteBuddy框架,生成类的字节码,然后往内存(方法区)中加载。首先引入依赖:
<!--ByteBuddy是一个用于生成和操作Java字节码的框架-->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.23</version>
</dependency>
基本使用方式:
//创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0);
//生成字节码数据
classWriter.visit(Opcodes.V1_7,Opcodes.ACC_PUBLIC,name,null ,"java/lang/Object",null);
byte[] bytes = classWriter.toByteArray();
//visit方法的形参中,第一个为编译类的JDK版本,name为类名,批量生成时,注意别重复,第五个为父类
Demo代码:
public class Demo1 extends ClassLoader {
public static void main(String[] args) throws Exception {
int count = 1;
Demo1 demo1 = new Demo1();
while (true) {
ClassWriter classWriter = new ClassWriter(0);
//参数1为JDK版本(JDK8),参数2为public修饰符,参数3为类名,参数4为包名,参数5为父类,参数6为接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + count, null, "java/lang/Object", null);
byte[] bytes = classWriter.toByteArray();
//加载字节码
demo1.defineClass("Class" + count, bytes, 0, bytes.length);
System.out.println(count++);
}
}
}
JDK7的JVM上运行,报错PermGen Space,而JDK8的JVM下则只是看到系统内存一直在涨:
用-XX:MaxMetaspaceSize=值
将元空间最大大小进行限制,再运行:
4、方法区的字符串常量池
字符串常量池存储在代码中定义的常量字符串的内容,比如"123"
关于字符串常量池和运行时常量池的关系:
图示:JDK6时:
JDK7时:
JDK8时:
5、常量池案例
如下,根据字节码,c指向字符串常量池,而a+b实际是用StringBuilder,得到一个String对象,指向堆内存,c不等于d
调整变量d的代码,现在输出为true,字节码中不再用StringBuilder:
+的两边是变量还是常量的区别为:
6、String的intern方法
intern方法手动将字符串放入字符串常量池中,如下:常量池中只是存了一份,结果为true:
案例2:
JDK6下运行:
false
false
JDK8下运行:
true
false
分析前置Tip:JVM启动时就会把java加入到常量池中。
原因:JDK6下的intern方法,第一次遇到字符串实例时,复制到永久代的字符串常量池中,并返回常量池中的引用,即s1.intern是一个指向字符串常量池的引用,而s1后面是个对象,因此s1是指向堆的一个引用。s1 不等于 s1.intern。同理,java字符串对象,s2.intern,发现常量池已有java,直接返回引用(地址),也是false。
JDK7及之后版本中由于字符串常量池在堆上,所以intern 方法会把第一次遇到的字符串的引用放入字符串常量池,此时,s1和s1.intern都指向堆里的think123对象,为true
而对于s2,常量池中已有java,因此s2.intern直接是字符串常量池中java的地址,不等于s2.
JDK7及以后,在堆上创建的字符串(对象),去调用intern时,只是在常量池中存放了这个对象的引用,而不是将字符串搬运到常量池中。
7、静态变量的存放位置
和JDK版本有关:
- JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代
- JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代
PS:
8、常量池和运行时常量池的区别
常量池(Constant Pool)
常量池 是指在编译阶段生成的,用于存储编译期确定的一些常量,包括字符串常量、基本类型常量(如 int、float、long 等)、类引用、方法引用、字段引用等。常量池存在于每个 .class 文件中,具体来说,可以分为以下几个部分:
- 字面量:包括字符串常量和基本类型的常量
- 符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符等
这些常量在 .class 文件中以常量池表(constant pool table)的形式存在,每个常量都有一个唯一的索引
运行时常量池(Runtime Constant Pool)
运行时常量池 是 JVM 在类加载过程中,从 .class 文件的常量池中提取出来并放入方法区中的一部分数据结构。它主要用于存储运行时需要用到的各种常量和符号引用。运行时常量池的特点:
- 动态性:除了从编译期常量池中获取的常量外,运
行时常量池可以动态地添加新的常量,比如通过 String.intern() 方法
- 符号引用解析:在类加载阶段,JVM 会将符号引用解析为直接引用,这个过程可能会触发类加载、连接和初始化
区别总结
位置和存储时间:
- 常量池:存在于每个 .class 文件中,是编译期生成的,在磁盘上存储
- 运行时常量池:存在于 JVM 的方法区中,是运行时生成的,在内存中存储
内容:
- 常量池:包含编译期确定的字面量和符号引用
- 运行时常量池:包含编译期常量池中的内容,并且可以在运行时动态扩展,包含运行时解析后的符号引用
修改性:
- 常量池:不可修改,一旦编译完成就固定了
- 运行时常量池:可以在运行时动态添加新的常量
用途:
- 常量池:用于支持编译期的常量表达式和符号引用
运- 行时常量池:用于支持运行时的类加载和动态链接