一、方法区
1.1 定义
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(比如class文件)、常量、静态变量、即时编译器编译后的代码等数据。
1.2 结构
方法区是JVM 所有线程共享。
主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与 堆 进行区分,通常又叫 非堆。 关于 方法区内存溢出 的问题会在下文中详细探讨。
1.3 永久代和元空间
1.3.1 PermGen(永久代)
PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到 PermGen space 中。
绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError: PermGen space 这个异常。
这里的 PermGen space 其实指的就是 方法区 。不过 方法区 和 PermGen space又有一定的区别。
- 方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
- PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。
- Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机 对 方法区 的新的实现。
JDK6、JDK7 时,方法区 就是 PermGen(永久代)。
JDK8 时,方法区就是 Metaspace(元空间)
由于方法区 主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
JDK1.7演示:
package com.aop8.jvm.test;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class PermGenOomMock{
List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
public static void main(String[] args) {
URL url = null;
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.aop8.jvm.test.TestDemo");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
本例中使用的 JDK 版本是7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 java.lang.OutOfMemoryError: PermGen space 异常了。
JDK1.8演示:
/**
* 演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class main1 extends ClassLoader {//可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
main1 test = new main1();
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);
}
} finally {
System.out.println(j);
}
}
}
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.itcast.itheima.xpp.main1.main(main1.java:26)
4865
得出结论:
JDK6 、JDK7 存在 PermGen space;
JDK8 中, Hotspot 已经没有 PermGen space ,取而代之是一个叫做 Metaspace(元空间)
1.8以前会导致永久代内存溢出java.lang.OutOfMemoryError: PermGen space
1.8以后会导致元空间内存溢出java.lang.OutOfMemoryError: Metaspace
1.3.2 Metaspace(元空间)
Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
Oracle 移除PermGen(永久代)从从JDK7 就开始。例如,字符串内部池,已经在JDK7 中从永久代中移除。直到JDK8 的发布将宣告 PermGen(永久代)的终结。
其实,移除 PermGen 的工作从 JDK7 就开始,永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。
但永久代仍存在于JDK7 中,并没完全移除,比如:
- 字面量 (interned strings)转移到 Java heap;
- 类的静态变量(class statics)转移到Java heap ;
- 符号引用(Symbols) 转移到 Native heap ;
必须知道的是 JDK6 、JDK7 依然存在 PermGen space;
1.3.3 JDK6 、JDK7、JDK8 内存溢出的示例
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());
}
}
}
JDK6 的运行结果:
JDK7 的运行结果:
JDK8 的运行结果:
从运行结果可以得出:
1)、运行时常量池 :
- 在 JDK6 ,抛出永久代(PermGen space)异常,说明 运行时常量池 存在于 方法区;
- 在 JDK7、JDK8 抛出堆(Java heap space)异常,说明 运行时常量池 此时在 Java堆 中;
2)、 方法区(永久代 、元空间):
JDK8 打印ignoring option PermSize=10M; support was removed in 8.0 … 警告的原因:
- 我们都知道,JDK8时,永久代已被移除,所以不支持 -XX:PermSize=10M -XX:MaxPermSize=10M
永久代的参数设置。 - JDK8 的方法区是 元空间,其参数设置是 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N 。
- 反推证出 JDK6 、 JDK7 时,永久代 还是存在的,否则打印不支持参数设置的警告。
1.3.4 元空间与本地内存
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。默认情况下,元空间的大小仅受 本地内存 限制,但可以通过以下参数来指定元空间的大小:
- -XX:MetaspaceSize ,初始空间大小:达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
- -XX:MaxMetaspaceSize,最大空间:默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
- -XX:MaxMetaspaceFreeRatio ,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;
1.3.5 问题
通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK8 中永久代 向 元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:
1)字符串存在永久代中,容易出现性能问题和内存溢出。
2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4)Oracle 可能会将HotSpot 与 JRockit 合二为一。
1.4 常量池
1.4.1 javac+javap获取反编译文件
先看一个简单的代码,并进入其控制台内:
然后输入javap -v HelloWorld.class进行反编译。(若找不到文件则先用javac HelloWold.java编译出.class字节码文件)
类的基本信息:
常量池:
类的方法定义:
解读:在类的方法定义下的Code内便是System.out.println(“hello world”);的虚拟机指令。getstatic获取静态变量System.out;ldc创建一个变量;invokevirtual执行一次方法调用;return方法执行结束。
1.4.2 虚拟机指令和常量池
虚拟机指令执行过程,便是去常量池查找相应的常量进行执行的。
(注:这些指令,需要学习编译原理,才能看得懂,博主也要去补课了~~)
1.5 运行时常量池
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
总结
- 方法区是逻辑上的一个概念,而元空间(或永久代)是落地的实现,也就是说元空间(或永久代)就是方法区。
- 方法区在jdk1.8前的实现是永久代;jdk1.8开始则转换为元空间。
- jdk1.8前方法区(永久代)逻辑上属于堆的一部分,即其用的是堆内存;jdk1.8开始方法区(元空间)用的是本地内存。
- jdk1.6及以前运行时常量存在于方法区,jdk1.7及以后运行时常量存在于堆