一起学习JVM-内存结构-方法区(Method Area)(线程共享的区域)

我怕爱的太早我们不能终老 提交于 2020-01-29 09:53:49

方法区(Method Area)

定义: 存储了跟类相关的信息,如:成员变量、方法、构造器及常量池等。
逻辑上是堆的一部分,但是具体的实现是不一样的。比如:oracle公司的Hotspot JVM
在1.8之前方法区的实现叫永久代,就是使用堆的一部分作为方法区。
而1.8之后方法区的实现叫元空间,使用的是本地内存也就是系统内存。
在这里插入图片描述
特点:
1.所有Java线程共享的区域
2.能发生outOfMemoryErrot(内存溢出)
产生方法区内存溢出的场景:
动态产生class并加载的场景:如,
spring利用cglib生成的代理类,mybatis用cglib动态生成mapper接口的实现类等
在JDK1.8之前,spring、mybatis等动态生成的类还是很容易造成永久代的内存溢出。
在1.8之后,因为元空间使用的是系统内存,相对来说充裕了很多,而且垃圾回收也是由自己管理的
演示代码:
1.8 以前会导致永久代内存溢出:
注意:该代码应用于JDK1.6版本

/**
 * 演示永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
 * 设置启动参数,永久代最大内存为8M: -XX:MaxPermSize=8m
 */
public class Demo1_6 extends ClassLoader {// 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_6 test = new Demo1_6();
            for (int i = 0; i < 20000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 参数:版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_6, 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);
        }
    }
}

1.8 之后会导致元空间内存溢出:
注意:该代码应用于JDK1.8版本及之后

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * 设置启动参数,元空间最大内存为8M: -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);
        }
    }
}

运行时常量池

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

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

输入指令: javac HelloWorld.java-将HelloWorld 类编译成class文件
输入指令:javap -v HelloWorld.class-将class二进制文件反编译

StringTable(串池)

首先看一段面试代码:

 public class Demo1 {
    public static void main(String[] args) {
        String s1 = "a"; 
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        String s5 = "a" + "b";  
        //问
        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
    }
}

请问,在不运行的情况下,能否明确肯定的说出结果,如果不能,请看以下分析。
常量池与串池(StringTable)的关系:
1.类加载时常量池中的信息,都会被加载到运行时常量池中, 这时 a,b,ab 都是常量池中的符号,还没有变为 java 字符串对象。
2.第一次用到时才变为对象(懒惰的方式:延迟加载)
3.变成String对象后就会把它当成key到StringTable中去找有没有取值相同的key。
4.StringTable的数据结构就是一个哈希表,长度一开始是固定可并且是不能扩容的。
5.第一次从StringTable中找取值相同的key肯定是没有的,他就会把这个值存入StringTable中。
字符串变量的拼接:

String s4 = s1 + s2;

字符串变量的拼接其实会有以下操作:
1.首先new StringBuilder();说到这你肯定就会想到之后的操作了
2.append一个s1对应的值,在append一个s2对应的值
3.调用toString方法
4.赋值给字符串变量s4
其实做String s4 = s1 + s2;这样的操作时,底层就是new StringBuilder().append("a").append("b").toString()
那么s3 == s4吗?

System.out.println(s3 == s4);输出结果是什么,相信心中有了答案

false
也许你有疑惑,s3的值是“ab”,s4的值是“ab”为什么是false呢?
问题就在StringBuilder的toString()这个方法里。
看toString的源码:

 @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

它是new了一个新的String对象。所以说System.out.println(s3 == s4)输出结果为false
字符串常量的拼接:

String s5 = "a" + "b";  

这个相对于变量的拼接就简单许多,它其实在编译期间就会优化,因为“a”是常量,“b”是常量,“a” + "b"肯定也是常量啊。结果已经在编译期确定为“ab”了。
那么 System.out.println(s3 == s5)输出的结果是什么呢?

true

你是否存在疑惑?其实很简单:

 String s3 = "ab";

这个操作已经把常量"ab"放入StringTable中了。之后的String s5 = "a" + "b"; 这个操作就会去StringTable中找对应的值,因为StringTable中有"ab"这个值,所以s3和s5指向的是同一个常量"ab",所以 System.out.println(s3 == s5)输出的结果为true
StringTable 的特性:
1.常量池中的字符串仅是符号,第一次用到时才变为对象
2.利用串池的机制,来避免重复创建字符串对象
3.字符串变量拼接的原理是 StringBuilder (1.8)
4.字符串常量拼接的原理是编译期优化

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!