一、栈、堆、方法区的交互关系
从线程共享与否的角度来看:
下面一个简单的图示,说明了栈、堆、方法区之间关系:
- person 对象的引用存放在栈中
- new Person() 对象本身存放在堆中
- Person 生成的结构,class文件存放在方法区
二、方法区的理解
方法区是线程共享的,存放的java文件编译后的指令(class)。
尽管所有方法区在逻辑上都属于堆的一部分,但一些简单的实现可能不会选择去进行GC或者进行压缩。但是HtoSpotJVM,把方法区叫做Non-Heap,目的就是要与堆分开。所以方法区看做是独立于java堆的内存空间。
- jvm启动的时候创建,物理上可以不连续
- 大小固定或者扩展
- 大小决定了保存多少个类,可能OOM:Metaspace/PermGen Space
- jvm关闭后,方法区释放
Hotspot中方法的演进:
- jdk7-永久代、jdk8-元空间
- 永久代和方法区并不等价,但是在Hotspot JVM中,是等价的,永久代还是使用java内存(-XX:MaxPermSize),更容易出现OOM
- jdk8之后,使用元空间代替了永久代,也使用了本地内存,相对更不容易出现OOM了,同时内部实现也改变了
三、设置防区大小与OOM
大小可以设置成固定或者可变的。
jdk7及以前:
- -XX:PermSize,默认值是20.75M
- -XX:MaxPermSize,32位默认64M,64位默认82M
jdk8及以后:
- -XX:MetaspaceSize,win平台 21M
- -XX:MaxMetaspaceSize,win平台无限制
- MetaspaceSize满了之后,会触发Full GC,所以初始值甚至的大一些
动态反射,会一直创建新的类,一直到OOM。
遇到OOM怎么解决:
- 导出dump,分析文件,是内存泄露(Memory Leak)还是内存溢出(Memoru Overflow)
- 如果是内存泄露,查看GC Roots引用链。
- 如果不是内存泄露,那么就调整堆大小,或者调整对象的生命周期
四、方法区的内部结构
主要存放类信息和运行时常量池(字符串常量)。
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
1、类型信息(class、interface、enum、annotation),必须存储如下信息
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名称(interface、Object 都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子类)
- 这个类型直接接口的一个有序列表
2、域信息(属性\成员变量)
- jvm在方法区必须保存类型的所有域信息以及域的声明顺序
- 域的类型包括:域名称、域类型、域修饰符
3、方法信息
- 方法名称
- 方法返回值
- 方法参数的梳理和类型(按顺序)
- 方法的修饰符
- 方法的字节码、操作数栈、局部变量表及大小
- 异常表,每个异常的开始位置、结束位置、代码处理在程序计数器中的便宜地址、被捕获的异常类的常量池索引
类加载到方法区之后,会记录是哪个ClassLoader加载的自己,ClassLoader也会记录了加载了哪些类。
non-final变量:
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例是,也可以访问它
全局常量(static final):被声明为final的类变量的处理方法不同,每个全局常量在编译的时候就被分配了,就是在编译的时候,值就确定了。static是在初始化的时候才会赋值。
运行时常量池VS常量池
常量池就是字节码的一部分(Constant Pool Table),包含了字面量和对类型、域、方法的符号引用。
- 数值
- 类引用
- 字段引用
- 方法引用
- 字符串值
一段程序运行成功,需要的关联的各种类会很多,所以这些用到的信息,作为一个符号出现也就是符号引用,当真正使用的时候,才运行真正的程序。其实就是为了减小程序。
常量池可以看做是一张表,JVM质量根据这张表,找到需要执行的类名、方法名、参数类型、字面量等类型。
字节码文件的常量池,经过ClassLoader加载到方法区之后,就成为运行时常量池(Runtime Constant Pool),但是相对而言具备了动态性。
五、方法区的使用举例
六、方法区的演进细节
Hotspot的方法区的变化:
jdk6及之前 | 有永久代(permanent generation),静态变量放在永久带上 |
jdk7 | 有永久代,但是已经逐渐开始“去永久代”,字符串常量池、静态变量移除,保存在堆中 |
jdk8及之后 | 无永久代,类型信息、字段、方法、常量保存在本地内存元空间,但是字符串常量池、静态变量仍然在堆中 |
为什么替换:
- 永久代设置空间大小,很难确定,如果加载类多,那么容易出现OOM,空间小也容易出现Full GC
- 对永久代调优很困难,判断类或者常量不在使用也是耗费时间,所以STW时间很长
StringTable、静态变量为什么调整位置:
1、StringTable:因为永久代垃圾回收效率很低,在Full GC的时候才会触发,而Full GC是老年代的空间不足、永久代空间不足才会触发。这就是导致StringTable回收效率低。而在开发中,会大量创建字符串,回收效率低,到时永久代内存不足。放到堆里,才能及时回收。
2、静态变量存放在哪里:静态引用对应的对象都放在堆空间中,静态变量的引用,从jdk7开始存放在堆中了
七、方法区的垃圾回收
因为类的卸载条件很苛刻,所以方法区的垃圾回收效果很难令人满意,但是和部分的GC有时确实需要。
主要回收的内容:常量池中废弃的常量和不在使用的类型
判断类不在使用的三个条件:
- 所有对象都被回收,也就是堆中不存在该类及其任何派生子类的实例
- 加载该类的ClassLoader已经回收
- 该类对应的java.lang.Class对象没有在任何地方被引用