JVM方法区详解

线程共享角度来分析运行时数据区

栈,堆,方法区的交互

一、概述

1、方法区概念

Hotspot虚拟机,方法区有个别称non-heap(非堆),方法区可以看作是一块独立于堆的内存空间

(1)方法区与java堆一样,是各个线程共享的内存区域

(2)方法区在jvm启动的时候被创建,并且它的实际的物理内存空间中和java堆区一样都是可以不连续的

(3)方法区的大小,跟堆空间一样,可以选择固定大小或者扩展

(4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误

java8以前(java.lang.OutOfMemoryError:PermGen space)或者java8以后(java.lang.OutOfMemoryError:Metaspace)

(5)关闭jvm就会释放这个区域的内存

2、Hotspot的方法区演进过程

jdk7以前,方法区的实现是永久代,jdk8开始方法区的实现使用元空间取代了永久代。

元空间和永久代最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存

3、设置方法区的内存大小

jdk7之前

(1)-XX:PermSize设置永久代初始分配空间默认是20.75M

(2)-XX:MaxPermSize设置最大永久代分配空间,32位机器默认64M,64位机器默认82M

(3)当jvm加载的类信息容量超过了这个值,会报异常java.lang.OutOfMemoryError:PermGen space

jdk8及以后

(1)元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定

(2)默认值依赖于平台,windows下,-xx:MetaspaceSize是21.75M,-XX:MaxMetaspaceSize的值是-1,即没有限制

(3)默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生溢出,虚拟机一样会抛出OutOfMemoryError:Metaspace

(4)-xx:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端jvm来说,其默认值-xx:MetaspaceSize的值是21M,这就是初始的高水位线,一旦触及这个水位线,Full GC将会触发并卸载没用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则降低该值。

(5)如果初始高水位线设置过低,上述高水位线调整情况发生很多次,通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁的GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

4、处理OOM

(1)OOM或者heap space异常,一般使用内存映像分析工具对dump出来的堆存储快照分析,分析是内存泄漏还是内存溢出

(2)如果是内存泄漏,可进一步通过查看GC roots的引用链,查找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

(3)如果不是内存泄漏,那就是说对象必须存活,此时需要增加虚拟机的参数(-Xmx和-Xms)。

二、方法区内部结构

方法区存储信息主要:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存

1、类型信息

对每个加载的类型(类class、接口、枚举、注解),jvm必须在方法区存储以下类型信息

(1)类型的完整有效名称(全名=报名.类名)

(2)类型直接父类的完整有效名(接口和java.lang.Object,没有父类)

(3)类型的修饰符(public,abstract,final的某个子集)

(4)类型直接接口的一个有序列表

2、域(Field)信息

(1)保存类型的所有域的相关信息以及域的声明顺序

(2)域的相关信息:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient)

3、方法(Method)信息

jvm保存所有方法的以下信息,同域信息一样的包括声明顺序

(1)方法名称

(2)方法返回参数(或者void)

(3)方法参数的数量和类型(按顺序)

(4)方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)

(5)方法的字节码,操作数栈、局部变量表及大小(abstract和native除外)

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

4、常量池

一个有效的字节码文件除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含一项信息那就是常量池,包含各种字面量(数量值,字符串值)和对类型(类),域和方法的符号引用。

常量池,可以看作是一个表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型

(1)方法区,内部包含了运行时常量池

(2)字节码文件,内部包含了常量池

字节码文件结构

常量池的作用

一个java源文件的类,接口,编译后产生一个字节码文件。而java中字节码需要数据支持,通常这种数据会很大以至于不能直接存储在字节码里,换一种方式,可以存储到常量池里。

运行时常量池

(1)运行时常量池是方法区的一部分,常量池表是class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载存放到方法区的运行时常量池中。

(2)运行时常量池创建时机:在加载类和接口到虚拟机后,就会创建对应的运行时常量池

(3)jvm为每一个已加载的类型(类或者接口)都维护一个常量池,池中的数据项和数组项类似,使用索引访问

(4)运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换成真实地址

(5)运行时常量池类似于传统编程语言的符号表,但是它所包含的数据比符号表更加丰富

(6)当创建类或者接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则jvm会抛出OutOfMemoryError异常。

(7)运行时常量池具备动态性,比如使用String类的intern方法加入运行时常量池中

String ss="hello";
ss.intern();

5、案例

public class Test {
    public static void main(String[] args) {
        int x=500;
        int y=100;
        int a=x/y;
        int b=50;
        System.out.println(a+b);
    }
}

对应的字节码文件

Classfile /D:/project/Java/target/classes/com/test/Test.class
  Last modified 2020-7-4; size 592 bytes
  MD5 checksum 0c50b054fc0a6201f72817b7ff36944c
  Compiled from "Test.java"
public class com.test.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool://常量池
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
   #4 = Class              #29            // com/test/Test
   #5 = Class              #30            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/test/Test;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               x
  #18 = Utf8               I
  #19 = Utf8               y
  #20 = Utf8               a
  #21 = Utf8               b
  #22 = Utf8               SourceFile
  #23 = Utf8               Test.java
  #24 = NameAndType        #6:#7          // "<init>":()V
  #25 = Class              #31            // java/lang/System
  #26 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #27 = Class              #34            // java/io/PrintStream
  #28 = NameAndType        #35:#36        // println:(I)V
  #29 = Utf8               com/test/Test
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (I)V
{
  public com.test.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/test/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code://代码主要逻辑
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 9: 0
        line 10: 4
        line 11: 7
        line 12: 11
        line 13: 15
        line 14: 25
      LocalVariableTable://局部变量表
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
            4      22     1     x   I
            7      19     2     y   I
           11      15     3     a   I
           15      11     4     b   I
}
SourceFile: "Test.java"

把焦点切入代码主逻辑

六、方法区演进过程(Hotspot)

jdk8使用元空间替换jdk7及以前的永久代的目的

(1)设置永久代空间大小,不太好确定

(2)对永久代调优比较困难

七、扩展

1、StringTable移入堆空间

jdk7将StringTable放入堆空间,因为永久代回收效率低,full gc时候才会触发,而full gc是老年代的空间不足,永久代空间不足才触发。这就导致StringTable回收效率低,而开发中有大量的字符串被创建,回收效率低,导致内存不足。当放入堆中,提高回收效率。

2、静态变量

jdk7及以后,

staticObj变量和它所指向的对象随着Test类的信息存放在堆中,

instanceObj变量存放在栈中,其对应的对象存放在堆中,

localObject存放在方法的栈帧的局部变量表中(栈中),其对应的对象在堆中

八、方法区的垃圾回收

方法区的回收效果不好,尤其是对类型的卸载,条件很苛刻,但是回收方法区又是必要的。

1、方法区的垃圾回收主要回收两部分

(1)常量池中废弃的常量

(2)不再使用的类型,需要将其卸载

判定一个类型是否能被卸载的条件

1)该类所有的实例都已经被回收,java堆中不存在该类以及任何派生子类的实例

2)加载该类的加载器已经被回收。

3)该类对应的java.lang.Class对象没有任何地方被引用,没有任何地方通过反射访问该类的方法

 

 

 

 

  • 26
    点赞
  • 116
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值