文章目录
一、存储内容
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 之后,字符串常量池不一定就是存字符串对象的,还有可能存储的是一个指向堆中地址的引用。