方法区线程共享,存了以下几部分:
类的元信息,即类生命周期的加载阶段的InstanceKlass对象。PS:图中InstanceKlass对象里的常量池、方法等,实际存的只是引用,JVM会把它们摘出来统一安排在一块内存上。
运行时常量池,和类生命周期的连接阶段的操作,把编号变为内存地址:
方法区是一个虚拟概念,不同的虚拟机有不同的实现,对于HotSpot:
PS:
使用阿尔萨斯查看:JDK8时,max为-1,即不设上限,但自然不会超过操作系统的内存上限
通过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);
//JDK版本为8
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=值
将元空间最大大小进行限制,再运行:
字符串常量池存储在代码中定义的常量字符串的内容,比如"123"
关于字符串常量池和运行时常量池的关系:
图示:JDK6时:
JDK7时:
JDK8时:
如下,根据字节码,c指向字符串常量池,而a+b实际是用StringBuilder,得到一个String对象,指向堆内存,c不等于d
调整变量d的代码,现在输出为true,字节码中不再用StringBuilder:
+的两边是变量还是常量的区别为:
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时,只是在常量池中存放了这个对象的引用,而不是将字符串搬运到常量池中。
和JDK版本有关: