方法区(线程共享)

方法区(线程共享)

一、基本概念

1、概述

  • 方法区(Method Area)与 Java 堆一样,是线程共享的内存区域。
  • 方法区在 JVM 启动的时候被创建,关闭 JVM 就会释放这个区域的内存。
  • 方法区的大小,跟堆空间一样,可以选择 固定大小 或者 可扩展。
  • 在 jdk7 及以前,习惯上把方法区,称为永久代。jdk8 开始,使用元空间取代了永久代。
    • 元空间与永久代最大的区别在于:元空间不在虚拟机的内存中,而是使用本地内存
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出OOM:
    • jdk8之前:java.lang.OutOfMemoryError: PermGen space
    • jdk8开始:java.lang.OutOfMemoryError: Metaspace

2、方法区属于堆?

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

所以,对于 HotSpotJVM 而言,方法区看作是一块独立于 Java 堆的内存空间

3、栈、堆、方法区(关系)

在这里插入图片描述

4、设置方法区大小

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

1)JDK7及以前(永久代)

设置永久代的大小:

  • -XX:Permsize来设置永久代初始分配空间。默认值是20.75M
  • -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M

查看永久代的大小:

  • jinfo -flag PermSize 进程id(通过jps获取进程id)
  • jinfo -flag MaxPermsize 进程id(通过jps获取进程id)

2)JDK8及以后(元空间)

设置元空间的大小:

  • -XX:MetaspaceSize来设置元空间初始分配空间。默认值约为21M
  • -XX:MaxMetaspaceSize来设定元空间最大可分配空间。默认值是-1,即没有限制。

查看元空间的大小:

  • jinfo -flag MetaspaceSize 进程id(通过jps获取进程id)
  • jinfo -flag MaxMetaspaceSize 进程id(通过jps获取进程id)

3)元空间的设置建议

-XX:MetaspaceSize 默认值约为21M,这就是初始的高水位线,一旦触及这个水位线,将会触发 Full GC 并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。

  • 如果释放的空间不足,那么在不超过MaxMetaspaceSize的情况下,适当提高该值。
  • 如果释放空间过多,则适当降低该值。

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。

为了避免频繁地GC调高水位线,建议将-XX:MetaspaceSize设置为一个相对较高的值(保持默认值-1即可)。

5、方法区的OOM

永久代:当 JVM 加载的类信息容量超过了 MaxPermsize,就会报OutOfMemoryError:PermGen space异常。

元空间:MaxMetaspaceSize默认无限制,JVM耗尽所有的可用系统内存,才会报OutOfMemoryError:Metaspace异常

