一文理解JVM的方法区
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文理解JVM的方法区相关的知识,希望对你有一定的参考价值。
5.1 栈、堆、方法区的交互关系
当我们写出如下的代码:
f()
Person person=new Person();
这里Person是我们生成的字节码,也就是.class文件,此时JVM到底是怎么存的呢?
方法区主要存放的是 Class,而堆中主要存放的是实例化的对象,而栈里存的是堆中对象的地址,针对上面的例子,具体的存储策略是:
-
Person 类的 .class 信息存放在方法区中
-
真正创建的 person 对象存放在 Java 堆中
-
person 变量存放在 Java 栈的局部变量表中,就是常见的reference字段。其实就是堆中person对象的地址。
-
在 person 对象中,有个指针指向方法区中的 Person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的。
首先,方法区在哪里呢?对于不同的虚拟机是略有不同,对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区可以看作是一块独立于Java堆的内存空间。
方法区(Method Area)是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等待该线程加载完毕,然后直接使用该类,即类只能加载一次。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。而且方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
。常见的问题有如下几种情况:加载大量的第三方的jar包、Tomcat部署的工程过多(30~50个)或者大量动态的生成反射类。
执行一个程序需要加载的类数量可能远超我们的想象,例如下面这个简单的类:
public class MethodAreaDemo
public static void main(String[] args)
System.out.println("start...");
try
Thread.sleep(1000000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("end...");
执行时,通过jvisualvm观察,可以发现其加载了1600多个类。
方法区与永久代、元空间的关系
在 JDK7 及以前习惯上把方法区称为永久代,对于Hotspot来说,可以将方法区和永久代看作等价的。 到了JDK8就完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
永久代、元空间二者并不只是名字变了,内部结构也调整了。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。
6.2 方法区的内部结构
方法区存储什么呢?内部结构又是怎么样的呢?《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
本部分的内容,我们不需要全记住,只要理解清楚,分析的时候知道到哪里找就可以了。
6.2.1 类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
-
这个类型的完整有效名称(全名=包名.类名)
-
这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
-
这个类型的修饰符(public,abstract,final的某个子集)
-
这个类型直接接口的一个有序列表
6.2.2 域(Field)信息
也就是我们常说的成员变量,域信息是比较官方的称呼
-
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
-
域的相关信息包括:域名称,域类型,域修饰符例如public,private,protected,static,final,volatile,transient等等。
6.2.3 方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
-
方法名称
-
方法的返回类型(包括 void 返回类型),void 在 Java 中对应的为 void.class
-
方法参数的数量和类型(按顺序)
-
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
-
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
-
异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
举例
public class MethodStructTest extends Object implements Comparable<String>,Serializable
//属性
public int num = 10;
public String info = "info";
private static String str = "内部结构";
public void test1()
int count = 20;
System.out.println("count = " + count);
public static int test2(int cal)
int result = 0;
try
int value = 30;
result = value / cal;
catch (Exception e)
e.printStackTrace();
return result;
@Override
public int compareTo(String o)
return 0;
然后使用如下的命令:
javap -v MethodStrucTest.class
此时可以看到生成一段特别长的代码,我们将其中的几个重要部分截取出来看看。
1.类型信息
在运行时方法区中,类信息中记录了哪个加载器加载了该类,同时类加载器也记录了它加载了哪些类
public class ch3_JMM.topic3_Dump.MethodStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
2.域信息(成员的信息)
我们在上面定义了两个成员变量num和info,其相关信息是:
public int num;
descriptor: I
flags: ACC_PUBLIC
public java.lang.String info;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
这里的descriptor: I 表示字段类型为 Integer,而info对应的就是java.lang下的String类型。
这里的flags: ACC_PUBLIC表示类型是public的,ACC_PRIVATE表示是private的,ACC_STATIC表示是静态的
//域信息
public int num;
descriptor: I
flags: ACC_PUBLIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
3.方法信息
-
descriptor: ()V 表示方法返回值类型为 void
-
flags: ACC_PUBLIC 表示方法权限修饰符为 public
-
stack=3 表示操作数栈深度为 3
-
locals=2 表示局部变量个数为 2 个(实力方法包含 this)
-
test1() 方法虽然没有参数,但是其 args_size=1 ,这是因为将 this 作为了参数
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: bipush 20
2: istore_1
3: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #6 // class java/lang/StringBuilder
9: dup
10: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
13: ldc #8 // String count =
15: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: iload_1
19: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 13: 0
line 14: 3
line 15: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lch3_JMM/topic3_Dump/MethodStructTest;
3 26 1 count I
6.2.4 non-final 类型的类变量
-
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
-
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
举例
-
如下代码所示,即使我们把order设置为null,也不会出现空指针异常
-
这更加表明了 static 类型的字段和方法随着类的加载而加载,并不属于特定的类实例
public class MethodAreaTest
public static void main(String[] args)
Order order = null;
order.hello();
System.out.println(order.count);
class Order
public static int count = 1;
public static final int number = 2;
public static void hello()
System.out.println("hello!");
6.2.5 全局常量:static final
-
全局常量就是使用 static final 进行修饰
-
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
查看上面代码,这部分的字节码指令
class Order
public static int count = 1;
public static final int number = 2;
...
执行javap -v Order.class可以看到与上述两个变量有关的信息如下:
public static int count;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int number;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2
可以发现 staitc和final同时修饰的number 的值在编译上的时候已经写死在字节码文件中了。
6.2.6 运行时常量池
在我们上面使用javap查看class文件内容时总有这么长一段的内容:
Constant pool:
#1 = Methodref #13.#31 // java/lang/Object."<init>":()V
#2 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #34 // start...
·····
#55 = Utf8 (J)V
#56 = Utf8 printStackTrace
这就是字节码的常量池,而将其加载到内存的过程就是运行时常量池。为什么需要常量池呢?一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,因此可以将其存到常量池。这个字节码包含了指向常量池的地址,在动态链接的时候再将其链接到一起,比如如下的代码:
public class SimpleClass
public void sayHello()
System.out.println("hello");
虽然上述代码只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。再比如我们这个文件中有6个地方用到了"hello"这个字符串,如果不用常量池,就需要在6个地方全写一遍,造成臃肿。我们可以将"hello"等所需用到的结构信息记录在常量池中,并通过引用的方式,来加载、调用所需的结构。如果代码多的话,引用的结构将会更多,这里就体现出常量池的好处了。
常量池可以看做是一张表,常量池内部的内容比较多,例如数量值、字符串值、类引用、字段引用、方法引用等等,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
而运行时常量池(Runtime Constant Pool)是方法区的一部分。JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
以上是关于一文理解JVM的方法区的主要内容,如果未能解决你的问题,请参考以下文章