深入理解JVM(六)一一运行时数据区(方法区)

2,599 阅读18分钟

运行时数据区(方法区)

虚拟机规范关于方法区描述

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

要点

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
    • java.lang.OutOfMemoryError:PermGen space (jdk7前)
    • java.lang.OutOfMemoryError:Metaspace(jdk8后)
  • 溢出场景:
    • 加载大量的第三方的jar包;
    • Tomcat部署的工程过多(30-50个)
    • 大量动态的生成反射类
  • 关闭JVM就会释放这个区域的内存。

查看并设置方法区大小

  1. jdk7及以前:
  • -XX:PermSize 来设置永久代初始分配空间。默认值是20.75M,
  • -XX:MaxPermSize 来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OutOfMemoryError:PermGen space。
  1. jdk8及以后:
  • -XX:MetaspaceSize 设置元空间初始化空间

  • -XX:MaxMetaspaceSize 设置元空间内存最大空间

  • 默认值依赖于平台。windows下,-XX:MetaspaceSize是约20.79M;-XX:MaxMetaspaceSize的值在windows下很大;如是-1,即没有限制。

  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError: Metaspace

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

  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,对应8G物理内存的机器来说,一般将这个值设置成256M。

3.window下查看大小

查看window元空间大小.png

方法区OOM例子

运行参数

-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

        /**
         * 测试方法区OOM
         * jdk6/7中:
         * -XX:PermSize=10m -XX:MaxPermSize=10m
         *
         * jdk8中:
         * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
         *
         */
        public class OOMTest extends ClassLoader {
            public static void main(String[] args) {
                int j = 0;
                try {
                    OOMTest test = new OOMTest();
                    for (int i = 0; i < 10000; i++) {
                        //创建ClassWriter对象,用于生成类的二进制字节码
                        ClassWriter classWriter = new ClassWriter(0);
                        //指明版本号,修饰符,类名,包名,父类,接口
                        classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                        //返回byte[]
                        byte[] code = classWriter.toByteArray();
                        //类的加载
                        test.defineClass("Class" + i, code, 0, code.length);//Class对象
                        j++;
                    }
                } finally {
                    System.out.println(j);
                }
            }
        }

方法区OOM结果.png

如何解决方法区OOM

  1. 要解决00M异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer) 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory 0verflow) 。

  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。

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

拾遗-内存泄露和内存溢出区别

  • 内存泄露(Memory Leak):程序在申请内存使用后的对象已没有存在的意义了,但对象没有被GC所回收,它始终占用内存,内存泄漏的堆积最终会造成内存溢出。
  • 内存溢出(Memory Overflow):程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于老年代垃圾回收后,仍然无内存空间容纳新的Java对象的情况。通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出

方法区

基于Hotspot1.8

image.png

类元信息

类型信息

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

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名(对于interface或是java. lang . object,都没有父类)
  • 这个类型的修饰符(public, abstract, final的某个子集)
  • 这个类型直接接口的一个有序列表

域信息

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

  • 域声明顺序
  • 域名称
  • 域类型
  • 域修饰符(public, private,protected, static, final, volatile, transient的某个子集)

注意:域(Field)=字段=属性=成员变量 一个意思

方法信息

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

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

方法表

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法。jvm可以通过方法表快速激活实例方法

类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。jvm在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是至关重要的。

Class类实例的引用

jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

运行时常量池

是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池和class文件的常量池是一一对应的,它就是class文件的常量池来构建的。

运行时常量池是方法区的一部分。CLass文件中常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

字面量

  • 文本字符串、被声明为final的常量值,基本数据类型值等

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(natation)。

区分方法:等号右边的八种基本类型的值、字符串值、声明为final的常量的值。例如:

        final int a = 10; //a为常量,10为字面量
        string b = “hello world!”; // b 为变量,hello world!为字面量

符号引用

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

JIT代码缓存

从字面意思理解就是代码缓存区,它缓存的是JIT(Just in Time)即时编译器编译的代码,简言之codeCache是存放JIT生成的机器码(native code)。当然JNI(Java本地接口)的机器码也放在codeCache里,不过JIT编译生成的native code占主要部分。

JVM会对频繁使用的代码,即热点代码(Hot Spot Code),达到一定的阈值后会编译成本地平台相关的机器码,并进行各层次的优化,提升执行效率。