/**
 * 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_8, 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);
        }
    }
}

3331
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:757)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:636)
	at methodArea.OOMTest.main(OOMTest.java:20)

二、方法区的内部结构

在这里插入图片描述

方法区(Method Area)用于存储以下信息:

在这里插入图片描述

注意:静态变量只有 jdk1.7 之前存放在方法区,从 jdk1.7 开始存放在堆中(这里的静态变量指的是静态变量引用,而不是实例)

1、类型信息

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

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

2、域信息(Field)

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

  1. 域名称
  2. 域类型
  3. 域修饰符(public,private,protected,static,final,volatile,transient 的某个子集)

3、方法信息(Method)

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

  1. 方法名称

  2. 方法的返回类型(或 void)

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

  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)

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

  6. 异常表(abstract 和 native 方法除外)

    每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

4、运行时常量池

  • 方法区,内部包含了运行时常量池
  • 字节码文件,内部包含了常量池
  • 要弄清楚方法区的运行时常量池,先要理解清楚 字节码文件 中的常量池。

1)常量池

常量池(Constant Pool)是 class 文件的一部分,用于存放编译期生成的各种 字面量符号引用

  • 字面量:Java语言层面的常量,如:文本字符串、基础数据、声明为final的常量等
  • 符号引用:编译原理方面的常量,如:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的 [1, count-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
Constant pool:
   #1 = Methodref          #18.#54        // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#55        // methodArea/MethodAreaStructTest.num:I
   ...
  #17 = Class              #71            // methodArea/MethodAreaStructTest
  #18 = Class              #72            // java/lang/Object
  ...
  #21 = Utf8               num
  #22 = Utf8               I
  ...
  #28 = Utf8               <init>
  #29 = Utf8               ()V
  ...
  #54 = NameAndType        #28:#29        // "<init>":()V
  #55 = NameAndType        #21:#22        // num:I
  ...
  #71 = Utf8               methodArea/MethodAreaStructTest
  #72 = Utf8               java/lang/Object

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

2)运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,是常量池在运行期的一个表示形式。

  • 在加载类和接口到虚拟机后,就会创建对应的运行时常量池,并将常量池的内容加载到运行时常量池中。
  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址
  • 运行时常量池,相对于 Class 文件中常量池的另一重要特征是:具备动态性。( 例如String.intern()
  • 运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,也会造成OOM

3)整型常量池

整数常量池是运行时常量池的一部分,是运行时常量池中专门用于存储整型常量的部分。

为了提高程序的执行效率,将 [-128, 127] 之间256个整数所有的包装对象提前创建好了,放在了整数常量池中。

  • 范围在[-128, 127]内的整数,装箱成包装类时,底层不会new对象。共用在整数常量池当中的256个Integer对象。
public class IntConstantPool {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;

        Integer c = 128;
        Integer d = 128;

        System.out.println(a == b);  // true
        System.out.println(c == d);  // false
    }
}

4)为什么需要常量池?

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

比如下的代码:

public class SimpleClass {
    public void sayHello() {
        System.out.println("hello");
    }
}

虽然只有 194 字节,但是里面却使用了 String、System、PrintStream 及 Object 等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。

  • 常量池的作用:就是为了提供一些符号和常量,便于指令的识别
  • 在不同的方法,都可能调用常量或者方法,只需要存储一份,然后记录其引用即可,节省了空间。

5、案例分析

  • 要弄清楚方法区,需要理解清楚 ClassFile,因为方法区中的类信息是从 ClassFile 加载的。
  • 要弄清楚方法区的运行时常量池,需要理解清楚 ClassFile 中的常量池。
package methodArea;

import java.io.Serializable;

public class MethodAreaStructTest implements Comparable<String>, Serializable {

    public int num = 10;
    private static String str = "测试方法区的内部结构";
    private static final int count = 2;

    public void test1() {
        int count = 20;
        System.out.println("count = " + count);
    }

    public static int test2(int numerator, int denominator) {
        int result = 0;
        try {
            result = numerator / denominator;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

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

通过 javap -v -p MethodInnerStrucTest.class 反编译(这里就不放完整的了,截取其中相关部分说明)

0)常量池

字节码文件反编译的结果中,那些带#的,都是引用的常量池中的内容

Constant pool:
   #1 = Methodref          #18.#54        // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#55        // methodArea/MethodAreaStructTest.num:I
   #3 = Fieldref           #56.#57        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #58            // java/lang/StringBuilder
   #5 = Methodref          #4.#54         // java/lang/StringBuilder."<init>":()V
   #6 = String             #59            // count =
   #7 = Methodref          #4.#60         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#61         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#62         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #63.#64        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #65            // java/lang/Exception
  #12 = Methodref          #11.#66        // java/lang/Exception.printStackTrace:()V
  #13 = Class              #67            // java/lang/String
  #14 = Methodref          #17.#68        // methodArea/MethodAreaStructTest.compareTo:(Ljava/lang/String;)I
  #15 = String             #69            // 测试方法区的内部结构
  #16 = Fieldref           #17.#70        // methodArea/MethodAreaStructTest.str:Ljava/lang/String;
  #17 = Class              #71            // methodArea/MethodAreaStructTest
  #18 = Class              #72            // java/lang/Object
  #19 = Class              #73            // java/lang/Comparable
  #20 = Class              #74            // java/io/Serializable
  #21 = Utf8               num
  #22 = Utf8               I
  #23 = Utf8               str
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               count
  #26 = Utf8               ConstantValue
  #27 = Integer            2
  #28 = Utf8               <init>
  #29 = Utf8               ()V
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               LocalVariableTable
  #33 = Utf8               this
  #34 = Utf8               LmethodArea/MethodAreaStructTest;
  #35 = Utf8               test1
  #36 = Utf8               test2
  #37 = Utf8               (II)I
  #38 = Utf8               e
  #39 = Utf8               Ljava/lang/Exception;
  #40 = Utf8               numerator
  #41 = Utf8               denominator
  #42 = Utf8               result
  #43 = Utf8               StackMapTable
  #44 = Class              #65            // java/lang/Exception
  #45 = Utf8               compareTo
  #46 = Utf8               (Ljava/lang/String;)I
  #47 = Utf8               o
  #48 = Utf8               (Ljava/lang/Object;)I
  #49 = Utf8               <clinit>
  #50 = Utf8               Signature
  #51 = Utf8               Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
  #52 = Utf8               SourceFile
  #53 = Utf8               MethodAreaStructTest.java
  #54 = NameAndType        #28:#29        // "<init>":()V
  #55 = NameAndType        #21:#22        // num:I
  #56 = Class              #75            // java/lang/System
  #57 = NameAndType        #76:#77        // out:Ljava/io/PrintStream;
  #58 = Utf8               java/lang/StringBuilder
  #59 = Utf8               count =
  #60 = NameAndType        #78:#79        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #61 = NameAndType        #78:#80        // append:(I)Ljava/lang/StringBuilder;
  #62 = NameAndType        #81:#82        // toString:()Ljava/lang/String;
  #63 = Class              #83            // java/io/PrintStream
  #64 = NameAndType        #84:#85        // println:(Ljava/lang/String;)V
  #65 = Utf8               java/lang/Exception
  #66 = NameAndType        #86:#29        // printStackTrace:()V
  #67 = Utf8               java/lang/String
  #68 = NameAndType        #45:#46        // compareTo:(Ljava/lang/String;)I
  #69 = Utf8               测试方法区的内部结构
  #70 = NameAndType        #23:#24        // str:Ljava/lang/String;
  #71 = Utf8               methodArea/MethodAreaStructTest
  #72 = Utf8               java/lang/Object
  #73 = Utf8               java/lang/Comparable
  #74 = Utf8               java/io/Serializable
  #75 = Utf8               java/lang/System
  #76 = Utf8               out
  #77 = Utf8               Ljava/io/PrintStream;
  #78 = Utf8               append
  #79 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #80 = Utf8               (I)Ljava/lang/StringBuilder;
  #81 = Utf8               toString
  #82 = Utf8               ()Ljava/lang/String;
  #83 = Utf8               java/io/PrintStream
  #84 = Utf8               println
  #85 = Utf8               (Ljava/lang/String;)V
  #86 = Utf8               printStackTrace

1)类型信息

public class MethodAreaStructTest implements Comparable<String>, Serializable {}
public class methodArea.MethodAreaStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #17                         // methodArea/MethodAreaStructTest
  super_class: #18                        // java/lang/Object
  interfaces: 2, fields: 3, methods: 6, attributes: 2

注意:class文件是静态的,所以反编译看不到classLoader的信息,运行时方法区的类型信息中还会包含classLoader的信息:

  • 类的类型信息中,还会记录哪个类加载器加载了这个类。
  • 同时,类加载器的类也会加载到方法区,其类型信息中,也会记录这个加载器加载了哪些类。

2)域信息

    public int num = 10;
    private static String str = "测试方法区的内部结构";
    private static final int count = 2;
  public int num;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC

  private static java.lang.String str;
    descriptor: Ljava/lang/String;
    flags: (0x000a) ACC_PRIVATE, ACC_STATIC

  private static final int count;
    descriptor: I
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2

3)方法信息 - 构造器

代码中没有单独给出构造器,这里是默认的空参构造

  public methodArea.MethodAreaStructTest();
    descriptor: ()V
    flags: (0x0001) 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 5: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LmethodArea/MethodAreaStructTest;

4)方法信息 - 实例方法

public void test1() {
    int count = 20;
    System.out.println("count = " + count);
}
  public void test1();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      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 11: 0
        line 12: 3
        line 13: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   LmethodArea/MethodAreaStructTest;
            3      26     1 count   I
  • descriptor: ()V 表示方法返回值类型为 void
  • flags: ACC_PUBLIC 表示方法权限修饰符为 public
  • stack=3 表示操作数栈深度为 3
  • locals=2 表示局部变量个数为 2 个(实例方法包含 this)
  • args_size=1 表示方法参数个数为 1 个
    • test1() 方法虽然没有参数,但是其 args_size=1 ,这是因为非静态方法默认将 this 作为了参数

5)方法信息 - 静态方法

public static int test2(int numerator, int denominator) {
    int result = 0;
    try {
        result = numerator / denominator;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}
  public static int test2(int, int);
    descriptor: (II)I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_0
         3: iload_1
         4: idiv
         5: istore_2
         6: goto          14
         9: astore_3
        10: aload_3
        11: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
        14: iload_2
        15: ireturn
      Exception table:
         from    to  target type
             2     6     9   Class java/lang/Exception
      LineNumberTable:
        line 16: 0
        line 18: 2
        line 21: 6
        line 19: 9
        line 20: 10
        line 22: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10       4     3     e   Ljava/lang/Exception;
            0      16     0 numerator   I
            0      16     1 denominator   I
            2      14     2 result   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ int, int, int ]
          stack = [ class java/lang/Exception ]
        frame_type = 4 /* same */
  • args_size=2 表示方法参数个数为 2 个
    • 和实例方法不同的是,静态方法不会有 this 占一个参数个数
  • Exception table就是异常表
    • 2、6、9 对应的是字节码的行数,可以通过LineNumberTable查看对应代码的行数

