一、栈、堆、方法区的交互关系
- 三者的交互关系从一行简单的代码出发
Person p = new Person();
1、类信息Person存储在方法区中
2、引用p存储在栈中
3、new Person()这个操作主要在堆上进行
二、方法区的理解
1、方法区在哪里?
- 方法区被看作是一块独立于堆的内存空间
2、一些理解
-
方法区与堆都是线程共享
-
方法区在jvm启动的时候就被创建了,而且它的实际的物理内存空间与堆一样,可以不连续
-
方法区的大小可以固定大小也可以动态扩展,这点和堆一样
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,可能会使方法区溢出,出现OOM。所以方法区溢出的一个原因就是加载了过多的类。
-
关闭jvm就是释放方法区的内存
三、设置方法区大小与OOM
1、不同jdk版本设置方法区大小
jdk7及以前版本
- -XX:PermSize:设置用接待初始化空间,默认为20.75M
- -XX:MaxPermSize:设置永久代最大可分配空间。32位机器为64M,64位机器为82M
jdk8及以后版本:
- -XX:MetaspaceSize:设置元空间的初始大小
- -XX:MaxMetaspaceSize:设置元空间的最大大小
2、解决OOM的方法论
- 先使用内存映像分析工具堆dump出来的堆转储快照进行分析。重点是确定是内存溢出还是内存泄露
- 如果是内存泄露,可进一步通过哦工具查看泄露对象到GC Roots的引用链。这样便可以找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器不发自动回收对象的。掌握了西楼对象的类型信息,以及GC Roots引用连的信息,就可以比较准确的定位出泄露代码的位置。
- 如果不存在内存泄露,换句话说就是内存中的对象确实都需要存活,那就应该检查虚拟机的堆参数,与机器物理内存对比是否可以调大;从代码检查是否存在某些对象生命周期过长、持有状态时间过长的情况。
四、方法区的内部结构
1、方法区存储什么?
- 存储已经被虚拟机加载的类的类型信息、常量、静态变量、即时编译器编译后的代码缓存、域信息、方法信息。
- 类型信息:对每个加载的类型(class,interface,enum,annotation)jvm必须在方法区中存储一下类型的信息:
- 这个类型的完整的有效名称
- 这个类型直接父类的完整有效名
- 这个类型的修饰符
- 这个类型有接口的一个有序列表
- 域信息:保存属性的相关信息以及域的声明顺序。包含:域名称、域类型、修饰符
- 方法信息:包含方法名称、返回类型、参数和类型、修饰符、方法的字节码、操作数栈、局部变量表
- final修饰的值:final修饰的常量在编译的时候就已经分配了值;static修饰的值在链接的第二个阶段:准备阶段做初始化。
2、常量池VS运行时常量池
- 一个有效的字节码文件中包含类的版本信息、字段、方法以及接口等描述信息外,还包含常量池,常量池中包含各种字面量和对类型、域和方法的符号引用
常量池
-
class文件的一部分
-
为什么需要常量池呢?
- 其实主要解释为什么存储的是符号引用?主要是不能将所有需要的类直接加载记录下来,所以通过符号引用去找对应的类
-
常量池中存储的数据类型包含哪些?
- 数量值、字符串值、类引用、字段引用、方法引用
运行时常量池
- 方法区的一部分
- 在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
- 具有动态性
五、方法区的使用举例
六、方法区的细节演示
1、变迁历史
- 这边讨论的内容都是在Hotspot虚拟机上讨论
| jdk版本 | 变化 |
|---|---|
| jdk1.6及以前 | 有永久代,静态变量存放在永久代上 |
| jdk1.7 | 有永久代,但已经在逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 |
| jdk1.8 | 无永久代;类型信息、字段、方法、常量保存在本地内存的元空间;字符串常量池、静态变量保存在堆中 |
2、为什么永久代会被元空间替换?
- 主要是永久代的大小不能很好的被设置:在不断的加载类时,需要不断的申请永久代的大小,虽然前面说过可以设置永久代的初始值和最大值,但是这两个值都制约着系统性能。一方面设置太小容易引起OOM;另一方面设置太大容易浪费虚拟机资源。
- 对永久代的调优比较困难:主要是类的回收的条件比较苛刻,这个后面会说。
3、StringTable(字符串常量池)为什么会被调整?
- 永久代的GC的效率比较低,只有FullGC时才会触发回收字符串。我们在开发时,常常会用到i很多字符串,所以调整到堆中。堆中的GC效率相比永久代要高。
4、静态变量存放在哪里?
- 在前面的变迁史中说到过静态变量存放在哪里;但是这边说的内容和上面有些不同。
class A{
public static byte[] arr = new byte[1024*1024*100];
main()方法
}
参数:-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m
- 上面的这个代码,运行后得出的结论是什么呢?
- 通过new关键字得到的数组,不论jdk版本的变迁,都是保存在堆上
- 引用arr会随着不同jdk版本保存在不同的地方,具体位置参考上面的变迁史的列表
七、方法区的垃圾回收
- java虚拟机规范没有特别要求需要在方法区进行GC
- 一般来说在方法区的回收效果比较差,特别是类型的卸载。
- 方法区的GC主要收集下面两部分的内容:常量池的废弃常量和不在使用的类型
1、常量池的回收
- 主要常量没有被任务地方引用就可以被回收
2、类的回收
- 需要满足一些条件才能被回收
- 该类的所有实例都已经被回收了,包含子类、父类
- 该类的类加载器被卸载
- 该类对应的java.lang.class对象没有被任何地方引用。
- 满足以上条件后,仅仅是有可能被回收