JVM系列六——方法区(Method Area)

一、存储内容

在这里插入图片描述

1.1 类型信息

  • 类型的全限定名
  • 超类的全限定名
  • 直接超接口的全限定名
  • 类型标准(类类型或接口类型)
  • 类的访问描述符(public、private、 default、final、abstract、static)

1.2 类型常量池

存放该类所有用到的常量的有序集合,包括直接常量(字符串、整数、浮点数常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的元素一样。

1.3 字段信息

  • 字段修饰符(public、protected、private、final、static)
  • 字段的类型
  • 字段名称

1.4 方法信息

方法信息包含类的所有方法,每个方法包含以下信息:

  • 方法修饰符
  • 方法返回类型
  • 方法名
  • 方法参数个数、类型、顺序
  • 方法字节码
  • 操作数栈和该方法在栈帧中的局部变量区大小
  • 异常表

1.5 类变量(静态变量)

指该类所有对象共享的变量,即使没有实例对象,也可以直接访问的类变量。它们与类进行绑定。

1.6 指向类加载器的引用

每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。

1.7 指向Class实例的引用

类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该Class对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。

1.8 方法表

为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。

1.9 运行时常量池(Runtime Constant Pool)

  • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
  • 运行时常量池具有动态性,可以在运行期间将新的常量放入池中(如String类的intern()方法)。

二、永久代和元空间的区别

永久代和元空间存储位置和存储内容的区别:

  • 存储位置不同,永久代是堆中的一部分,和新生代,老年代的地址是连续的,而元空间使用本地内存
  • 永久代的大小比较小,元空间的大小取决于本地内存
  • 永久代容易出现OOM,而元空间一般不会
  • 元空间存储类的元信息,静态变量常量池移入对中。相当于永久代的数据被分割到元空间和堆中

2.1 为什么要把永久代替换为元空间

  • .原来Java是属于Sun公司的,后来Java被Oracle收购了。Sun公司实现的Java中的JVM是Hotspot。当时Oracle堆Java的JVM也有一个实现,交JRockit。后来Oracle收购了Java之后,也同时想把Hotspot和JRockit合二为一。他们俩很大的不同,就是方法区的实现。
  • 字符串存在永久代中,容易出现性能问题和永久代内存溢出
  • 类及方法的信息比较难确定大小,因此对于永久代大小的指定比较困难,太小容易出现永久代内存溢出,太大容易出现老年代内存溢出
  • 永久代增加GC复杂度,并且回收效率偏低

三、方法区异常演示

3.1 类加载导致OOM异常

我们现在通过动态生成类来模拟方法区的内存溢出:

package com.kkb.test.memory;
public class Test {}

测试代码:

public class PermGenOomMock{
    public static void main(String[] args) {
        URL url = null;
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();

        try {
            url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true){
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                //每个ClassLoader对象,对同一个类进行加载,会产生不同的Class对象
                loader.loadClass("Test");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

JDK1.7分析指定的 PermGen 区的大小为 8M。
在这里插入图片描述

绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “ PermGen space ”其实指的就是方法区。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

JSP页面,需要动态生成Servlet类class文件

JDK1.8+
现在我们在 JDK 8下重新运行一下案例代码,不过这次不再指定 PermSize 和 MaxPermSize 。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize 的大小。输出结果如下:
在这里插入图片描述
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

3.2 字符串OOM异常

以下这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存:

package com.kkb.test.memory;
import java.util.ArrayList;
import java.util.List;
public class StringOomMock {    
    static String  base = "string";    
    public static void main(String[] args) {        
        List<String> list = new ArrayList<String>();        
        for (int i=0;i< Integer.MAX_VALUE;i++){            
            String str = base + base;            
            base = str;            
            list.add(str.intern());        
       }    
   }
}

JDK 1.6 的运行结果:
在这里插入图片描述
JDK 1.6下,会出现永久代的内存溢出。
JDK 1.7的运行结果:
在这里插入图片描述
在 JDK 1.7中,会出现堆内存溢出。结论是:JDK 1.7 已经将字符串常量由永久代转移到堆中。
JDK 1.8的运行结果:
在这里插入图片描述
在JDK 1.8 中,也会出现堆内存溢出,并且显示 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以验证 JDK 1.8 中已经不存在永久代的结论。

四、运行时常量池和字符串常量池

4.1 三大常量池区别

  • class常量池
    每一个class文件中都有class常量池,既然是文件中的,那么自然此时的常量池是“静态的”(虽然还是叫池会给人一种在内存中的感觉,可实际上所谓的class常量池,就是class文件里面的具体字节码)。
  • 运行时常量池
    class文件中的常量池字节码会被加载到内存里,所有的class常量池,最后都会被加载到运行时常量池中。运行时常量池在jdk1.6时放在方法区中,jdk7放在堆内存中,jdk8放在元空间中。
  • 字符串常量池
    jdk7以后,字符串常量池存在于堆中(这与运行时常量池不同)。
    当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
    当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s。

4.2 字符串常量池如何存储数据的?

字符串常量池使用StringTable,它是类似于hashtable的数据结构在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=99991。
hashtable/hashmap数据结构如下:

  • 底层数组是:数组+链表
  • 数组中的元素是entry对象。
  • entry对象是包含一个K/V对的。
  • K就是hashmap中的key。
  • V就是hashmap中的value。

字符串常量池查找字符串的方式:
在这里插入图片描述

  • 根据字符串的 hashcode找到对应entry。如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串。
  • 如果找得到字符串,返回引用。如果找不到字符串,会把字符串放到常量池,并把引用保存到StringTable里。

注意
StringTable大小是固定的,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

4.3 字符串常量池中存储的是字符串的值对象,还是引用?

使用 new String() 在堆中创建了一个字符串对象
在这里插入图片描述
使用了 intern() 之后发生了什么呢,在常量池新增了一个对象,但是 并没有 将字符串复制一份到常量池,而是直接指向了之前已经存在于堆中的字符串对象。下图是只调用了 s2.intern(),并没有返回给一个变量。其中字符串常量池(0x88)指向堆中字符串对象(0x99)就是intern() 的过程。
在这里插入图片描述
只有当我们把 s2.intern() 的结果返回给 s2 时,s2 才真正的指向字符串常量池。
在这里插入图片描述

结论
在 JDK 1.7 之后,字符串常量池不一定就是存字符串对象的,还有可能存储的是一个指向堆中地址的引用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值