上述的文章我们了解了除了方法区外的运行时数据区,本文我们着重来讲解方法区。
方法区
- 系列文章目录
- 一、方法区的含义
- 二、栈、方法区、堆之间的联系
- 三、方法区详情
- 1.方法区的演进
- 1.1、永久代(PermGen)
- 1.2、元空间(metaSpace)
- 2.方法区大小参数设置
- 3.方法区的内部结构
- 1.类型信息
- 2.域信息
- 3.方法信息
- 4、运行时常量池
- 总结
一、方法区的含义
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
说到方法区的实现,对于JDK8之前的版本,我们都把他称为永久代,或者将两者混为一谈,其实两者并不是一个概念,使用永久代来实现方法区,可以像java堆一样去管理方法区的内存,而它会更容易导致内存溢出的问题(永久代有上限,参数:-XX:MaxPermSize,即使不设置也会有默认大小),到了JDK7,尝试将字符串常量池、静态变量移出来,而在JDK8之后的版本,就完全舍弃了永久代,改用元空间来实现
二、栈、方法区、堆之间的联系对于这三者之间的联系,它们的详细内容这里就不做过多的阐述了,我们用一行代码来演示:
A a=new A();
等号左边的A为类型信息,存储于方法区中,a为变量,存放于局部变量表中,也就是存于栈中,等号右边的new A(),也就是对象的创建,就是创建在堆中,这就是它们三者之间的联系。
对于方法区的详情,我们可以参考虚拟机官方文档
1.方法区的演进上文也描述了随着1JDK版本的演进,方法区的结构也在不断演进。而对于永久代和元空间的描述我们还不是很清楚,所以下文来详细讲解元空间和永久代。
1.1、永久代(PermGen)我相信许多开发者们都见过java.lang.OutOfMemoryError: PermGen space这个异常,这个异常里面的PermGen指的就是方法区,而对于方法区的实现,永久代不能和方法区一概而论,前者是方法区的实现,是具体的,而方法区是JVM的虚拟机规范,是一种抽象的。我们讨论永久代的时候,都是在讨论Hotspot虚拟机的永久代,因为像 BEA的JRockit和IBM的J9虚拟机并没有永久代,所以本文的永久代都是针对Hotspot虚拟机的,我们都知道了方法区是用来存放类型信息,接口信息等等,所以我们可以用代码来实现永久代的OOM,代码如下:
package bio.optimization;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class TestPermGen {
public static void main(String[] args) {
URL url = null ;
List classLoaderList = new ArrayList();
try {
url = new File( "/tmp" ).toURI().toURL();
URL[] urls = {url};
while ( true ){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass( "bio.optimization.Point" );
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里的Point类就是一个普通的类,上述我们用到的JDK的版本是JDK1.7,永久代的默认大小8m,所以在不断加载类型信息的时候会发生永久代的OOM,你们演示的时候,一定得注意JDK的版本,引文JDK1.8之后就没有永久代这个概念了,下面我们来讲述永久代和元空间的区别。
1.2、元空间(metaSpace)在JDK7时,就把一部分的类型信息等部分数据存储到本地内存中,而到了JDK8之后,就完全用元空间取代了永久代,下面我们用代码来演示:
package bio.optimization;
import java.util.ArrayList;
import java.util.List;
public class TestmetaSpace {
static String base = "string" ;
public static void main(String[] args) {
List list = new ArrayList();
for ( int i= 0 ;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
用不同版本的JDK演示,JDK1.6的结果:
JDK1.6的时候,还是用永久代来实现,字符串等都存放在这里,到了1.7后:
此时的字符串常量都转移到堆区中,所以此时报了堆内存溢出,到了1.8之后:
我们使用参数调整之后,发现调整永久代的参数并不生效了,而元空间使用的是本地内存。
在JDK8之前:
-XX:PermSize:永久代初始大小 -XX:MaxPermSize:永久代最大容量
JDK8及以后:
-XX:metaSpaceSize:元空间初始大小 -XX:MaxmetaSpaceSize:元空间最大容量3.方法区的内部结构 1.类型信息
对于要加载的类(Class)、接口(interface)、枚举、注解等等,JVM都必须将他们的类型信息存储到方法区内:
- 这个类型的完整的有效名称
- 这个类型的直接父类的完整的有效名称
- 这个类型的直接接口的序列集
- 这个类型的修饰符
对于类的所有域信息,也会存储到方法区内:
- 域名称
- 域类型
- 域修饰符
方法的相关信息:
- 方法名
- 方法返回类型
- 方法的参数的数量,类型
- 方法的字节码
- 方法的修饰符
- 方法的异常表
对于上述的类型信息,我们可以用代码生成的字节码来分析:
package bio.methodArea;
public class TestMethodArea {
public int a1;
public String a2;
private int b1;
private String b2;
public void foo(){
//do something
}
private void foo2(){
//do something
}
public int foo3(){
return 0;
}
public static void main(String[] args) {
}
}
// class version 55.0 (55)
// access flags 0x21
public class bio/methodArea/TestMethodArea {
// compiled from: TestMethodArea.java
// access flags 0x1
public I a1
// access flags 0x1
public Ljava/lang/String; a2
// access flags 0x2
private I b1
// access flags 0x2
private Ljava/lang/String; b2
// access flags 0x1
public ()V
L0
LINENUMBER 10 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
RETURN
L1
LOCALVARIABLE this Lbio/methodArea/TestMethodArea; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public foo()V
L0
LINENUMBER 17 L0
RETURN
L1
LOCALVARIABLE this Lbio/methodArea/TestMethodArea; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x2
private foo2()V
L0
LINENUMBER 20 L0
RETURN
L1
LOCALVARIABLE this Lbio/methodArea/TestMethodArea; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x1
public foo3()I
L0
LINENUMBER 22 L0
ICONST_0
IRETURN
L1
LOCALVARIABLE this Lbio/methodArea/TestMethodArea; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 27 L0
RETURN
L1
LOCALVARIABLE args [Ljava/lang/String; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
}
我们就可以很清楚的知道相关的类型信息。
4、运行时常量池他是方法区的一部分,除了上述的类型信息,接口信息相关的信息外,还有一项信息就是运行时常量池,用于存放运行时产生的字面量和符号引用,详细的运行时常量池信息我们可以参考字节码文件来分析。
Java虚拟机对于Class文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
这里详细讲解了方法区的相关知识,更详细的可以参考相关资料。



