JVM运行时数据区之方法区

方法区

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

运行时数据区的结构图
运行时数据区的结构图
从线程共享与否的角度看
从线程共享与否的角度看
栈、堆、方法区的交互关系
栈、堆、方法区的交互关系

方法区的理解

官方文档

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

访问官网

方法区在哪里?

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会去选择去进行垃圾收集或者进行压缩。”但对于HotspotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
运行时数据区

方法区的基本理解
  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候别创建,并且它的实际物理内存空间中和Java堆区一样都可以是不连续的。
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误:java.lang.OutofMemoryError:PermGen space或者java.lang.OutofMemoryError:Metaspace
    • 加载大量的第三方的jar包:Tomcat部署的工程过多(30~50个),大量动态的生成反射类。
  • 关闭JVM就会释放这个区域的内存
Hotsp中方法区的演进
  • 在jdk7以前,习惯上把方法区,成为永久代。Jdk8开始,使用元空间取代了永久代。

  • 本质上,方法区和永久代并不等价。仅是对Hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA Jrockit/IBM J9 中不存在永久代的概念。

    • 现在看来,当年使用永久代,不是好的idea。导致Java程序更容易OOM(超过-XX:MaxPermSize上限)
      方法区概述
  • 而到了JDK 8 ,终于完全废弃了永久代的概念,改用与JRockit、J9一样本地内存中实现的元空间(Metaspace)来代替
    Hotsp中方法区的演进

  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

  • 永久代、元空间二者并不只是名字变了,内部结构也调整了。

  • 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

设置方法区大小与OOM

设置方法区大小
  • 方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
  • jdk7及以前:
    • 通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
    • -XX:MaxPermSize来设定永久代最大可分配空间。32为机器默认是64M,64位机器模式是82M
    • 当JVM加载的类信息容量超过这个值,会报异常OutOfMemoryError:PermGen space.
  • jdk8及以后:
    • 元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaSpaceSize指定,替代上述原有的两个参数。
    • 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
    • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出OutOfMemoryError:Metaspace
    • -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为12MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不在存活),然后这个高水位线将会重置。新的高水位线的值取决于GC释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
    • 如果初始化的高水位线设置过低,上述高水位线调整情况或发生很多次。通过垃圾回收器的日志可以观察Full GC多次调用。为了避免频繁GC,建议将-XX:MetaspaceSize设置一个相对高的值。

代码举例

  /**
 * jdk 8 中  -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * jdk 6 中 -XX:PermSize=8M -XX:MaxPermSize=8M
 在jdk 6 中的ClassWriter这个包的位置和jdk8不一致需要重新导入
 * @program: JVMDemo
 * @description: test
 * @author: 郑朝文
 * @create: 2021-08-30 17:13
 **/
public class MethodOOMTest extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        MethodOOMTest test = new MethodOOMTest();
        try {

            for (int i = 0; i < 10000; i++) {
                // 创建classwriter对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                // 指明版本号,public,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);

                byte[] bytes = classWriter.toByteArray();
                test.defineClass("Class" + i, bytes, 0, bytes.length);
                j++;

            }
        } finally {
            System.out.println(j);
        }
    }
}

jdk6中的异常信息
jdk6
jdk8中的异常信息
jdk8

如何解决这些OOM
  • 1、要解决OOM异常或heap spece 异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memeory Overflow).
  • 2、如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄露代码的位置。
  • 3、如果不存在内存泄漏,换句话说就是内存中的对象还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗。

方法区的内部结构

方法区的内部结构

方法区存储什么?

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的内部结构

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

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

方法(Method)信息
JVM必须保存方法的一下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(或void)
方法参数的数量或类型(按顺序)
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
方法的字节码(bytecodes)、操作数栈,局部变量表大小(abstract和native方法除外)
异常表(abstract和native方法除外)(每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引)

public class MethodInnerStructTest extends Object implements Comparable<String> , Serializable {
    // 属性
    public int num1 = 2;
    private static int num =1  ;
    // 方法
    public void test(){
        int count=20;
        System.out.println(count);
    }

    public static int test1(int a){
        return a;
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
}

字节码指令

non-final的类变量
  • 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
  • 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

public class MethodAreaTest {
    public static void main(String[] args) {
        Order order =null;

        order.hello();
        System.out.println(order.count);
    }
}
class  Order{
    public static  int count =1 ;
    public static  void hello(){
        System.out.println("hello");
    }
}

字节码指令

补充说明:全局常量:static final
被声明为fianl的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

运行时常量池vs常量池

