一、JVM的方法区是干什么用的?
1、跑一个main函数需要多少个类?类存在哪里?
package heap;
public class GcTest02 {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000000000);
}
}
在JDK安装目录下,C:\Program Files\Java\jdk1.8.0_291\bin
打开jvisualvm.exe,选择我们运行的这个应用,抽样器 - 内存 - 停止
2、方法区
以上字节码以class保存在磁盘中,java运行的时候必须把这个文件加载到内存中,存储在jvm的方法区中
1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
2)方法区在JVM启动时就会被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
3)方法区的大小,跟堆空间一样,可以选择固定大小(默认20MB)或者可拓展
4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace
例如:
有大量的第三方jar包加载;
Tomcat部署的工程过多;
大量动态生成反射类;
在Java1.8中,HotSpot虚拟机已经将方法区(永久代)移除,取而代之的就是元空间。
3、试验
限制springboot的方法区大小,看是否发生java.lang.OutOfMemoryError:Metaspace。
jvm配置上Metaspace的大小
-XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m
报错:
Exception in thread "background-preinit"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "background-preinit"
Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
二、方法区的内部结构
1、类信息
下图画框的就是类信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
1)这个类型的完整有效名称(全名=包名.类名)
2)这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
3)这个类型的修饰符(public、abstract、final的某个子集)
4)这个类的直接接口的一个有序列表
2、域信息(成员变量)
JVM必须在方法区中保存类的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
3、方法信息
三、class常量池有什么作用?
1、常量池
1)常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
2)字面量就是我们所说的常量概念,如文本字符串String、被声明为final的常量值等。
3)符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
类在解析阶段会将符号引用转换为直接引用
一般包括下面三类常量:
类和接口名
类成员变量名
方法的名称
2、常量池有什么好处?有什么作用?
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
3、类的静态变量如何引用
#5就是对应常量池里的第5个,引用的字符串是#9,就是nameA
引用#9
static String nameA = "nameA"; //静态成员变量在类的初始化方法里初始化
static final String nameB = "nameB"; //静态常量在编译时就初始化好了
String nameC = "nameC"; //成员变量在实例的初始化方法里初始化
四、方法区的全局字符串池与运行时常量池有什么区别?
1、全局字符串池(string pool)
字符串池 / 字符串常量池(String Pool / String Pool Constant Pool):是class常量池中的一部分,存储编译期类中产生的字符串类型数据。
然后将该字符串对象实例的引用值存到String Pool中。有关字符串的数据全部存储在这个池里面。
String Pool的底层就是一个StringTable类,它是一个固定大小的HashTable,默认值大小长度是1009,里面存的是驻留字符串的引用。
也即是说数据存于堆中,StringTable保持引用地址。
在JVM中,StringTable只有一个,被所有类共享。
2、运行时常量池(runtime constant pool)
运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把Class常量池中的数据放入到运行时常量池。
运行时常量池是在类加载完成之后,将每个Class常量池中的符号引用值 转存 到运行时常量池中。
即,类加载解析之后,将符号引用 替换成 直接引用,与全局常量池中的引用值保持一致。
3、字符串常量池是属于.class类文件的,运行时常量池是JVM加载字符串常量池到方法区
4、几个例子
package Metaspace;
import org.junit.Test;
public class TestB {
@Test
public void test1() {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); //false
}
@Test
public void test2() {
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s2); //false
System.out.println(s2 == s3); //true
}
@Test
public void test3() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; //程序执行时处理
System.out.println(s3 == s4); //false
String s5 = "a" + "b"; //编译时就处理了
System.out.println(s3 == s5); //true
}
@Test
public void test4() {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); //true
}
}
4-1、test1()执行结果为false
(1)类加载对一个类只会进行一次,“abc”在类加载时就已经创建并驻留在全局字符串池StringTable中。
(2)new String("abc")将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s2持有。
4-2、test2()执行结果为false、true
(1)s2是在运行的时候调用intern()函数,返回常量池StringTable中"abc"的引用值,故s2、s3都是常量池,就相同了。
(2)s1的“abc”是保存在堆中。
4-3、test3()执行结果为false、true
(1)s1 + s2的执行细节
StringBuilder s = new StringBuilder();
s.append("a")
s.append("b")
s.toString() --> 约等于new String("ab")
补充:在jdk5.0之后使用StringBuilder,jdk5.0之前使用StringBuffer
(2)s5常量与常量的拼接结果在常量池,原理是编译期就把它处理,“ab”存于常量池。
4-4、test4()执行结果为true
(1)如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
5、补充题
@Test
public void test() {
String s1 = "abcdef";
String s2 = "abc";
String s3 = s2 + "def";
System.out.println(s1 == s3);
final String s4 = "abc";
String s5 = s4 + "def";
System.out.println(s1 == s5);
}
第一个输出是false,第二个输出是true
s3编译时,符号引用替换为值,s3 = "abc" + "def",程序执行时,用StringBuilder拼接一个新的String。
s5编译时,都是常量,从常量池里取“abcdef”。
new String(“abc”)创建了几个对象
https://baijiahao.baidu.com/s?id=1714914983791082793