热点代码也分两种:

  • 被多次调用的方法
  • 被多次执行的循环体

方法区jdk7和jdk8区别

jdk7和jdk8方法区区别图.png

  • 在jdk7及以前, 习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。

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

  • 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代。

  • 在JDK1.7字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。

  • 本质上,方法区和永久代并不等价。仅是对hotspot而言的。《Java虛 拟机规范》对如何实现方法区,不做统一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。方法区只是一规范, 在不同的虚拟机中的实现是不一样的,例如永久代和元空间。

  • 现在来看,当年使用永久代,不是好的idea。 导致Java程序更容易OOM (超过-XX:MaxPermSize上限)

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

方法区各版本演进区别

版本变化
jdk1.6及之前有永久代(permanent generation),静态变量存放在永久代上
jdk1.7有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8及之后无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

jdk6方法区.png

jdk7方法区.png

jdk8方法区.png

总结:

  • 永久代改为元空间
  • 元空间使用直接内存(本地内存,系统内存)

官网关于静态变量移到堆中说明

Currently static fields are stored in the instanceKlass but when those are moved into native memory we'd have to have a new card mark strategy for static fields. This could be something like setting a flag in the instanceKlass and then rescanning every klass during a GC which seems expensive or marking the card for the java.lang.Class then making sure to scan the instanceKlass when scanning the Class. If we move them into the Class then almost all the existing machinery works exactly as it always has. The only execution difference is which constant is materialized for the field access.

官网关于字符串常量池移到堆中说明

Interned strings are currently stored in the permanent generation. A new approach for managing meta-data is being designed and it requires interned strings to live elsewhere in the the heap (young gen and/or old gen).

永久代为什么要被元空间替换

  1. 为永久代设置空间大小是很难确定的。

在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误:java.lang.OutOfMemoryError: PermGen space

元空间并不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制。

  1. 对永久代进行调优是很困难的。

字符串常量池(StringTable)为什么放堆

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

符号引用和直接引用

String site="www.flydean.com"

上面的字符串“www.flydean.com” 可以看做是一个静态常量,因为它是不会变化的,是什么样的就展示什么样的。

而上面的字符串的名字“site”就是符号引用,需要在运行期间进行解析,为什么呢?

因为site的值是可以变化的,我们不能在第一时间确定其真正的值,需要在动态运行中进行解析,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

  1. 符号引用 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

  2. 直接引用 直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

直接引用可以有不同的实现方式:

  • 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄
  1. 结构体说明
  • String
    • 是从class中的CONSTANT_String_info结构体构建的:
          CONSTANT_String_info {
                    u1 tag;
                    u2 string_index;
                    }
    

tag就是结构体的标记,string_index是string在class常量池的index。

string_index对应的class常量池的内容是一个CONSTANT_Utf8_info结构体。

        CONSTANT_Utf8_info {
            u1 tag;
            u2 length;
            u1 bytes[length];
            }
  • 数字常量

    • 数字常量是从class文件中的CONSTANT_Integer_info, CONSTANT_Float_info, CONSTANT_Long_info和 CONSTANT_Double_info 构建的。
  • 符号引用(是从class中的constant_pool中构建的。)

    • 对class和interface的符号引用来自于CONSTANT_Class_info。

    • 对class和interface中字段的引用来自于CONSTANT_Fieldref_info。

    • class中方法的引用来自于CONSTANT_Methodref_info。

    • interface中方法的引用来自于CONSTANT_InterfaceMethodref_info。

    • 对方法句柄的引用来自于CONSTANT_MethodHandle_info。

    • 对方法类型的引用来自于CONSTANT_MethodType_info。

    • 对动态计算常量的符号引用来自于CONSTANT_MethodType_info。

    • 对动态计算的call site的引用来自于CONSTANT_InvokeDynamic_info。

变量,静态变量,常量,字面量

  1. 变量

有些数据在程序运行中可以变化或者被赋值,这称为变量。例如:

int a;

String b;

  1. 静态变量(类变量)

static修饰的变量

private static String str = "静态变量";

3 常量

java中是指以final关键字修饰的变量(C语言中是constant关键字),由于final的不可改变性,因此,final类变量的值在编译期间,就被确定了,因此被保存在类的常量池里面,然后在加载类的时候,复制进方法区的运行时常量池里面 ;final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用

