文章目录
运行时数据区概述
JVM运行时数据区规范
jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。
jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。
方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot 虚拟机不同版本的 两种实现。
Hotspot运行时数据区
分配jvm内存空间
- 分配堆的大小
–Xms(堆的初始容量)
-Xmx(堆的最大容量)
- 分配方法区的大小
-XX:PermSize 永久代的初始容量
-XX:MaxPermSize 永久代的最大容量
-XX:MetaspaceSize 元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释 放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适 当提高该值。
-XX:MaxMetaspaceSize 最大空间,默认是没有限制的。
- 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
- 分配线程空间的大小
-Xss:为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M
方法区
方法区存储内容
- 1:类型信息
- 类型的全限定名
- 超类的全限定名
- 直接超接口的全限定名
- 类型标志(该类是类类型还是接口类型)
- 类的访问描述符(public、private、default、abstract、final、static)
- 2:类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类 型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存着所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。 - 3:字段信息(类声明的所有字段)
- 字段修饰符(public、protect、private、default)
- 字段的类型
- 字段名称
- 4:方法信息
方法信息中包含类的所有方法,每个方法包含以下信息:- 方法修饰符
- 方法返回类型
- 方法名
- 方法参数个数、类型、顺序等
- 方法字节码
- 操作数栈和该方法在栈帧中的局部变量区大小
- 异常表
- 5:类变量
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。 - 6:指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。 - 7:指向Class类实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。 - 8:方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的。 - 9:运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面常量和符号引用,这部分内容被类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池中(典型的如String类的intern()方法)。
永久带与元空间区别
永久代和元空间存储位置和存储内容的区别:
- 存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存
- 存储内容不同,元空间存储类的元信息,[静态变量]和[常量池]等并入堆中。相当于永久代的数据被分到 了堆和元空间中。
JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。
不过大家应该都有一个疑问,就是为什么要做这个转换?带着这个疑问,最后给大家总结以下几点原
因:- 1:字符串存在永久代中,容易出现性能问题和永久代内存溢出
- 2:类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢 出,太大则容易导致老年代溢出
- 3:永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
- 4:Oracle 可能会将HotSpot 与 JRockit 合二为一
其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
方法区异常示例
类加载导致OOM异常
代码
public class Test {}
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList; i
mport java.util.List;
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);
loader.loadClass("Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
JDK1.7
指定的 PermGen 区的大小为 8M
绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “ PermGen space ”其实指的就是方法区。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况, 容易出现永久代内存溢出
JDK1.8+
现在我们在 JDK 8下重新运行一下案例代码,不过这次不再指定 PermSize 和 MaxPermSize 。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize 的大小。输出结果如下
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。
字符串OOM异常
案例代码
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());
}
}
}
JDK1.6
- JDK 1.6下,会出现永久代的内存溢出。
JDK1.7
- 会出现堆内存溢出。结论是:JDK 1.7 已经将字符串常量由永久代转移到堆中。
JDK1.8+
- 会出现堆内存溢出,并且显示 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以验证 JDK 1.8 中已经不存在永久代的结论。