跳到主要内容

09、JVM 实战 - 方法区

1、堆、栈、方法区的交互关系

 

 

其中:

  • Person 类的 .class 信息存放在方法区中
  • person 变量存放在 Java 栈的局部变量表中
  • 真正的 person 对象存放在 Java 堆中
  • 在 person 对象中,有个指针指向方法区中的 person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的

2、方法区的理解

Java里面的Class在运行时是包含两部分数据的,一部分是类的元数据信息,这部分数据存储在方法区,包括常量池、属性表、方法表和异常表等,JVM在加载完一个类后会创建一个java.lang.Class类型的实例,这个实例是存储在堆中;除了基本数据类型外,java中所有的对象都是在堆中创建的。

方法区和Java堆一样,是各个线程共享的内存区域,在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,都是可以不连续的。

方法区的大小和堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGen space或者Metaspace 。

举例说明方法区OOM:

  • 加载大量的第三方的jar包
  • Tomcat部署的工程过多(30~50个)
  • 大量动态的生成反射类

HotSpot中方法区的演进:在jdk7及以前,方法区–>永久代,jdk8开始,永久代–>元空间

  • 元空间永久代区别:元空间不在虚拟机中设置内存,使用本地内存(堆外内存)。
  • 根据Jvm规范,如果方法区无法满足新的内存分配需求,将抛出OOM异常。

 

3、设置方法区大小与OOM

方法区大小不是固定的,jvm可以根据应用动态调整。

DK7及以前:

  • 通过-XX:PermSize 来设置永久代初始分配空间,默认值是20.75M

  • -XX:MaxPermSize来设定永久代最大可分配空间。

  • 32位机器默认是64M

  • 64位机器默认是82M

  • 如果JVM加载的类信息容量超过了这个值,会报OOM:PermGenspace

JDK8及以后:

  • -XX:MetaspaceSize
  • -XX:MaxMetaspaceSize
  • 默认值依赖平台:windows下初始为21M,最大是-1即没有限制
  • 如果不指定大小,虚拟机耗用所有可用系统内存,元数据区发生溢出,一样OOM:Metaspace

对于一个64位服务端 JVM来说,默认的初始元数据区空间为21M,这就是初始的高水位线。一旦触及这个水位线,FULLGC会触发并卸载没有用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。

 

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,fullGC多次调用。为了避免频繁FullGC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

如何解决OOM或heap space异常?

通过内存映像分析工具(如Eclipse Memory Analyzer),对dump出来的堆转存储快照分析,重点确认:内存中的对象是否是必要的。先分清:内存泄露,还是内存溢出。

什么是内存泄漏? 大量引用指向某些对象,但是这些对象以后不会使用。这些对象还和GC ROOT有关联,所以也不会被回收。

若内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置。

如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存消耗。

4、方法区的内部结构

 

用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存。

 

类型信息:

  • 对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation)JVM必须在方法区中存储以下类型信息
  • 类型的修饰符(public,abstract,final的某个子集)
  • 类的完整有效名称(全名=包名.类名)
  • 类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
  • 类型的直接接口的一个有序列表

 

域(Field)信息:

  • JVM必须在方法区中保存类型的所有域的相关信息,以及域的声明顺序
  • 域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)

 

域信息特殊情况

  • 类变量:non-final 类型

  • 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分

  • 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它(空指针调用不会异常)

  • 全局常量:static final 进行修饰

  • 每个全局常量在编译阶段被分配。

  • 反编译,查看字节码指令,可以发现 全局常量 的值已写死在字节码文件中

方法信息:

  • JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
  • 方法的返回类型(包括 void 返回类型),void 在 Java 中对应的类为 void.class
  • 方法名称
  • 方法参数的数量和类型(按顺序)
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)

 

  • 异常表(abstract和native方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引 

5、运行时常量池

方法区内部包含了运行时常量池,而字节码文件内部包含了常量池。

  • 运行时将常量池加载到方法区,就是运行时常量池
  • 执行时,将常量池中的符号引用(字面量)转换为直接引用(真正的地址值)

要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。一个有效的字节码文件中除了包含的类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用

 

为什么要用常量池?

一个java源文件中的类、接口、编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。换一种方式,可以存到常量池(复用),这个字节码包含了指向常量池的引用。在动态链接会用到运行时常量池。

常量池有什么?

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

常量池总结:常量池,可看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

运行时常量池:

  • 运行时常量池(Runtime Constant Pool)是方法区一部分
  • 常量池表(Constant Pool Table)是class字节码文件一部分,(用于存放编译生成各个字面量和对类型、域和方法的符号引用),这部分内容将在类加载后存放到方法区的运行时常量池中。
  • 创建:在加载类和接口到虚拟机后,就会创建对应的运行时常量池
  • JVM为每个已加载的类和接口都维护一个运行常量池,池中的数据像数组项一样,通过索引访问
  • 当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
  • 运行时常量池包含多种不同的常量,(包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。)此时不再是常量池中的符号地址,这里转换为真实地址。
  • 运行时常量池,相对于class文件常量池:具备动态性
  • 常量池数量为N,则索引为1到N-1

6、方法区的演进细节

首先明确,只有HotSpot才有永久代

  • jdk1.6及之前:有永久代,静态变量存放在永久代上

 

  • jdk1.7:有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中

 

  • jdk1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆。

 

(面试常问)永久代为什么要被元空间替代?

1、 永久代设置空间大小很难确定:;

  • 如果动态加载类过多,就容易产生OOM

  • 存储在本地内存,仅受本地内存限制。

  • -XX:MetaspaceSize

  • -XX:MaxMetaspaceSize,设置一样大,一般设置256M

  • 达到-XX:MetaspaceSize–>触发FGC–>进行类型卸载,同时GC会对该值进行调整**(可动态调整)**

2、 对永久代进行调优很困难,方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了减少FullGC次数;

(面试常问)字符串常量池 StringTable 为什么要调整位置?

  • JDK7中将StringTable从运行时常量池移到堆空间。Full GC执行永久代的垃圾回收,永久代回收效率低。Full GC触发条件:老年代空间不足、永久代空间不足
  • 开发中会有大量字符串被创建,回收效率低,导致永久代内存不足。
  • 移动到堆,提高回收效率

7、方法区的垃圾回收

方法区的垃圾回收:常量池中废弃的常量和不再使用的类型。

常量池中废弃的常量:

  • HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收

  • 回收废弃常量与回收Java堆中对象非常类似

  • 方法区内常量池中主要存放两大类常量:

  • 字面量(常量):如文本字符串,被声明为final的常量值等

  • 符号引用(编译原理):类和接口的全限定名、字段的方法和描述符、方法的名称和描述符

方法区类的回收不再使用的类型,需要同时满足三个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收(难达成)
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 满足以上三个条件后,并不是和对象一样立即被回收,仅仅是被允许。

在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。