6、类变量分析

1)non-final static

  1. 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
  2. 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null;
        order.hello();	// 这里不会抛NPE
        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

可以看到这里没有出现NPE,表明了 static 类型的字段和方法随着类的加载而加载,并不属于特定的类实例

2)final static

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

class Order {
    public static int count = 1;
    public static final int number = 2;
}    
public static int count;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public static final int number;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2	// 可以发现final修饰的static变量在编译的时候就已经确定值了 

三、方法区的结构演进

1、方法区的演进过程

首先明确:只有 Hotspot 才有永久代。对于 BEA JRockit、IBMJ9 等虚拟机来说,是不存在永久代的概念的。

Hotspot 中方法区的演进:

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

下面用几张图说明 Hotspot 中方法区的演进:

  • 主要的区别一个在于 永久代 --> 元空间;还有一个在于 静态变量字符串常量池 的位置变化

    注意:这里的静态变量指的是静态引用,静态引用对应的对象实体始终都存在堆空间中的。(不理解的话看第3小节)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2、字符串常量池的位置

JDK1.6 及之前,StringTable放在永久代;JDK1.7及之后,StringTable放在堆空间

字符串常量池 StringTable 为什么要调整位置?

因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。

这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。

放到堆里,能及时回收这块的内存。

3、静态变量放在哪里

  1. 静态引用对应的对象实体(也就是下面的new byte[1024 * 1024 * 100]始终都存在堆空间
  2. 只是那个变量(也就是下面的arr)在JDK6、JDK7、JDK8存放位置中有所变化

1)对象实体在哪里放着?

