文章目录
- 1、栈、堆、方法区的交互关系
- 2、方法区的理解
- 2.1、方法区的官方描述
- 2.2、方法区的基本理解
- 2.3、JDK中方法区的变化
- 3、设置方法区大小与OOM
- 3.1、设置方法区内存的大小
- 3.2、方法区内存溢出
- 4、方法区的内部结构
- 4.1、类型信息、域信息和方法信息介绍
- 4.1.1、类型信息
- 4.1.2、域信息
- 4.1.3、方法信息
- 4.2、类变量和常量
- 4.3、常量池
- 4.4、运行时常量池
- 5、方法区使用举例
- 6、方法区的演进细节
- 6.1、HotSpot虚拟机中方法区的变化
- 6.2、永久代为什么被元空间替换
- 6.3、静态变量存放的位置
- 7、方法区的垃圾回收
点讲解运行时数据区中的方法区,将会把堆、栈、方法区三者的关系串起来,这样大家对数据在内存中的展示就会更加清晰。说完了它们的关系之后,还会讲解方法区中存储的内容、方法区的内部结构以及方法区的异常情况。最后大家要注意,方法区是随着JDK版本变更而不断变化的。
1、栈、堆、方法区的交互关系
如下图所示:
针对HotSpot虚拟机,从内存结构上看运行时数据区包含本地方法栈、程序计数器、虚拟机栈、堆和方法区。本帖子将重点讲解方法区,也就是上图中的Method Area。
上面是从内存结构的角度看方法区在运行时数据区所处的位置,下面从线程共享与否的角度来看运行时数据区的划分,如下图所示:
栈、堆、方法区三者之间的交互关系如下图所示:
从最简单的代码角度出发,当前声明的变量是Student类型的student,把整个Student类的结构加载到方法区,把变量student放到虚拟机栈中,new的对象放到Java堆中。
如图下图所示:
在虚拟机栈局部变量表中存放的是各个变量,其中reference区域就相当于上上图中的student变量,引用类型reference指向了堆空间中对象的实例数据,在堆的对象实例数据中有一个到对象类型数据的指针,这个指针指向了方法区中对象类型的数据。
2、方法区的理解
2.1、方法区的官方描述
方法区的官方描述如下图所示:
所示文档的意思是在JVM中,方法区是可供各个线程共享的运行时内存区域。方法区与传统语言中的编译代码存储区或者操作系统进程的正文段的作用非常类似,它存储了每一个类的结构信息,例如,运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集和压缩。Java 8的虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要太多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
Java虚拟机规范中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpot虚拟机而言,方法区还有一个别名叫作Non-Heap(非堆),目的就是要和堆区分开。所以,方法区可以看作是一块独立于Java堆的内存空间,如下图所示:
2.2、方法区的基本理解
对于方法区的理解我们要注意以下几个方面:
- (1)方法区(Method Area)与堆一样,是各个线程共享的内存区域。
- (2)方法区在JVM启动的时候被创建,并且它实际的物理内存空间和虚拟机堆区一样都可以是不连续的。
- (2)方法区在JVM启动的时候被创建,并且它实际的物理内存空间和虚拟机堆区一样都可以是不连续的。
- (3)方法区的大小跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类。如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误,如java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace。
以下情况都可能导致方法区发生OOM异常:加载大量的第三方jar包、Tomcat部署的工程过多(30~50个)或者大量动态地生成反射类。关闭JVM就会释放这个区域的内存。
2.3、JDK中方法区的变化
在JDK 7及以前,习惯上把方法区称为永久代。但是JDK 8移除了永久代,官方说明如下图所示:
为什么会有上面的变化呢?Java虚拟机规范对如何实现方法区,不做统一要求,例如BEA JRockit和IBM J9等虚拟机中不存在永久代的概念。JDK 7及之前的HotSpot虚拟机把垃圾收集扩展到永久代,这样HotSpot虚拟机就可以像管理堆一样管理永久代,不需要单独针对方法区写内存管理代码了。现在看来,让虚拟机管理永久代内存并不是很好的想法,因为永久代很容易让Java程序发生内存溢出(超过-XX:MaxPermSize上限)。而BEA JRockit和IBM J9虚拟机是在本地内存中实现的方法区,只要没有触碰到进程可用的内存上限就不会出问题。借鉴BEA JRockit虚拟机对于方法区的实现,HotSpot虚拟机在JDK 8也完全废弃了永久代的概念,取而代之的是在本地内存中实现的元空间(Metaspace),
JDK 7中的方法区实现:永久代:
JDK 8中的方法区实现:元空间:
分别说明了不同版本的JDK对方法区的描述,JDK 7及其之前的方法区一般称为永久代,JDK 8之后称为元空间。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于元空间不在虚拟机设置的内存中,而是在本地内存。另外,永久代、元空间二者并不只是名字变了,内部结构也调整了,稍后会做介绍。
根据Java虚拟机规范的规定,如果方法区无法满足新的内存分配需求,将抛出OOM异常。
3、设置方法区大小与OOM
3.1、设置方法区内存的大小
方法区的大小不必是固定的,JVM可以根据应用的需要动态调整,下面根据JDK版本来分别说明方法区的大小设置和注意事项。
JDK 7及以前的方法区相关设置如下:
- (1)通过-XX:PermSize参数设置永久代初始分配空间。默认值是20.75MB。
- (2)通过-XX:MaxPermSize参数设置永久代最大可分配空间。32位机器默认是64MB,64位机器模式是82MB,可以使用jinfo命令查看相关参数设置,如上图所示。当JVM加载的类信息容量超过了该值,会报异常OutOfMemoryError:PermGen space。
JDK8及以后方法区相关设置如下:
元空间大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代JDK7中的永久代的初始值和最大值。默认值依赖于具体的系统平台,取值范围是12~20MB。例如在Windows平台下,-XX:MetaspaceSize默认大约是20MB,如果-XX:MaxMetaspaceSize的值是-1,表示没有空间限制。与永久代不同,如果不指定大小,在默认情况下,虚拟机会耗尽所有的可用系统内存。如果元空间发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
假设-XX:MetaspaceSize默认值为20MB,这是初始的高水位线,一旦方法区内存使用触及这个水位线,Full GC将会被触发并卸载没用的类(包括这些类对应的类加载器也不再存活)。垃圾收集后,高水位标记可能会根据类元数据释放的空间量自动提高或降低,如果释放的空间很少,那么在不超过MaxMetaspaceSize时,该值会被提高,以免过早引发下一次垃圾收集。如果释放空间过多,那么该值会被降低。如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
下面我们用如下代码测试JDK 8中方法区内存的设置:
/*** @title MethodAreaDemo* @description 测试设计方法区大小* JDK7以前: -XX:PermSize=100m -XX:MaxPermSize=100m* JDK8及以后:-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m* @author: yangyongbing* @date: 2024/3/5 11:34*/
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...");}
}
如上述代码所示,把元空间设置为100MB,设置参数“-XX:MetaspaceSize=100m -XX:Max MetaspaceSize=100m”,查看元空间大小的设置如下图所示:
显示为104857600,单位是byte,换算为MB正好是100MB。
在JDK 8及以上版本中,设定MaxPermSize参数,JVM在启动时并不会报错,但是会提示如下信息:
Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m;
support was removed in 8.0
3.2、方法区内存溢出
当方法区发生内存溢出的时候,我们应该怎么去解决呢?下面举例展示如何解决方法区内存溢出,将JDK 7版本的永久代大小设置为5MB,将JDK 8版本的方法区空间大小设置为10MB,分别查看加载多少类的时候发生内存溢出。如代码清单如下所示:
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;/*** @title OOMTest* @description 方法区内存溢出* JDK6/7中:-XX:PermSize=5m -XX:MaxPermSize=10m* JDK8中:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m* @author: yangyongbing* @date: 2024/3/5 11:52*/
public class OOMTest extends ClassLoader{public static void main(String[] args) {int j=0;try {OOMTest oomTest = new OOMTest();for (int i = 0; i < 10000; i++) {// 创建ClassWriter对象,用于生成类的二进制字节码ClassWriter classWriter = new ClassWriter(0);// 指明版本号、修饰符、类名、包名、父类、接口classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);// 返回byte[]byte[] code = classWriter.toByteArray();// 类的加载,Class对象oomTest.defineClass("Class"+i,code,0,code.length);j++;}} catch (Exception e) {e.printStackTrace();}finally {System.out.println(j);}}
}
在JDK 8中把元空间设置为10M,创建了8531个对象,抛出异常“java.lang.OutOfMemoryError:Metaspace”,如下图所示:
在JDK 7中把永久代设置为5M,创建了1315个对象,抛出异常“java.lang.OutOfMemoryError:PermGen space”,如下图所示:
4、方法区的内部结构
方法区内部结构如下图所示:
Java源代码编译之后生成class文件,经过类加载器把class文件中的内容加载到JVM运行时数据区。class文件中的一部分信息加载到方法区,比如类class、接口interface、枚举enum、注解annotation以及运行时常量池等类型信息。
上面我们从类加载到运行时数据区的角度说明了方法区什么时候放入数据,下面我们比较详细地说明方法区中存放什么样的数据。方法区和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等,如下图所示:
接下来对方法区中存储的内容信息分别详细说明。
4.1、类型信息、域信息和方法信息介绍
4.1.1、类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:
- 完整有效全类名,包括包名和类名。
- 直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)。
- 修饰符(public、abstract、final的某个子集)。
- 直接接口的一个有序列表。
4.1.2、域信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)。
4.1.3、方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称。
- 方法的返回类型(或void)。
- 方法参数的数量和类型(按顺序)。
- 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的一个子集)。
- 方法的字节码(bytecodes)、操作数栈深度、局部变量表大小(abstract和native方法除外)。
- 异常表(abstract和native方法除外),异常表会记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
通过下面的代码测试方法区的内部构成:
上述代码的class文件经反编译(使用命令javap –v –p MethodInnerStrucTest.class)之后的类型信息如下所示:
public class com.young.MethodInnerStrucTest extends java.lang.Object
implements java.lang.Comparable<java.lang.String>,java.io.Serializable
可以看到完整有效全类名为com.young.MethodInnerStrucTest;父类为java.lang.Object;实现的接口为java.lang.Comparable<java.lang.String>;修饰符为public。
上述代码的class文件经反编译之后的域信息如下所示:
其中包含两个域信息,分别是num和str。首先分析num的各项信息,num为域名称、I表示域类型为Integer、ACC_PUBLIC表示域修饰符为public。接着分析str的各项信息,str为域名称、Ljava/lang/String表示域类型为String、ACC_PRIVATE和ACC_STATIC表示域修饰符为private static。
上述代码的class文件经反编译之后的test1()方法信息如下所示:
可以看到方法名称是test1()😭)V表示返回值是void;该方法没有参数,所以没有参数名称和类型;ACC_PUBLIC表示方法修饰符是public;Code后面的字节码包括方法的字节码指令、操作数栈深度为3、局部变量表大小为2。
除了test1()方法外,大家可以看到反编译文件中还有一个方法叫作MethodInnerStruc Test(),我们知道Java中如果不手动定义构造方法的话,Java默认会提供一个无参的构造方法,在class文件反编译之后,可以看到无参构造方法信息如下所示:
最后,我们看到test2()方法的信息中还存在一个异常表,如下所示,其中Exception table表示异常表。
4.2、类变量和常量
static修饰的成员变量为类变量或者静态变量,静态变量和类关联在一起,随着类的加载而加载。类变量被类的所有实例共享,即使没有类实例时也可以访问它。
在JDK 7之前类变量也是方法区的一部分,JDK 7及以后的JDK类变量放在了堆空间。此外,使用final修饰的成员变量表示常量,使用static final修饰的成员变量称为静态常量,静态常量和静态变量的区别是静态常量在编译期就已经为其赋值。
在JDK 7之前类变量也是方法区的一部分,JDK 7及以后的JDK类变量放在了堆空间。此外,使用final修饰的成员变量表示常量,使用static final修饰的成员变量称为静态常量,静态常量和静态变量的区别是静态常量在编译期就已经为其赋值。
下面用案例说明静态常量和静态变量的区别,如下代码清单所示:
运行结果如下图所示:
可以看到当order对象设置为null的时候,调用类变量count时并没有报空指针异常,这是因为类变量被类的所有实例共享,即使没有类实例时也可以访问它,但是在工作中,一般不会写这样的代码,都是直接使用类名来调用。
再说一下类变量和静态常量的区别。首先将上面代码使用javap命令反编译,结果如下图所示:
可以发现被声明为static的类变量count在编译时期并没有做赋值处理,而对于声明为“static final”的常量处理方法则不同,每个全局常量在编译的时候就会被赋值。
4.3、常量池
方法区内部包含了运行时常量池。class文件中有个constant pool,翻译过来就是常量池。当class文件被加载到内存中之后,方法区中会存放class文件的constant pool相关信息,这时候就成为了运行时常量池。所以要弄清楚方法区的运行时常量池,需要理解class文件中的常量池。一个Java应用程序中所包含的所有Java类的常量池组成了JVM中的大的运行时常量池。常量池在class文件中的相关结构如下图所示:
图中画框的地方有两个元素,分别是constant_pool_count和constant_pool[constant_pool_count-1],它们分别表示常量池容量和所有的常量。
常量池内存储的数据类型包括数量值、字符串值、类引用、字段引用以及方法引用。下面我们使用代码说明常量池中的内容,如代码清单如下所示:
通过“javap –v DynamicLinkingTest.class”命令查看class文件,如代下所示:
可以看到在class文件中包含了名为Constant Pool的属性,该属性表示class文件中的常量池,Methodref表示方法的符号引用,Fieldref表示字段的符号引用。
可以通过jclasslib工具查看常量池中的内容,如下图所示:
如下图所示:
DynamicLinkingTest.java的文件大小为265字节,但是里面却使用了String、System、PrintStream及Object等多种类结构。如果使用常量池存储这些结构的符号引用和常量,在Java文件中直接调用这些引用和常量即可,这样便可以节省很多空间。如果没有常量池这样的设计,就需要手动在Java代码中体现这些完整的类结构,这样就会导致Java文件占用空间变大。企业开发中,随着Java文件的增多和代码量的增加,就会导致Java文件非常庞大,冗余度过高。综上,常量池的作用就是提供一些符号和常量,便于指令的识别。
可以把常量池看作一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。在这里大家对字节码中常量池存放的数据有了大概的认识。
4.4、运行时常量池
上面我们讲了class文件中的常量池,接下来我们再讲一下什么是运行时常量池。运行时常量池(Runtime Constant Pool)是方法区的一部分。常量池表是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
虚拟机加载类或接口后,就会创建对应的运行时常量池。JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OOM异常。
5、方法区使用举例
上面我们讲了方法区比较经典的内部存储结构,包括类型信息、常量、静态变量、即时编译器编译后的代码缓存等。下面我们从代码的角度深度剖析方法区的使用过程,如代码清单如下所示:
上面的代码很简单,使用javap命令反编译代码,常量池中的内容如下所示:
class文件反编译之后方法区中main()方法的信息如下:
根据反编译的结果我们可以看到局部变量表大小是2,这是在编译期已经确定好的,程序中有2个变量,分别是args和x,所以局部变量表的大小是2。还可以看到操作数栈的深度是2,剩下的就是字节码指令了。
我们主要查看方法区中字节码指令如何与程序计数器以及虚拟机栈之间协同合作。字节码指令前面的序号表示程序计数器中指令编号,字节码指令表示当前指令的具体操作。接下来我们针对执行过程画图讲解,具体流程如下:
- (1)首先执行“ldc #2”指令,指令序号为0,程序计数器指令编号是0,该指令表示的含义是把常量池中编号为2的符号引用放入操作数栈,常量池中编号为2的符号引用又指向了编号为23的符号引用,继续查找常量池便可看到23号符号引用是“shangguigu”,所以我们把“shangguigu”放入操作数栈,如下图所示:
- (2)接下来执行“astore_1”指令,指令序号为2,程序计数器指令编号是2,该指令表示的含义是把操作数栈顶中的元素放入局部变量表中序号为1的位置,如下图所示:
- (3)“getstatic #3”指令的序号为3,程序计数器指令编号是3,该指令表示的含义是将常量池中编号为#24、#25的符号引用放入操作数栈,24号和25号的符号引用又指向30号、31号和32号,分别对应java/lang/System、out和Ljava/io/PrintStream,也就是说把System类中的静态常量out放入操作数栈,如下图所示:
- (4)“aload_1”指令的序号为6,程序计数器指令编号是6,该指令表示的含义是把局部变量表中序号为1的数据放入操作数栈,如下图所示:
- (5)“invokevirtual #4”指令的序号为7,程序计数器指令编号为7,该指令调用常量池中编号为4指向的方法引用,查看常量池内容可知该方法是PrintStream.println(),将操作数栈中的两个元素弹出,作为println()方法的参数传入println()方法的操作数栈中,如下图所示:
- (6)最后调用return指令,该指令的含义是main()方法调用结束返回void,如下图所示:
我们通过简单的案例说明方法区中字节码指令与常量池、程序计数器以及虚拟机栈之间的协作关系流程图。随着字节码指令的执行,程序计数器中存储的指令会发生变化;虚拟机栈中的操作数栈和局部变量表也会根据字节码指令而发生变化,这就是内存区域之间的协作关系。
6、方法区的演进细节
6.1、HotSpot虚拟机中方法区的变化
以JDK 7为例,前面讲过只有HotSpot虚拟机才有永久代的概念。对于BEA JRockit、IBM J9等虚拟机来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现的细节,不受Java虚拟机规范管束,并不要求统一。下面看一下HotSpot虚拟机中方法区的变化,如下表所示:
JDK 6中方法区的内容如下图所示:
JDK 7中方法区的内容如下图所示:
可以发现相对JDK 6来说,字符串常量池(StringTable)位置发生了变化。为什么要对字符串常量池的位置进行调整呢?因为永久代的回收效率很低,在Full GC的时候才会触发,而Full GC是老年代的空间不足、永久代不足时才会触发。这就导致字符串常量池回收效率不高。我们程序中一定会有大量的字符串被创建,而很多字符串往往不需要永久保存,那么回收效率低的话,就会导致永久代内存严重不足。如果将字符串放到堆里,内存就能及时回收利用。
字符串常量池调整的官方声明如下图所示:
上图所示的声明大致意思为:在JDK 7中,字符串常量不再在Java堆的永久代中分配,而是和应用程序创建的其他对象一样,在Java堆的主要部分(称为新生代和老年代)中分配。这一更改将导致更多数据驻留在主Java堆中,而在永久生成中数据更少,因此可能需要调整堆大小。由于这一更改,大多数应用程序在堆使用方面只会看到相对较小的差异,但加载更多的类或大量使用String.intern()方法的大型应用程序将看到更显著的差异。
JDK 8中方法区的内容如下图所示:
这个时候方法区的实现元空间不再占用JVM内存,而是把元空间放到了本地内存。
6.2、永久代为什么被元空间替换
官方解释元空间替换永久代的原因如下图所示:
这段话的意思是,元空间替换永久代这部分内容是JRockit虚拟机和HotSpot虚拟机融合的一部分,我们知道JRockit不需要配置永久代,HotSpot虚拟机也在慢慢地去永久代。
JDK 7之前的版本中,HotSpot虚拟机将类型信息、内部字符串和类静态变量存储在永久代中,垃圾收集器也会对该区域进行垃圾回收。JDK 7将HotSpot虚拟机中永久代内部字符串和类静态变量数据移动到Java堆中,但是依然存在永久代。
随着Java 8的到来,HotSpot虚拟机中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫作元空间,元数据信息内存的分配将受本机可用内存量的限制,而不是由“-XX:MaxPermSize”的值固定。由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。这项改动是很有必要的,原因有以下两点:
- (1)为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生永久代的OOM。比如某个集成了很多框架的Web工程中,因为功能繁多,在运行过程中要不断动态加载很多类,可能出现如下致命错误:
Exception in thread"main"java.lang.OutOfMemoryError:PermGen space
- (2)将元数据从永久代剥离出来放到元空间中,不仅实现了对元数据的无缝管理,而且因为元空间大小仅受本地内存限制,也简化了Full GC,并且可以在GC不暂停的情况下并发地释放元数据。
6.3、静态变量存放的位置
我们讲了JDK 7及以后的版本中静态变量存放位置的改变,从方法区存储改为堆内存存储,是我们学习JVM过程中的一个结论,下面我们用代码去验证上面的结论。如代码清单如下所示:
在JDK 7中创建100M字节数组存放到老年代,如下图所示:
在JDK 8中创建100M字节数组存放到老年代,如下图所示:
静态引用对应的对象实体始终都存放在堆空间,所以JDK 7和JDK 8中创建的字节数组都是存放在堆空间,JDK 7及之后的版本创建对象的引用名(即定义的arr)也存放在堆空间中,如下代码清单所示:
在JDK 9中,新增了一款工具叫jhsdb,使用jhsdb工具来连接到Java进程或启动事后调试器来分析JVM的核心转储内容。这是JDK 9的新特性,其实在JDK 9之前,JAVA_HOME/lib目录下有个sa-jdi.jar,可以通过如下命令启动JHSDB(图形界面)及CLHSDB(命令行形式)。简而言之,jhsdb工具就是对sa-jdi.jar进行了一层封装。
D:\Program Files\Java\jdk1.7.0_80\lib>java -cp sa-jdi.jar sun.jvm.hotspot.HSDBD:\Program Files\Java\jdk1.7.0_80\lib>java -cp sa-jdi.jar sun.jvm.hotspot.CLHSDB
sa-jdi.jar中sa的全称为Serviceability Agent,它之前是Sun公司提供的一个用于协助调试HotSpot的组件,HSDB便是使用Serviceability Agent来实现的。
从Java虚拟机规范所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,Java虚拟机规范并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中。
7、方法区的垃圾回收
有些人认为方法区是没有垃圾收集行为的,其实不然。一般来说这个区域的回收效果比较难令人满意,尤其是类的卸载,条件相当苛刻。但是这部分区域的回收又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型信息。
先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名。
- 字段的名称和描述符。
- 方法的名称和描述符。
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。判定一个常量是否“废弃”还是相对简单的,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用“-verbose:class”“-XX:+TraceClassLoading”以及“-XX:+TraceClassUnLoading”查看类加载和卸载信息。
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要JVM具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。