Java基础之《JVM性能调优(4)—JVM方法区原理》

一、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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值