public class StaticFieldTest {
    private static byte[] arr = new byte[1024 * 1024 * 100]; // 100MB

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

JDK6 & JDK7-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails

在这里插入图片描述

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

在这里插入图片描述

2)变量本身存放在哪里?

/**
 * 《深入理解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();
    }
}

JDK6及之前:

  1. staticObj随着Test的类型信息存放在方法区
  2. instanceObj随着Test的对象实例存放在Java堆
  3. localObject则是存放在foo()方法栈帧的局部变量表中。

JDK7及之后:

  • 选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中

用JHSDB工具来进行分析(JDK9开始自带,在JDK9的bin目录下可以找到,JDK9以前没有)

在这里插入图片描述

  • 0x00007f32c7800000(Eden区的起始地址)~ 0x00007f32c7b50000(Eden区的终止地址)
  • 可以发现三个变量都在这个范围内

四、方法区的垃圾回收

有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。

《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)。

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

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

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

而要判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收。也就是 J a v a 堆中不存在该类及其任何派生子类的实例。 \color{red}{该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。} 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如 O S G i 、 J S P 的重加载等,否则通常是很难达成的。 \color{red}{加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。} 加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如OSGiJSP的重加载等,否则通常是很难达成的。
  3. 该类对应的 j a v a . l a n g . C l a s s 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 \color{red}{该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。} 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。

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

# 对Class类的引用
	jvm必须以某种方式把 Class实例 和存储在方法区中的 Class类型信息 联系起来。
	jvm会为每个加载的类型(包括类和接口)都创建一个java.lang.Class的实例。(在堆中)
	jvm需要获取堆中的Class对象,然后通过 Class对象 获得 相应类型存储在方法区中的Class类型信息。

在这里插入图片描述

关于是否要对类型进行回收,HotSpot 虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX:+TraceClassLoading-XX:+TraceClassUnLoading查看类加载和卸载信息

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

五、综合案例分析

下面我们通过一个案例来分析一下程序计数器、虚拟机栈、方法区的执行流程

package methodArea;

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);
    }
}
{
  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 5: 0
        line 6: 4
        line 7: 7
        line 8: 11
        line 9: 15
        line 10: 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
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

六、运行时数据区小结

在这里插入图片描述

从线程共享与否的角度来看运行时数据区

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

scj1022

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

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

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

打赏作者

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

抵扣说明:

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

余额充值