方法区

  • 根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。

由此先来看看方法区的作用。

1、方法区概述

  • 尽管方法区在逻辑上属于堆的一部分,但是实际上其被看作是独立于Java堆的内存空间,它有个别名叫非堆(Non-Heap),目的是要和堆分开。
  • 与Java堆一样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区
    溢出,虚拟机同样会抛出内存溢出错误:java.lang. OutofMemoryerror:PermGen space或者 java. lang. OutofMemoryError: Metaspace。(加载大量的第三方Jar包;Tomcat部署的工程过多(30~50个);大量动态的生成反射类)
  • 关闭JVM就会释放这个区域的内存。
  • 方法区是Java虚拟机规范的一个概念,而永久代(JDK7及以前)和元空间(JDK8及以后)是不同JVM对方法区的具体实现。
  • 元空间的本质和永久代类似,都是JVM规范中方法区的实现。不过两者最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。

2、设置方法区内存大小

  • 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

    • jdk7及以前:

      • 通过-XX::PermSize来设置永久代初始分配空间,默认值是20.75M。

      • 通过-XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是

        82M,当JVM加载的类信息容量超过了这个值,会报异常 OutofMemoryError: PermGen space。

    • jdk8及以后:

      • 元数据区大小可以使用参数-XX: MetaspaceSize和-XX: MaxMetaspaceSize指定,替代上述原有的两个参数。
      • 默认值依赖于平台。windows下,-XX: Metaspacesize是21M,XX: MaxMetaspaceSize的值是-1,即没有限制。
      • 对于一个64位的服务器端JVM来说,其默认的-XX: Metaspace Size值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
      • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回
        收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX: MetaspaceSize设置为一个相对较高的值。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}

设置JVM参数: jdk6/7中: -XX:PermSize=10m ,-XX:MaxPermSize=10m;jdk8中: -XX:MetaspaceSize=10m ,-XX:MaxMetaspaceSize=10m。

因为方法区设置的容量无法承载这么多类信息,所以打印结果如下:

1
2
3
4
5
6
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
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.company.java05.OOMTest.main(OOMTest.java:25)
3331

3、方法区的内部结构

  • 方法区用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。
  • 类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
    • 这个类型的完整有效名称(全名=包名.类名)。
    • 这个类型直接父类的完整有效名(对于 interface或是java.lang. object,都没有父类)。
    • 这个类型的修饰符( public, abstract,final的某个子集)。
    • 这个类型直接接口的一个有序列表。
  • 域信息:
    • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
    • 域的相关信息包括:域名称、域类型、域修饰符( public, private,protected, static, final, volatile, transient的某个子集)。
  • 方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序。
    • 方法名称。
    • 方法的返回类型(或void)。
    • 方法参数的数量和类型(按顺序)。
    • 方法的修饰符(pub1ic, private, protected, static,final,synchronized, native, abstract的一个子集。
    • 方法的字节码( bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外)。
    • 异常表( abstract和 native方法除外)。
      • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

3.1 运行时常量池:Runtime Constant Pool

  • 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
  • 运行时常量池是方法区的一部分。
  • 常量池表( Constant Pool Table)是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  • 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  • JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所霱的内存空间超过了方法区所能提供的最大值,则JVM会抛 OutofMemoryError异常。

例子:

1
2
3
4
5
6
7
8
9
public class MethodAreaDemo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}

对应的字节码指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 0 sipush 500
3 istore_1
4 bipush 100
6 istore_2
7 iload_1
8 iload_2
9 idiv
10 istore_3
11 bipush 50
13 istore 4
15 getstatic #2 <java/lang/System.out>
18 iload_3
19 iload 4
21 iadd
22 invokevirtual #3 <java/io/PrintStream.println>
25 return

4、方法区的演进

  • HotSpot中方法区的变化(只有HotSpot才有永久代):

    • jdk6及之前:有永久代(permanent generation),静态变量存放在永久代上。
    • jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中。
    • jdk8及以后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆。
  • 永久代为什么会被元空间替换?http://openjdk.java.net/jeps/122官方文档)

    • 因为永久代设置空间大小是很难确定的。
      • 在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
      • 而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
    • 对永久代进行调优是很困难的。
  • 字符串常量池为什么要调整?

    • jdk7中将 StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致 StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
  • 静态变量放在哪?

    • 由下面例子可得出结论:
      • staticObj变量本身在堆空间中, instanceObj随着Test的对象实例存放在Java堆,localObj则是存放在foo()方法栈帧的局部变量表中。
      • 测试发现:三个对象的数据在内存中的地址都落在Eden区范围内,所以结论:只要
        是对象实例必然会在Java堆中分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StaticObjTest {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();

void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done");
}
}

private static class ObjectHolder {
}

public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}

5、方法区的垃圾回收

  • 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

  • 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。