final int a;

final String b;

  1. 字面量

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(natation)。

区分方法:等号右边的八种基本类型的值、字符串值、声明为final的常量的值。例如:

final int a = 10; //a为常量,10为字面量

string b = “hello world!”; // b 为变量,hello world!为字面量

静态变量和常量区别

  • 静态变量 (类变量):和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共事即使没有类实例时你也可以访问它。static修饰。准备阶段默认值初始化,初始化阶段显式赋值

  • 常量(全局常量):被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。static final修饰。

        /**
         * non-final的类变量
         */
        public class MethodAreaTest {
            public static void main(String[] args) {
                Order order = null;
                //虽然为null,但是可以运行,是静态变量和静态方法,只和类相关,不和实例相关
                order.hello();
                System.out.println(order.count);
            }
        }

        class Order {
            public static int count = 1;
            public static final int number = 2;


            public static void hello() {
                System.out.println("hello!");
            }
        }

输出结果:

hello!
1

常量编译期确定.png

clinit方法.png

结论: 静态变量可以通过对象调用,也可以通过类名调用。

静态变量、成员变量、局部变量存放位置?

静态变量(类变量)

  • 定义在类中、方法外,有关键字 static 修饰,有默认初始值。
  • 可以通过对象调用,也可以通过类名调用。
  • 生命周期与类共存亡。
  • 静态变量存放在堆区,引用在Class对象持有

成员变量(实例变量)

  • 定义在类中、方法外,有默认初始值。
  • 通过对象的引用来访问实例变量。
  • 随着对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中。

局部变量

  • 定义在方法中,或者方法的形参,没有初始化值。
  • 生命周期与方法共存亡。
  • 存放在栈中。局部的对象的引用所指对象在堆中的地址在存储在了栈中。
        /**
         * 结论:
         * 静态变量的引用对应的对象实体始终都存在堆空间
         *
         * jdk7:
         * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
         * jdk 8:
         * -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
         */
        public class StaticFieldTest {
            private static byte[] arr = new byte[1024 * 1024 * 100];//100MB

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

            }
        }

运行参数:

-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails

    [B@1b6d3586
    Heap
     PSYoungGen      total 59904K, used 4150K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
      eden space 51712K, 8% used [0x00000000fbd80000,0x00000000fc18dbf0,0x00000000ff000000)
      from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
      to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
     ParOldGen       total 136704K, used 102400K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
      object space 136704K, 74% used [0x00000000f3800000,0x00000000f9c00010,0x00000000fbd80000)
     Metaspace       used 3219K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

    Process finished with exit code 0

ParOldGen total 136704K, used 102400K就是arr对象存储的位置老年区。

结论:通过new 生成的静态变量始终都存在堆空间,arr和Class对象绑定在一起,Class对象也存在于堆中

Class对象

什么是Class对象

在类加载(Class Loading)的5个过程中,加载(Loading)的最终产物是一个 java.lang.Class 对象,它是我们访问方法区中的类元数据的外部接口,我们需要通过这个对象来访问类的字段、方法、运行时常量池等。

类是一类事物的描述,类包含了这类事物的信息,比如车这个类,包含了车的类型,用途,行为信息,可以在路上跑。鱼这个类,包含了鱼的种类,名称,行为信息,可以在水里游。

image.png

Class类就是用来描述类的信息的。Class也是一种类型,它专门用来描述类的特征。

image.png

从类的加载和对象创建来说。每写完一个类文件,首先会被编译成.class文件,然后在运行时,这个.class文件会被加载到jvm中,如果是第一次加载这个类,那么会 在Java堆内存中实例化一个java.lang.Class类的对象,用来访问方法区中的类数据。 当使用new关键字创建类的对象的时候,会首先去这个类对应的Class对象获取该类的信息,然后创建对象,所以可以将Class对象看做是类的模板,同一个类的对象创建都使用这个模板。类创建的对象可以有很多,但是模板只有一份,也就是说每个类对应的Class对象只有一个。

image.png

java文件被编译加载后创建Class对象,当这个java文件的类需要创建对象的时候,也就是使用new关键字创建对象的时候,会去获取那个已经被创建好的Class对象中的信息。这里要注意一个重点,获取Class对象信息的时候是运行时,只有在运行时才能通过Class获取类的信息。

获取Class的方法

  1. Class.forName("类名"); 通过类名字符串获取Class对象。

  2. 通过类的对象调用getClass() 获取该类型的Class对象

  3. 通过类型直接获取Class对象

image.png

使用Class方法

image.png

Class对象和类元信息区别

  • Class对象是存放在堆区的,不是方法区。
  • 类元信息才是存在方法区的(类元信息并不是类的Class对象!Class对象是加载的最终产品):类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)
  • 对象头的class pointer指针直接指向方法区中的类元信息。不是堆中Class对象。

