方法区概述
栈、堆、方法区交互
方法区在哪里
尽管所有的方法区在逻辑上属于堆,但是一些简单的实现可能不会选择去进行垃圾回收或者压缩,但是对于HotspotJVM来说,方法区还有一个别名叫做非堆,目的就是与堆分开。
所以,方法区可以看做是一个独立于Java堆的内存空间。
方法区基本理解
- 与堆一样,是线程共享的内存区域
- 在JVM启动时被创建,并且实际的物理内存和Java堆一样可以是不连续的
- 可以选择固定或扩展
- 方法区大小决定了系统可以保存多少个类,如果系统定义了过多的类,导致方法区溢出,虚拟机会抛出java.lang.OutOfMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace,比如加载大量的第三方jar包,大量创建动态代理类
方法区演进
在jdk7以前,称为永久代,jdk8以后,称为元空间。
本质上,方法区和永久代并不等价,仅是对hotspot而言的,Java虚拟机规范中,对如何实现方法区,没有统一规范,有的虚拟机不存在永久代的概念。
使用永久代,不是一个好的设计,导致java程序更容易OOM。
到了jdk8版本,改用在本地内存实现元空间来替代永久代。
元空间与永久代类似,都是对JVM规范中方法区的实现,但是元空间与永久代的最大区别是:元空间不在虚拟机设置的内存中,而是使用本地内存。
二者内存结构也变了。
方法区进阶
设置内存
方法区的大小不是固定的,可以调整。
jdk7及以前:
-XX:PermSize设置初始分配空间,默认值是20.75M。
-XX:MaxPermSize设置最大分配空间,32位机器默认64M,64位机器默认82M。
超过了最大值,抛出java.lang.OutOfMemoryError:PermGen space错误。
jinfo -flag PermSize pid
jinfo -flag MaxPermSize pid
jdk8开始:
可以使用-XX:MetaspaceSize设置元空间初始大小,windows下默认21M。
使用-XX:MaxMetaspaceSize设置元空间最大大小,默认-1。
与永久代不同,如果不指定最大值,元空间会耗尽所有的可用系统内存。
一旦溢出,就会抛出java.lang.OutOfMemoryError:Metaspace错误。
建议将-XX:MetaspaceSize设置的大一些,因为这个空间内存不足,就会触发Full GC回收。
CGLib代理类导致方法区溢出
OOM解决
一般通过内存分析工具对dump出来的堆快照进行分析,确认内存中的对象是否是必要的,也就是先分清楚到底是内存泄漏还是真的内存不够用。
如果是内存泄漏,可以进一步通过工具查看泄漏对象到GC Roots的引用链,可以找到泄漏对象的通过怎么样的路径与GC Roots相关联并导致无法回收,这样就可以比较准确的定位泄漏代码的位置。
如果不存在内存泄漏,而是内存不够用,就需要适当调整堆参数,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
方法区存储什么
存储类型信息、常量、静态变量、即时编译器代码缓存等。
类型信息
对每个加载的类型,JVM必须在方法区存储以下类型信息:
- 类型的完整名称
- 类型的直接父类的完整名称
- 类型的修饰符
- 类型直接接口的一个有序列表
non-final的类变量:静态变量和类关联在一起,随着类的加载而加载,成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例也可以访问。
Field信息
JVM必须在方法区保存类型的所有Field的相关信息以及域的声明顺序。
Field相关信息:名称、类型、修饰符。
方法信息
JVM必须保存所有方法的以下信息,和Field一样包括声明顺序:
- 方法名称
- 返回类型
- 参数数量和类型
- 修饰符
- 字节码、操作数栈、局部变量表及大小
- 异常表,每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
运行时常量池
全局常量
被声明为final的类变量处理方式不同,每个全局变量在编译时就被分配了。
运行时常量池 vs 常量池
- 方法区内部包含运行时常量池
- 字节码文件包含常量池
为什么需要常量池
一个源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存储在字节码里面,而是换一种方式,存储在常量池,这个字节码包含了指向常量池的引用,在动态链接的时候会用到运行时常量池。
常量池可以看做是一张表,虚拟机指令根据这个表找到要执行的类名、方法名、参数类型、字面量等类型。
常量池有什么
几种在常量池内存储的数据类型:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
运行时常量池
运行时常量池是方法区的一部分。
而常量池是字节码文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。
在加载类和接口后,就会创建对应的运行时常量池。
JVM为每个已加载的类型或接口都维护一个常量池。池中的数据项和数组项一样,使用索引访问。
运行时常量池中包含多种不同的常量,包括编译期就明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号引用了,这里是真实地址。
相对于字节码文件的常量池,运行时常量池具备动态性。
运行时常量池类似传统编程语言的符号表,但是它包含的数据比符号表更加丰富。
当创建类或者接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区能够提供的最大值,就会java.lang.OutOfMemoryError错误。
方法区演进
只有hotspot才有永久代,原则上如何实现方法区属于虚拟机实现细节,不受JVM规范约束。
hotspot方法区变化
- jdk1.6及以前:有永久代,静态变量(对应的对象实体始终存储在堆里面)存放在永久代。
- jdk1.7有永久代,但是字符串常量池、静态变量移除,保存到堆。
- jdk1.8及以后,无永久代,类信息、字段、方法、常量保存在本地内存的元空间,但是字符串常量池、静态变量在堆。
为什么使用元空间替换永久代
- 为永久代分配空间是很难确定的,如果动态加载类过多,容易产生OOM
- 而元空间并不在虚拟机内,而是使用本地内存,因此默认情况下,元空间大小仅受本地内存限制
- 对永久代调优比较困难
垃圾回收
方法区也是有垃圾回收的,约束非常宽松,JVM规范提到可以不要求方法区实现垃圾收集。
事实上也确实有未实现或未能完整实现方法区类型卸载的收集器,例如JDK11的ZGC就不支持类卸载。
一般来说方法区的垃圾回收效果很难令人满意,尤其是类型卸载,条件非常苛刻。但是这部分区域的垃圾回收有时候也是必要的。以前出现过若干严重BUG就是由于低版本的hotspot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中的废弃常量和不再使用的类型。
方法区常量池
主要存放两大类常量:字面量和符号引用。
字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值。而符号引用则属于编译原理方面的概念:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
注意:静态变量引用的对象始终保存在堆中。
hotspot虚拟机对常量池的回收策略是很明确的,只要常量池的常量没有被任何地方引用,就可以被回收。
回收废弃常量与回收Java堆的对象非常类似。
判断一个常量是否废弃相对简单,而要判断一个类是否属于不再被使用是比较苛刻的,三个条件:
- 该类的所有实例都已经被回收,也就是Java堆不存在该类及其任何子类的实例
- 加载该类的类加载器被回收,很难达成
- 该类对应的Class对象没有在任何地方被使用
Java虚拟机被允许对满足上述三个条件的无用类进行回收,而并不是必然被回收。
hotspot虚拟机提供了-Xnoclassgc参数控制类的垃圾回收。
还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类的加载和卸载。
在大量使用反射、动态代理和CGLib的场景下,通常都需要Java虚拟机具备卸载类的能力,以保证不会对方法区造成过大的压力。
StringTable为什么调整
Jdk7将StringTable放入了堆中,因为永久代的垃圾回收效率低下,在Full GC的时候才会触发,而Full GC是老年代空间不足、永久代空间不足时触发。
这就导致StringTable回收效率不高,而开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放入堆里面之后,就可以及时回收内存。
工具
JHSDB - JDK9的一个新工具。
一个值得注意的事
在JVM规范中,所有Class相关信息都应该保存在方法区,但是方法区应该如何实现,并没有规定。
JDK 7及以后版本的hotspot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储在堆。
直接内存
概述
不是虚拟机运行时数据区的一部分,也不是JVM规范定义的内存区域。
直接内存是在Java堆外、直接向操作系统申请的内存空间。
来源与NIO,通过堆中的DirectByteBuffer操作本地内存。
通常,访问直接内存的速度由于Java堆,可以提高读写性能。
因此处于性能考虑读写频繁的场合可以考虑使用直接内存。
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
可能导致OOM异常。
由于直接内存在Java堆外,因此大小不会直接受限于-Xmx指定的值,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统的物理内存。
缺点
- 分配回收成本高
- 不受JVM内存回收管理
参数
MaxDirectMemorySize
如果不指定,默认与堆的最大值一致。
方法区题目
- JVM内存模型
- Java8内存分代改进
- JVM内存分几个区,作用是什么
- JVM内存分布,内存结构。栈和堆的区别,堆的结构,为什么有两个S区
- Eden和S区的比例
- JVM为什么有新生代和老年代
- JVM运行时数据区
- 什么时候对象会进入老年代
- Java内存分配
- 永久代会发生垃圾回收吗