一、栈、堆、方法区的交互关系
变量名存放在栈帧的局部变量表中,创建出来的对象实例数据存放在堆中,对象类型的具体数据则存放在方法区。同属对象实例中维护一个指向对象类型的指针,用于获取该对象实例的类型数据。
二、方法区
方法区在逻辑上是属于堆的一部分,但是方法区可以看作是一块独立于Java堆的内存空间
- 方法区与堆一样,是各个线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,可能会导致方法区溢出
- 关闭JVM就会释放这个区域的内存
- JDK 8及以后,方法区的落地实现从永久代改为了元空间,元空间不在虚拟机设置的内存中,而是使用本地内存
三、设置方法区内存的大小
方法区的大小不一定是固定的,JVM可以根据应用的需要动态调整
JDK 7以及之前:
-XX:PermSize
来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermSize
来设定永久代最大可分配空间。32位机器默认是64M
,64位机器模式是82M
JDK 8之后:
- 元数据区大小可以使用参
-XX:MetaspaceSize
和-XX: MaxMetaspaceSize
指定,替代上述原有的两个参数。 - 默认值依赖于平台。windows下,
-XX:MetaspaceSize
是21M
,-XX: MaxMetaspaceSize
的值是-1
,即没有限制 -XX:MetaspaceSize
设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize
值为21MB
.这就是初始的高水位线,一旦触及这个水位线,Full GC
将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC
后释放了多少元空间。如果释放的空间不多,那么在不超过MaxMetaspaceSize
时,适当提高该值。如果释放空间过多,则适当降低该值。如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC
多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize
设置为一个相对较高的值
四、方法区的内部结构
类加载子系统会加载class
字节码文件,加载的类型信息会存放到方法区。
方法区存储着已被加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存、域信息、方法信息等
类型信息
对每个加载的类型(类class
、接口interface
、枚举enum
、注解annotation
),JVM必须在方法区中储存以下类型信息:
- 这个类型的完整有效名称(全名=包名+类名)
- 这个类型直接父类的完整有效名
- 这个类型的修饰符(
publish
、abstract
、final
的某个子集) - 这个类型直接接口的一个有序列表
域信息
- JVM必须在方法区保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型
- 方法参数的数量和类型、顺序
- 方法的修饰符
- 方法的字节码、操作数栈、局部变量表及大小(
abstract
和native
方法除外) - 异常表(
abstract
和native
除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移位置、被捕获的异常类的常量池索引
non-final
的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们称为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时也可以访问
- 被声明为
final
的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配
常量池
class
字节码文件中的常量池。
一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表,常量表包括字面量和对类型、域、方法的符号引用
一个Java源文件中的类、接口、编译后产生一个字节码文件。而Java中的字节码需要数据支持, 通常这种数据会很大以至于不能直接存到字节码里,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
常量池内存储的数据类型包括:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
运行时常量池
- 运行时常量池是方法区的一部分
- 常量池表是
class
文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时运行时常量池中 - 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,会被换为真实地址,运行时常量池相对于
class
文件常量池的一个重要特征就是具备动态性 - 运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表更加详细
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,就会报内存溢出异常
五、方法区的演进
在JDK 6及以前,方法区的落地实现称为永久代,静态变量和字符串常量池都保存在永久代中
在JDK 7的时候,使用开始逐渐去掉永久代,并且将静态变量和字符串常量池移动到堆空间中保存。此时方法区还是用的虚拟机的内存。
字符串常量池保存在堆空间中,是因为永久代的回收效率很低,在Full GC
的时候才会触发永久代的垃圾回收。这就导致字符串常量池回收效率不高,而程序中会有大量字符串被创建,回收效率低导致永久代内存不足。如果保存在堆空间中,就能及时回收内存。
JDK 8及以后,方法区的落地实现改为元空间,并且使用本地内存,而不再使用虚拟机的内存。静态变量和字符串常量池还是保存在堆空间中。
元空间替换永久代是因为:
- 为永久代设置空间大小是很难确定的。因为有些场景需要动态加载很多类,永久代很容易出现内存溢出的情况,所以元空间直接使用本地内存
- 对永久代进行调优也很困难。垃圾回收判断哪些类不再被使用非常繁琐
六、方法区的垃圾回收
方法区的垃圾回收主要回收:常量池中废弃的常量和不再使用的类型
常量池中主要存放字面量和符号引用
- 字面量例如文本字符串、被声明为
final
的常量值等 - 符号引用包含类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
只要常量池中的常量没有被任何地方引用,就可以被回收,回收废弃常量与回收堆空间的对象非常相似
要判定一个类型是否不再使用的条件比较苛刻,需要同时满足三个条件:
- 该类所有的实例都已经被回收,也就是堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法