参考相关文章:

hotpot java虚拟机Class对象是放在 方法区 还是堆中 ?

类加载与Class对象

在JVM加载类的时候会在堆内存创建一个该类的Class的实例对象,这个Class实例对象的作用是什么?

探究变量存储位置

《深入理解JAVA虚拟机 第三版》 4.3.1 例子:

        /**
         * 《深入理解Java虚拟机》中的案例:
         * staticObj、instanceObj、localObj存放在哪里?
         */
        public class StaticObjTest {
            static class Test {
                //静态变量、类变量
                static ObjectHolder staticObj = new ObjectHolder();
                //实例变量、成员变量
                ObjectHolder instanceObj = new ObjectHolder();

                void foo() {
                    //局部变量
                    ObjectHolder localObj = new ObjectHolder();
                    System.out.println("done");
                }
            }

            private static class ObjectHolder {
            }

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

存放位置.png

image.png image.png

结论:

  • 只要是new实例对象,必然会在Java堆中分配。
  • 静态变量引用随着Test的类型信息存放在Class对象后(Hotspot),class对象在堆中
  • 成员变量引用 随着Test的对象实例存放在Java堆
  • 方法变量存放栈帧局部变量表中

静态变量和Class对象存放区域的相关文章

java中的静态变量和Class对象究竟存放在哪个区域?

JDK 1.8 下的 java.lang.Class 对象和 static 成员变量在堆还是方法区?

总结:

《Java虚拟机规范》对于方法区该如何实现并未作出规定,不同虚拟机灵活把握。

  1. JDK7及以后版本的Hotspot虚拟机,把静态变量与Class对象放一起,存储与堆中,而不是元空间。
  2. 类加载时,.class文件被加载到内存中,并构建了一个Class对象,在堆中。
  3. 静态变量经过追踪,发现其和Class对象绑定在一起,存在于堆中(都说静态变量在方法区,只能说.class文件的信息都在方法区,所以静态变量的信息存在于方法区,静态变量本身并不在)
  4. 将静态变量、StringTable都从方法区移动到堆中,主要是想进行GC,方法区也能GC,但只能full GC,频率会很低。

举例子

    public class  PersonDemo
    {
        public static void main(String[] args) 
        {   //局部变量p和形参args都在main方法的栈帧中
            //new Person()对象在堆中分配空间
            Person p = new Person();
            //sum在栈中,new int[10]在堆中分配空间
            int[] sum = new int[10];
        }
    }
    class Person
    {   //实例变量name和age在堆(Heap)中分配空间
        private String name;
        private int age;
        //类变量(引用类型)name1和"cn"都在堆中  "cn"字符串常量池
        private static String name1 = "cn";
        //类变量(引用类型)name2在 在堆中; "cn" 字符串常量池
        //new String("cn")对象在堆(Heap)中分配空间
        private static String name2 = new String("cn");
        //num在堆中,new int[10]也在堆中
        private int[] num = new int[10];
        Person(String name,int age)
        {   
            //this及形参name、age在构造方法被调用时
            //会在构造方法的栈帧中开辟空间
            this.name = name;
            this.age = age;
        }
        //setName()方法在方法区中
        public void setName(String name)
        {
            this.name = name;
        }
        //speak()方法在方法区中
        public void speak()
        {
            System.out.println(this.name+"..."+this.age);
        }
        //showCountry()方法在方法区中
        public static void  showCountry()
        {
            System.out.println("country="+country);
        }
    }

image.png

常量池-运行时常量池-字符串常量池

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

虚拟机规范网址

从字节码理解方法区结构

-javap 反编译 image.png

  • 源码
        /**
         * 测试方法区的内部构成
         */
        public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable {
            //属性
            public int num = 10;
            private static String str = "测试方法的内部结构";

            //构造器
            //方法
            public void test1() {
                int count = 20;
                System.out.println("count = " + count);
            }

            public static int test2(int cal) {
                int result = 0;
                try {
                    int value = 30;
                    result = value / cal;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return result;
            }

            @Override
            public int compareTo(String o) {
                return 0;
            }
        }
  • 反编译:javap -v -p MethodInnerstrucTest.class

          Classfile /E:/gitee-project/JVMDemo/out/production/chapter09/com/atguigu/java/MethodInnerStrucTest.class
          Last modified 2021-8-8; size 1626 bytes
          MD5 checksum e1c1d3676c744536b4115807127aeb77
          Compiled from "MethodInnerStrucTest.java"
          //类型信息
        public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
          minor version: 0
          major version: 52
          flags: ACC_PUBLIC, ACC_SUPER
          //常量池
        Constant pool:
           #1 = Methodref          #18.#52        // java/lang/Object."<init>":()V
           #2 = Fieldref           #17.#53        // com/atguigu/java/MethodInnerStrucTest.num:I
           #3 = Fieldref           #54.#55        // java/lang/System.out:Ljava/io/PrintStream;
           #4 = Class              #56            // java/lang/StringBuilder
           #5 = Methodref          #4.#52         // java/lang/StringBuilder."<init>":()V
           #6 = String             #57            // count =
           #7 = Methodref          #4.#58         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
           #8 = Methodref          #4.#59         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
           #9 = Methodref          #4.#60         // java/lang/StringBuilder.toString:()Ljava/lang/String;
          #10 = Methodref          #61.#62        // java/io/PrintStream.println:(Ljava/lang/String;)V
          #11 = Class              #63            // java/lang/Exception
          #12 = Methodref          #11.#64        // java/lang/Exception.printStackTrace:()V
          #13 = Class              #65            // java/lang/String
          #14 = Methodref          #17.#66        // com/atguigu/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I
          #15 = String             #67            // 测试方法的内部结构
          #16 = Fieldref           #17.#68        // com/atguigu/java/MethodInnerStrucTest.str:Ljava/lang/String;
          #17 = Class              #69            // com/atguigu/java/MethodInnerStrucTest
          #18 = Class              #70            // java/lang/Object
          #19 = Class              #71            // java/lang/Comparable
          #20 = Class              #72            // java/io/Serializable
          #21 = Utf8               num
          #22 = Utf8               I
          #23 = Utf8               str
          #24 = Utf8               Ljava/lang/String;
          #25 = Utf8               <init>
          #26 = Utf8               ()V
          #27 = Utf8               Code
          #28 = Utf8               LineNumberTable
          #29 = Utf8               LocalVariableTable
          #30 = Utf8               this
          #31 = Utf8               Lcom/atguigu/java/MethodInnerStrucTest;
          #32 = Utf8               test1
          #33 = Utf8               count
          #34 = Utf8               test2
          #35 = Utf8               (I)I
          #36 = Utf8               value
          #37 = Utf8               e
          #38 = Utf8               Ljava/lang/Exception;
          #39 = Utf8               cal
          #40 = Utf8               result
          #41 = Utf8               StackMapTable
          #42 = Class              #63            // java/lang/Exception
          #43 = Utf8               compareTo
          #44 = Utf8               (Ljava/lang/String;)I
          #45 = Utf8               o
          #46 = Utf8               (Ljava/lang/Object;)I
          #47 = Utf8               <clinit>
          #48 = Utf8               Signature
          #49 = Utf8               Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
          #50 = Utf8               SourceFile
          #51 = Utf8               MethodInnerStrucTest.java
          #52 = NameAndType        #25:#26        // "<init>":()V
          #53 = NameAndType        #21:#22        // num:I
          #54 = Class              #73            // java/lang/System
          #55 = NameAndType        #74:#75        // out:Ljava/io/PrintStream;
          #56 = Utf8               java/lang/StringBuilder
          #57 = Utf8               count =
          #58 = NameAndType        #76:#77        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          #59 = NameAndType        #76:#78        // append:(I)Ljava/lang/StringBuilder;
          #60 = NameAndType        #79:#80        // toString:()Ljava/lang/String;
          #61 = Class              #81            // java/io/PrintStream
          #62 = NameAndType        #82:#83        // println:(Ljava/lang/String;)V
          #63 = Utf8               java/lang/Exception
          #64 = NameAndType        #84:#26        // printStackTrace:()V
          #65 = Utf8               java/lang/String
          #66 = NameAndType        #43:#44        // compareTo:(Ljava/lang/String;)I
          #67 = Utf8               测试方法的内部结构
          #68 = NameAndType        #23:#24        // str:Ljava/lang/String;
          #69 = Utf8               com/atguigu/java/MethodInnerStrucTest
          #70 = Utf8               java/lang/Object
          #71 = Utf8               java/lang/Comparable
          #72 = Utf8               java/io/Serializable
          #73 = Utf8               java/lang/System
          #74 = Utf8               out
          #75 = Utf8               Ljava/io/PrintStream;
          #76 = Utf8               append
          #77 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
          #78 = Utf8               (I)Ljava/lang/StringBuilder;
          #79 = Utf8               toString
          #80 = Utf8               ()Ljava/lang/String;
          #81 = Utf8               java/io/PrintStream
          #82 = Utf8               println
          #83 = Utf8               (Ljava/lang/String;)V
          #84 = Utf8               printStackTrace
        {
            //域信息
          public int num;
            descriptor: I
            flags: ACC_PUBLIC

          private static java.lang.String str;
            descriptor: Ljava/lang/String;
            flags: ACC_PRIVATE, ACC_STATIC
           //方法信息,这里是默认构造器(字节码角度也是方法)
          public com.atguigu.java.MethodInnerStrucTest();
            descriptor: ()V
            flags: ACC_PUBLIC
            Code:
              stack=2, locals=1, args_size=1
                 0: aload_0
                 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                 4: aload_0
                 5: bipush        10
                 7: putfield      #2                  // Field num:I
                10: return
              LineNumberTable:
                line 8: 0
                line 10: 4
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0      11     0  this   Lcom/atguigu/java/MethodInnerStrucTest;

          public void test1();
           //V 方法返回为void
            descriptor: ()V
            //访问权限
            flags: ACC_PUBLIC
            Code:
             //回顾栈篇章:栈深度3;局部变量表2;参数大小1 this
              stack=3, locals=2, args_size=1
                 0: bipush        20
                 2: istore_1
                 3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
                 6: new           #4                  // class java/lang/StringBuilder
                 9: dup
                10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
                13: ldc           #6                  // String count =
                15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
                18: iload_1
                19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
                22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
                25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
                28: return
              LineNumberTable:
                line 16: 0
                line 17: 3
                line 18: 28
                //局部变量表
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0      29     0  this   Lcom/atguigu/java/MethodInnerStrucTest;
                    3      26     1 count   I

          public static int test2(int);
            descriptor: (I)I
            flags: ACC_PUBLIC, ACC_STATIC
            Code:
              stack=2, locals=3, args_size=1
                 0: iconst_0
                 1: istore_1
                 2: bipush        30
                 4: istore_2
                 5: iload_2
                 6: iload_0
                 7: idiv
                 8: istore_1
                 9: goto          17
                12: astore_2
                13: aload_2
                14: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
                17: iload_1
                18: ireturn
                //异常表
              Exception table:
                 from    to  target type
                     2     9    12   Class java/lang/Exception
              LineNumberTable:
                line 21: 0
                line 23: 2
                line 24: 5
                line 27: 9
                line 25: 12
                line 26: 13
                line 28: 17
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    5       4     2 value   I
                   13       4     2     e   Ljava/lang/Exception;
                    0      19     0   cal   I
                    2      17     1 result   I
              StackMapTable: number_of_entries = 2
                frame_type = 255 /* full_frame */
                  offset_delta = 12
                  locals = [ int, int ]
                  stack = [ class java/lang/Exception ]
                frame_type = 4 /* same */

          public int compareTo(java.lang.String);
            descriptor: (Ljava/lang/String;)I
            flags: ACC_PUBLIC
            Code:
              stack=1, locals=2, args_size=2
                 0: iconst_0
                 1: ireturn
              LineNumberTable:
                line 33: 0
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0       2     0  this   Lcom/atguigu/java/MethodInnerStrucTest;
                    0       2     1     o   Ljava/lang/String;

          public int compareTo(java.lang.Object);
            descriptor: (Ljava/lang/Object;)I
            flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
            Code:
              stack=2, locals=2, args_size=2
                 0: aload_0
                 1: aload_1
                 2: checkcast     #13                 // class java/lang/String
                 5: invokevirtual #14                 // Method compareTo:(Ljava/lang/String;)I
                 8: ireturn
              LineNumberTable:
                line 8: 0
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0       9     0  this   Lcom/atguigu/java/MethodInnerStrucTest;

          static {};
            descriptor: ()V
            flags: ACC_STATIC
            Code:
              stack=1, locals=0, args_size=0
                 0: ldc           #15                 // String 测试方法的内部结构
                 2: putstatic     #16                 // Field str:Ljava/lang/String;
                 5: return
              LineNumberTable:
                line 11: 0
        }
        Signature: #49                          // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
        SourceFile: "MethodInnerStrucTest.java"

总结

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

  • class文件中有类型信息,域信息,方法信息(栈深度,局部变量表,异常表等)等信息。通过类加载子系统(前面篇章有介绍)加载信息到方法区。构成方法区结构。

image.png

在Class文件结构中,最头的4个字节用于 存储魔数 (Magic Number),用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储副版本号,后2个存储主版本号,再接着是用于存放常量的常量池,常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念。

常量池

常量池,也叫 Class 常量池(常量池==Class常量池)。Java文件被编译成 Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant pool),常量池是当Class文件被Java虚拟机加载进来后存放在方法区各种字面量和符号引用 。

特征:

  • 常量池是class文件的一部分
  • 用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
    • 字面量(字符串常量):
      • 如文本字符串
      • 声明为final的常量值
      • 基本数据类型的值等
    • 符号引用: 
      • 类和结构的完成限定名引用
      • 字段名称和描述符
      • 方法名称和描述符
  • 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

image.png

  1. 为什么需要常量池

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

比如:如下的代码:


        public class simpleClass {
            public void sayHel1o() {
                System.out. println("hello");
            }
         }

虽然只有514字节,但是里面却使用了String、System、 PrintStream及0bject等结构。这里代码量其实已经很小了。如果代码多,引用到的结构会更多!这里就需要常量池了!通过分解不同的常量。需要的时候再组合,可以大大减少方法区存储的代码量

常量池的好处:

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

  • 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

运行时常量池

运行时常量池是当Class文件被加载到内存后,Java虚拟机会将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个),其中的字符串转移到字符串常量池 ,运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中

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

