JVM专题-方法区

Posted IT老刘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM专题-方法区相关的知识,希望对你有一定的参考价值。

1.定义


方法区是一种规范永久代元空间都只是它的实现。

2.组成

3.方法区内存溢出

元空间大小调整参数

-XX:MaxMetaspaceSize=8m

案例代码

package com.method.area;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

由于元空间使用本地内存,所以很难方法区溢出所以需要手动调整元空间大小。

-XX:MaxMetaspaceSize=8m


运行结果 OutOfMemoryError: Metaspace

场景
spring、mybatis都用到了cglib技术,字节码的动态生成技术,动态加载类,动态生成类,运行期间,经常会产生大量的类,可能会产生方法区溢出

4.运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

案例

// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

通过反编译这段程序来学习相关的知识,用到的命令

  • 先进入到java源文件的目录
cd 目标目录
  • 将HelloWorld.java 编译成 HelloWorld.class
javac HelloWorld.java
  • 反编译HelloWorld.class
javap -v HelloWorld.class

结果如下

类基本信息

常量池

类方法定义

方法运行流程

  • 对于主方法来说,解释器依次执行指令。getstatic–>得到某个常量,索引为#2。

  • 以#2作为索引去常量池查询,得到 Fieldref ,即属性索引,索引为 #16,#17

  • 再以#16,#17为索引继续查询常量池

  • 依照这个步骤下去继续阅读即可

5.StringTable

5.1.常量池与串池的关系

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
 
}

运行时常量池

方法区

局部变量表

常量池最初存在于字节码中,运行时会被加载到运行时常量池中。这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象

字符串的加载是懒惰的,当执行到具体的引用时,才会创建对象。如String s1 = “a”。创建了s1对象。

同时会创建StringTable串池。将s1作为key去串池中寻找,如果没有,才会加入串池

5.2.变量字符串拼接

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
      // new StringBuilder().append("a").append("b").toString()  
      // new String("ab")
        String s4 = s1 + s2; 
      // 输出false,s3在常量池,s4在堆里
				System.out.println(s3 == s4);
    }
}



可以看到StringBuilder.toString是new了一个String,new的对象在堆里。

5.3.编译期优化(常量字符串拼接)

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
				// true
        System.out.println(s3 == s5);
    }
}

5.4 字符串延迟加载

/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println(); // 字符串个数 4695

        System.out.print("1");
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print("1"); // 字符串个数 4703
        System.out.print("2");
        System.out.print("3");
        System.out.print("4");
        System.out.print("5");
        System.out.print("6");
        System.out.print("7");
        System.out.print("8");
        System.out.print("9");
        System.out.print("0");
        System.out.print(x); // 字符串个数2285
    }
}



以上是关于JVM专题-方法区的主要内容,如果未能解决你的问题,请参考以下文章

JVM技术专题深入研究JVM内存逃逸原理分析「研究篇」

JVM 专题十二:运行时数据区对象的实例化内存布局与访问定位

jvm专题 - 1/3GC基础

JVM 专题七:运行时数据区程序计数器

JVM 专题十三:运行时数据区直接内存

Java技术专题较为深度的分析JVM的直接内存「上篇」