参考
尚硅谷:宋红康(b站视频)
一、方法区的理解
1.1 方法区的定位
- 《Java虚拟机规范》:尽管所有方法区在逻辑上属于堆一部分,但一些简单实现,可能不会进行垃圾收集或进行压缩。
- 对于HotSpot,方法区又名:Non-Heap(非堆),目的:区分堆。
- 方法区看作是一块独立于Java堆的内存空间
1.2 方法区和堆的区别
- 不同点
- 方法区主要存放Class(类元信息),堆中主要存放实例化对象
- 相同点
- 方法区/Java堆,各个线程共享内存区域
- 方法区/Java堆,在JVM启动时被创建,物理内存空间可以不连续,逻辑空间要连续
- 方法区/Java堆,可以选择固定大小或者可扩展
- 其他介绍
方法区大小决定了系统可以保存多少个类,如果类定义太多,导致方法区溢出,JVM同样抛出内存溢出异常 OOM
java.lang.OutofMemoryError:PermGen space (jdk 7)
java.lang.OutOfMemoryError:Metaspace (jdk 8)
会出现OOM的情况:
- 加载大量的第三方的jar包
- Tomcat部署的工程过多(30~50个)
- 大量动态的生成反射类
关闭JVM就会释放这个区域的内存
二、栈、堆、方法区交互关系
- 堆和方法区是线程共享的,一个JVM实例只会存在一个堆和元空间,而栈是线程私有的
- 三者之间的存储结构
- 方法区:存储这类元信息
- 堆:存放的是对象的实例数据
- 栈:存放的是实例变量
三者之间的关系:
- Person 类的 .class 信息存放在方法区中
- person 变量存放在 Java 栈的局部变量表中
- 真正的 person 对象存放在 Java 堆中
- 在 person 对象中,有个指针指向方法区中的 person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的
三、设置方法区大小与OOM
方法区大小可固定,jvm可根据应用需求动态调整
3.1 JDK7及之前设置永久代大小
- -XX:PermSize 设置永久代初始分配空间
- -XX:MaxPermSize 设置永久代最大可分配空间
- JVM加载类信息容量超过设定值,会报异常OutofMemoryError:PermGen space
3.2 JDK8设置元空间大小
- -XX:MetaspaceSize:设置初始元空间大小
- 64位服务端JVM,默认初始元数据区空间21M,初始的高水位线
- 触及水位线,FullGC触发并卸载没用类,高水位线会被重置。新高水位线值取决于GC后释放了多少元空间。
- 如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,FullGC多次调用。为避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高值
- -XX:MaxMetaspaceSize:-1(没有限制)
- 不指定大小,虚拟机耗尽所有系统可用内存,一样抛出异常OutOfMemoryError:Metaspace
这里还有一种情况,如果你的元空间设置的太小,JVM虚拟机便无法运行:
看下代码:
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 1000000; 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);
}
}
}
这里可以看到元空间的内存占用越来越多;
10052
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
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.atguigu.java.OOMTest.main(OOMTest.java:29)
3.3 如何解决OOM或Head space
- 一般手段:通过内存映像分析工具(如Eclipse Memory Analyzer),对dump出来的堆转存储快照分析,重点确认:内存中的对象是否是必要的。先分清:内存泄露,还是内存溢出
- 什么是内存泄漏?
- 大量引用指向某些对象,但是这些对象以后不会使用。这些对象还和GC ROOT有关联,所以也不会被回收
- 大量引用–>对象,不去用又不能回收
- 若内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置(jprofile)
- 若内存泄漏,导致垃圾回收器无法自动回收原因
- 如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
- 若内存溢出,jvm参数-Xmx/-Xms 调大,减少程序运行时的内存耗用
四、方法区的结构
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
4.1 类型信息
对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation)JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
- 例:public class MethodInnerStrucTest extends Object implements Comparable, Serializable
4.2 域(Field)信息
就是成员变量
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
- 域信息特殊情况
- 类变量:non-final 类型
a. 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
b. 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它(空指针调用不会异常)
c. 证明不属于特定类实例,随着类的加载而加载- 全局常量:static final 进行修饰
a. 每个全局常量在编译阶段被分配。
b. 反编译,查看字节码指令,可以发现 number 的值已写死在字节码文件中
4.3 方法(Method)信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
- 方法名称
- 方法的返回类型(包括 void 返回类型),void 在 Java 中对应的为 void.class
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
4.4 运行时常量池
4.4.1 运行时常量池 VS 常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
a. 运行时将常量池加载到方法区,就是运行时常量池
b. 执行时,将常量池中的符号引用(字面量)转换为直接引用(真正的地址值)- 加载类的信息在方法区,需要理解字节码文件
- 要弄清方法区的运行时常量池,需要理解字节码文件中的常量池
- 运行时常量池,相对于class文件常量池:具备动态性
4.4.2 常量池
- 字节码文件包含:类的版本信息、字段、方法以及接口等描述信息
- 还包含常量池表(Constant Pool Table),包括编译生成各个字面量和对类型、域和方法的符号引用
- 为什么要用常量池?
- 一个java源文件中的类、接口、编译后产生字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。
- 可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接会用到运行时常量池。
- 编译产生字节码文件需要大量数据支持,不能存在字节码文件中,存到常量池里,字节码包含指向常量池的引用
- 常量池有什么?
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
- 常量池总结:常量池,可看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
4.4.3 JVM运行时内部结构
- 运行时常量池(Runtime Constant Pool)是方法区一部分
- 常量池表(Constant Pool Table)是class字节码文件一部分,(用于存放编译生成各个字面量和对类型、域和方法的符号引用),这部分内容将在类加载后存放到方法区的运行时常量池中。
- 创建:在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- 当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
- JVM为每个已加载的类和接口都维护一个运行常量池,池中的数据像数组项一样,通过索引访问
- 运行时常量池包含多种不同的常量,(包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。)此时不再是常量池中的符号地址,这里转换为真实地址。
- 运行时常量池,相对于class文件常量池:具备动态性:例如:String.intern可以将字符串也放入运行时常量池
- 常量池数量为N,则索引为1到N-1?
五、方法区演进细节
5.1 方法区演进的简单过程
- HostSpot可看作方法区永久代等价,本质不等价,《Java虚拟机规范》对如何实现方法区,不做统一要求。
- 在jdk7及以前,方法区–>永久代,jdk8开始,永久代–>元空间
- 元空间永久代,都是对JVM规范中方法区的实现。
- 元空间永久代区别:元空间不在虚拟机中设置内存,使用本地内存(堆外内存)
- 根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常
- 永久代更容易导致Java程序OOM(超过-XX:MaxPermsize上限)
5.2 方法区演进的详细过程
- 首先明确,只有HotSpot才有永久代
-
jdk1.6及之前,有永久代,静态变量存放在永久代上。使用 JVM 虚拟机内存
-
jdk1.7,有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中。使用 JVM 虚拟机内存
-
Java8的内存分代改进(问题同:为什么被替代)
-
jdk7及之前:永久代和堆物理内存连续,与老年代一起进行垃圾回收
-
方法区是JVM的规范,永久代,元空间是方法区的实现
-
jdk8,取消永久代,使用元空间实现方法区(保存类型信息,字段,方法,常量) JVM内存–>本地内存。
5.3 永久代为什么要被元空间替代?
- 永久代设置空间大小很难确定
- 如果动态加载类过多,就容易产生OOM
- 会经常触发Full GC
- 设置-XX:PermSize,初始化分配一块连续的内存块
a. 设置过大:内存浪费
b. 设置过小:OOM
- 元空间存储在本地内存,仅受本地内存限制。
- 元空间的参数设置:
- -XX:MetaspaceSize
- XX:MaxMetaspaceSize
- 达到-XX:MetaspaceSize–>触发FGC–>进行类型卸载,同时GC会对该值进行调整(可动态调整)
a. 如果释放了大量的空间,就适当降低该值
b. 如果释放了很少的空间,那么在不超过MaxMetaspaceSize,适当提高该值。
- 对永久代进行调优很困难
- 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了减少Full GC次数
- 有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。
- 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏
5.4 字符串常量池 StringTable 为什么要调整位置?
- JDK7中将StringTable从运行时常量池移到堆空间。Full GC执行永久代的垃圾回收,永久代回收效率低。Full GC触发条件:老年代空间不足、永久代空间不足
- 开发中会有大量字符串被创建,回收效率低,导致永久代内存不足。
- 移动到堆,提高回收效率
静态变量存放在哪里?:
JDK7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中
六、方法区的垃圾回收
常量池中废弃的常量和不再使用的类型。
6.1 常量池中废弃的常量
- HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
- 回收废弃常量与回收Java堆中对象非常类似
- 方法区内常量池中主要存放两大类常量:
- 字面量(常量):如文本字符串,被声明为final的常量值等
符号引用(编译原理)
a. 类和接口的全限定名
b. 字段的名称和描述符
c. 方法的名称和描述符
6.2 方法区类的回收
- 不再使用的类型,需要同时满足三个条件(类卸载)
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收(难达成)
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 满足以上三个条件后,并不是和对象一样立即被回收,仅仅是被允许。
- HotSpot虚拟机提供了-Xnoclassgc参数进行控制
- 在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力
七、总结
方法区
感谢大家阅、互相学习;
感谢尚硅谷提供的学习资料;
有问题评论或者发邮箱;
gitee:很多代码仓库;
1449697757@qq.com