方法区
1. 简介
方法区是存放基础信息的位置,线程共享,主要包含三部分内容:
- 类的元信息: 保存了所有类的基本信息。
- 运行时常量池: 保存了字节码文件中的常量池内容。
- 字符串常量池: 保存了字符串常量。
2. 存储类的元信息
方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象。在类的加载阶段完成。
3. 存储运行时常量池
方法区还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
4. 方法区的变化
方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:
- JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
- JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
arthas中查看JDK8方法区
5. 模拟方法区溢出
通过ByteBuddy框架,动态生成字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察方法区是 否会出现内存溢出的情况。分别在JDK7和JDK8上运行上述代码。引入依赖:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.16.1</version>
</dependency>
编写代码:
public static void main(String[] args) {
Jvm03 jvm03 = new Jvm03();
String name = "Class";
int count = 1;
while (true) {
// 类名不断变化,可以让类加载器不断加载
name += count;
// 创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0);
// 调用visit方法,创建字节码数据
// Opcodes.V1_7表示生成jdk1.7版本的字节码
classWriter.visit(
Opcodes.V1_7,
Opcodes.ACC_PUBLIC,
name,
null ,
// 指明父类
"java/lang/Object",
null);
byte[] bytes = classWriter.toByteArray();
jvm03.defineClass(name, bytes, 0, bytes.length);
System.out.println(count++);
}
}
运行的话需要交给JDK7进行执行,然后修改生成字节码为1.8,再在JDK8环境运行。
实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行百万次,程序都没有出现任何错误,但是内 存会直线升高。这说明JDK7和JDK8在方法区的存放上,采用了不同的设计。
提示
ByteBuddy是一个基于Java的开源库,用于生成和操作Java字节码。
6. 方法区大小设置
- JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数
-XX:MaxPermSize
=值来控制。 - JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受 的上限,可以一直分配。可以使用
-XX:MaxMetaspaceSize
=值将元空间最大大小进行限制。
为何要设置方法区大小呢,毕竟已经交由操作系统管理,JVM又不能回收?
主要考虑到在一个操作系统里面部署多个应用的场景,防止某个应用的方法区不合理导致其他应用不可用,比如后部署的应用启动不起来,运行一段时间就所有应用都挂掉等等。
在JDK8中设置-XX:MaxMetaspaceSize
参数执行上面的程序:
7. 字符串常量池
方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)。字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123就会被放入字符串常量池。 字符串常量池和运行时常量池有什么关系?
早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整,将字符串常量池和运行时常量池做了拆分。
- 通过字节码指令分析如下代码的运行结果?
答案是false true。原因是
String d=a+b
编译为使用StringBuilder进行字符串拼接,StringBuilder对象创建时存放在堆内存的,而String c="12"
是在方法区字符串常量池创建,自然地址不同为false。而String d="1"+"2"
编译为编译阶段直接连接,也就是String d="12"
,对于前面变量c已经创建的字符串常量12, 后面相同就不会创建而是使用引用,因为比较结果为true。
8. 神奇的intern
String.intern()方法是可以手动将字符串放入字符串常量池中,分别在JDK6、JDK8下执行代码,JDK6中结果是false false ,JDK8中是true false jdk6中容易理解,使用
intern()
方法后两个字符串一个存在堆上一个存在字符串常量池,所以都是不等的都是false。
但是为何在jdk8中会第一个为true呢?原因在于JDK7及之后版本中由于字符串常量池在堆上,所以intern ()
方法会把第一次(只是第一次才这样)遇到的字符串的引用放入字符串常量池。所以字符串常量池中存放的还是堆上面的对象,所以为true。
问题:运行时数据区都学完了,静态变量存储在哪里呢?
JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。