  • 方法区,内部包含了运行时常量池。
  • 字节码文件,内部包含了常量池
  • 要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区。
  • 要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。

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

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

  public static void main(String[] args) {
        System.out.println("TEST" );
        try {
            Thread.sleep(1234234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

上面的代码虽然只有194字节,但是里面却使用了String,PrintStream及Object等结构。这里的代码量其实已经很小了。如果代码多,引用的机构会更多!这里就需要常量池了。

常量池中有什么?

几种在常量池内存存储的数据类型包括:

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用
    例如下面这段代码:

/**
 * @program: JVMDemo
 * @description: test
 * @author: 郑朝文
 * @create: 2021-08-31 15:01
 **/
public class MethodAreaTest2 {
    public static void main(String[] args) {
        Object o = new Object();
    }
}



Object o = new Object();
将会被编译成如下的字节码

         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V

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

运行时常量池
  • 运行时常量池(Runtime Constant Pool)是方法去的一部分。
  • 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  • 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  • JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
  • 运行时常量池中包含多种不同的常量,包括编译器就已经明确的数值字面量,也包括到运行时期解析后才能获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
    • 运行时常量池,相对Class文件常量池的另一个重要特征是:具备动态性。
      • String.intern();
  • 运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些。
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常。

方法区使用举例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

方法区的演进细节

  • 1、首先要明确:只有Hotspot才有永久代。
    BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。
  • 2、Hotspot中方法区的变化。
    方法区演进
    6
    7
    8
    官网说明
永久代为什么要被元空间替换?
  • 随着Java8的到来,Hotspot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到一个与堆并不相连的本地内存区域,这个区域被叫做元空间(Metaspace)
  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
  • 这项改动是很有必要的,原因有:
    • 为永久代设置空间大小是很难确定的
      在某些场景下,如果动态加载类过多,容易产生Perm 区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
      而元空间和永久代最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
    • 对永久代调优是很困难的。

StringTable 为什么要调整
jak7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会被触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆中,能及时回收内存。

静态变量放在哪里

/**
结论:静态引用对应的对象实体始终都存在堆空间中。
 * jdk 7 -Xms200M -Xmx200M -XX:PermSize=300M -XX:MaxPermSize=300M -XX:+PrintGCDetails
 * jdk 8 -Xms200M -Xmx200M -XX:MetaspaceSize=300M -XX:MaxMetaspaceSize=300M -XX:+PrintGCDetails
 * @program: JVMDemo
 * @description: test
 * @author: 郑朝文
 * @create: 2021-08-31 15:42
 **/
public class StaticFieldTest {
    private static  byte [] arr = new byte[1024* 1024 *100 ];

    public static void main(String[] args) {
        System.out.println(arr.length);
    }
}

import org.omg.CORBA.ObjectHolder;

/**
 * @program: JVMDemo
 * @description: Test
 * @author: 郑朝文
 * @create: 2021-08-31 15:51
 **/
public class StaticObject {
    static  class  Test{
        static StaticObject.ObjectHolder staticObj = new StaticObject.ObjectHolder();
        StaticObject.ObjectHolder instanceObj = new StaticObject.ObjectHolder();
        void foo(){
            StaticObject.ObjectHolder localObj = new StaticObject.ObjectHolder();
            System.out.println("done");

        }
    }
    private  static class  ObjectHolder{

    }

    public static void main(String[] args) {
        Test test = new Test() ;
        test.foo();
    }
}



使用JHSDB工具进行分析,这里细节忽略
1
staticObj随着Test的类型信息存放在方法区,instanceObj随着Test的对象实例存放在Java堆,localObject则是存放在foo()方法栈帧的局部变量表中。
2
测试发现:三个对象的数据在内存中的地址都落在Eden区范围内,所以结论:只要是对象实例必然会在Java堆中分配。
接着,找到一个引用该staticObj对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象的实例,可以清楚看到这确实是一个java.lang.Class类型对象的实例,里面有一个名为staticObj的实例字段:
2
从《java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应存放在方法区中,但方法区该如何实现,《Java虚拟机规范》并未作出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7及其以后版本的Hotspot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中,从我们的实验中也明确验证了这一点。

方法区的垃圾回收

垃圾1
垃圾2
垃圾3

总结

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最好的文酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值