字符串常量池

字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool或String table。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间。字符串常量池由String类私有的维护。

三池关系

image.png

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。常量池用于存放编译期生成的各种字面量和符号引用,而当类加载到内存中后,jvm就会将常量池中的内容存放到运行时常量池中,而字符串常量存在堆中字符串常量池中。

常量池:class文件中定义的常量池

运行时常量池:class文件中定义的常量池被加载到虚拟机构成运行时常量池。由字面量和符合引用构成。每个类对应一个运行时常量池。

字符串常量池(StringTable):专门存储字符串的常量池 ,字符串常量池存放的是字符串的引用或者字符串(两者都有),jvm只有一个,共享。

注意: String.intern的方法返回字符串对象的规范表示形式。其中它做的事情是:首先去判断该字符串是否在常量池中存在,如果存在返回常量池中的字符串(引用地址),如果在字符串常量池中不存在,先在字符串常量池中添加该字符串,然后返回引用地址

关于字符串常量池后面考虑出一篇章专门详细介绍!!

方法区垃圾回收

有些人认为方法区(如HotSpot虛拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》 对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的zGc收集器就不支持类卸载)。

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。 但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

“无用的类”判定条件

  1. 该类所有的实例已经被回收

  2. 加载该类的类加载已经被回收(这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、 JSP的重加载等,否则通常是很难达成的。)

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

Java虚拟机被允许对满足,上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看 类加载和卸载信息

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

类的卸载说明

  • 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
  • Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。由用户自定义的类加载器加载的类是可以被卸载的。
  • HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
  • 回收废弃常量与回收Java堆中的对象非常类似。

深入理解JVM系列