关于java:第-9-章-方法区

27次阅读

共计 18721 个字符,预计需要花费 47 分钟才能阅读完成。

第 9 章 办法区

微信搜一搜: 全栈小刘,获取文章全套 pdf 版

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

从内存构造来看

这次所讲述的是运行时数据区的最初一个局部

从线程共享与否的角度来看

ThreadLocal:如何保障多个线程在并发环境下的安全性?典型利用就是数据库连贯治理,以及独立会话治理

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

上面就波及了对象的拜访定位

  1. Person 类的 .class 信息寄存在办法区中
  2. person 变量寄存在 Java 栈的局部变量表中
  3. 真正的 person 对象寄存在 Java 堆中
  4. 在 person 对象中,有个指针指向办法区中的 person 类型数据,表明这个 person 对象是用办法区中的 Person 类 new 进去的

2、办法区的了解

官网文档

https://docs.oracle.com/javas…

2.1、办法去的地位

办法区的地位

  1. 《Java 虚拟机标准》中明确阐明:只管所有的办法区在逻辑上是属于堆的一部分,但一些简略的实现可能不会抉择去进行垃圾收集或者进行压缩。
  2. 但对于 HotSpotJVM 而言,办法区还有一个别名叫做 Non-Heap(非堆),目标就是要和堆离开。
  3. 所以,办法区能够看作是一块独立于 Java 堆的内存空间

2.2、办法区的了解

办法区的根本了解

办法区次要寄存的是 Class,而堆中次要寄存的是实例化的对象

  1. 办法区(Method Area)与 Java 堆一样,是 各个线程共享的内存区域
  2. 多个线程同时加载对立个类时,只能有一个线程能加载该类,其余线程只能等期待该线程加载结束,而后间接应用该类,即 类只能加载一次
  3. 办法区在 JVM 启动的时候被创立,并且它的理论的物理内存空间中和 Java 堆区一样都能够是不间断的。
  4. 办法区的大小,跟堆空间一样,能够抉择 固定大小或者可扩大
  5. 办法区的大小决定了零碎能够保留多少个类,如果零碎定义了太多的类,导致办法区溢出,虚拟机同样会抛出内存溢出谬误:

    • java.lang.OutofMemoryError:PermGen space
    • 或者
    • java.lang.OutOfMemoryError:Metaspace
  6. 举例说明办法区 OOM

    • 加载大量的第三方的 jar 包
    • Tomcat 部署的工程过多(30~50 个)
    • 大量动静的生成反射类
  7. 敞开 JVM 就会开释这个区域的内存。

代码举例

  • 代码
public class EdenSurvivorTest {public static void main(String[] args) {System.out.println("我只是来打个酱油~");
        try {Thread.sleep(1000000);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}
  • 简略的程序,加载了好多类

2.3、办法区演进过程

Hotspot 办法区的演进过程

  1. 在 JDK7 及以前,习惯上把办法区,称为永恒代。JDK8 开始,应用元空间取代了永恒代 。JDK 1.8 后,元空间寄存在 堆外内存中
  2. 咱们能够将办法区类比为 Java 中的接口,将永恒代或元空间类比为 Java 中具体的实现类
  3. 实质上,办法区和永恒代并不等价。仅是对 Hotspot 而言的能够看作等价。《Java 虚拟机标准》对如何实现办法区,不做对立要求。例如:BEAJRockit / IBM J9 中不存在永恒代的概念。
  4. 当初来看,当年应用永恒代,不是好的 idea。导致 Java 程序更容易 OOm(超过 -XX:MaxPermsize 下限)
  5. 而到了 JDK8,终于齐全废除了永恒代的概念,改用与 JRockit、J9 一样在本地内存中实现的元空间(Metaspace)来代替
  6. 元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代最大的区别在于:元空间不在虚拟机设置的内存中,而是应用本地内存
  7. 永恒代、元空间二者并不只是名字变了,内部结构也调整了
  8. 依据《Java 虚拟机标准》的规定,如果办法区无奈满足新的内存调配需要时,将抛出 OOM 异样

3、设置办法区大小与 OOM

办法区的大小不用是固定的,JVM 能够依据利用的须要动静调整。

3.1、JDK7 永恒代

JDK7 之前版本设置永恒代大小

  1. 通过 -XX:Permsize 来设置永恒代初始调配空间。默认值是 20.75M
  2. -XX:MaxPermsize 来设定永恒代最大可调配空间。32 位机器默认是 64M,64 位机器模式是 82M
  3. 当 JVM 加载的类信息容量超过了这个值,会报异样 OutofMemoryError:PermGen space。

3.2、JDK8 元空间

JDK8 版本设置元空间大小

  1. 元数据区大小能够应用参数 -XX:MetaspaceSize-XX:MaxMetaspaceSize 指定
  2. 默认值依赖于平台,Windows 下,-XX:MetaspaceSize 约为 21M,-XX:MaxMetaspaceSize 的值是 -1,即没有限度
  3. 与永恒代不同,如果不指定大小,默认状况下,虚构机会耗尽所有的可用零碎内存。如果元数据区产生溢出,虚拟机一样会抛出异样 OutOfMemoryError:Metaspace
  4. -XX:MetaspaceSize:设置初始的元空间大小。对于一个 64 位 的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize 值为 21MB。这就是初始的高水位线,一旦涉及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),而后这个高水位线将会重置。新的高水位线的值取决于 GC 后开释了多少元空间。

    • 如果开释的空间有余,那么在不超过 MaxMetaspaceSize 时,适当进步该值。
    • 如果开释空间过多,则适当升高该值。
  5. 如果初始化的高水位线设置过低,上述高水位线调整状况会产生很屡次。通过垃圾回收器的日志能够察看到 Full GC 屡次调用。为了防止频繁地 GC,倡议将 -XX:MetaspaceSize 设置为一个绝对较高的值

配置元空间大小示例

  • 代码

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

        System.out.println("end...");
    }
}
  • JVM 参数
-XX:MetaspaceSize=100m  -XX:MaxMetaspaceSize=100m
  • CMD 命令查看设置的元空间大小
C:\Users\Heygo>jps
C:\Users\Heygo>jinfo -flag MetaspaceSize pId
C:\Users\Heygo>jinfo -flag MaxMetaspaceSize pId

3.3、办法区 OOM

办法区 OOM 举例

  • 代码:OOMTest 类继承 ClassLoader 类,取得 defineClass() 办法,可本人进行类的加载

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 = new ClassWriter(0);

                classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);

                byte[] code = classWriter.toByteArray();

                test.defineClass("Class" + i, code, 0, code.length);
                j++;
            }
        } finally {System.out.println(j);
        }
    }
}

不设置元空间的下限

  • 应用默认的 JVM 参数,元空间不设置下限
10000

设置元空间的下限

  • JVM 参数
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
  • 元空间呈现 OOM
com.atguigu.java.OOMTest
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at com.atguigu.java.OOMTest.main(OOMTest.java:29)
8531

3.4、解决 OOM

如何解决 OOM?

  1. 要解决 OOM 异样或 heap space 的异样,个别的伎俩是首先通过内存映像剖析工具(如 Ec1ipse Memory Analyzer)对 dump 进去的堆转储快照进行剖析,重点是确认内存中的对象是否是必要的,也就是要先分分明到底是呈现了内存透露(Memory Leak)还是内存溢出(Memory Overflow)
  2. 内存透露就是有大量的援用指向某些对象,然而这些对象当前不会应用了,然而因为它们还和 GC ROOT 有关联,所以导致当前这些对象也不会被回收,这就是内存透露的问题
  3. 如果是内存透露,可进一步通过工具查看透露对象到 GC Roots 的援用链。于是就能 找到透露对象是通过怎么的门路与 GC Roots 相关联并导致垃圾收集器无奈主动回收它们的。把握了透露对象的类型信息,以及 GC Roots 援用链的信息,就能够比拟精确地定位出透露代码的地位。
  4. 如果不存在内存透露,换句话说就是内存中的对象的确都还必须存活着,那就该当查看虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存比照看是否还能够调大,从代码上查看是否存在某些对象生命周期过长、持有状态工夫过长的状况,尝试缩小程序运行期的内存耗费。

4、办法区的内部结构

4.1、办法区构造

办法区(Method Area)存储什么?

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

类型信息

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

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

域(Field)信息

  1. JVM 必须在办法区中保留类型的所有域的相干信息以及域的申明程序。
  2. 域的相干信息包含:

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

办法(Method)信息

JVM 必须保留所有办法的以下信息,同域信息一样包含申明程序:

  1. 办法名称
  2. 办法的返回类型(包含 void 返回类型),void 在 Java 中对应的类为 void.class
  3. 办法参数的数量和类型(按程序)
  4. 办法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
  5. 办法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 办法除外)
  6. 异样表(abstract 和 native 办法除外),异样表记录每个异样解决的开始地位、完结地位、代码解决在程序计数器中的偏移地址、被捕捉的异样类的常量池索引

代码示例

  • 代码

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;}
}
  • 反编译字节码文件,并输入值文本文件中,便于查看
  • 参数 -p 确保能查看 private 权限类型的字段或办法
javap -v -p MethodInnerStrucTest.class > Text.txt

类型信息

  • 插句嘴:在运行时办法区中,类信息中记录了哪个加载器加载了该类,同时类加载器也记录了它加载了哪些类
  • 从反编译文件能够看出,字节码文件记录了 MethodInnerStrucTest 继承了哪些类,实现了哪些办法
public class com.atguigu.java.MethodInnerStrucTest
    extends java.lang.Object
    implements java.lang.Comparable<java.lang.string>, java.io.Serializable
</java.lang.string>

域信息

  1. descriptor: I 示意字段类型为 Integer
  2. flags: ACC_PUBLIC 示意字段权限修饰符为 public
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

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

办法信息

  1. descriptor: ()V 示意办法返回值类型为 void
  2. flags: ACC_PUBLIC 示意办法权限修饰符为 public
  3. stack=3 示意操作数栈深度为 3
  4. locals=2 示意局部变量个数为 2 个(实力办法蕴含 this)
  5. test1() 办法尽管没有参数,然而其 args_size=1,这时因为将 this 作为了参数
  public void test1();
    descriptor: ()V
    flags: 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 17: 0
        line 18: 3
        line 19: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcom/atguigu/java/MethodInnerStrucTest;
            3      26     1 count   I
</init>

4.2、域信息非凡状况

non-final 类型的类变量

  1. 动态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
  2. 类变量被类的所有实例共享,即便没有类实例时,你也能够拜访它

代码示例

  1. 如下代码所示,即便咱们把 order 设置为 null,也不会呈现空指针异样
  2. 这更加表明了 static 类型的字段和办法随着类的加载而加载,并不属于特定的类实例

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 final int number = 2;

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

hello!

1

全局常量:static final

  1. 全局常量就是应用 static final 进行润饰
  2. 被申明为 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 final int number = 2;

    public static void hello() {System.out.println("hello!");
    }
}
  • 反编译,查看字节码指令,能够发现 number 的值曾经写死在字节码文件中了
  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

4.3、运行时常量池

运行时常量池 VS 常量池

官网文档

https://docs.oracle.com/javas…

  1. 办法区,外部蕴含了运行时常量池
  2. 字节码文件,外部蕴含了常量池
  3. 要弄清楚办法区,须要了解分明 ClassFile,因为加载类的信息都在办法区。
  4. 要弄清楚办法区的运行时常量池,须要了解分明 ClassFile 中的常量池。

常量池

  1. 一个无效的字节码文件中除了蕴含类的版本信息、字段、办法以及接口等描述符信息外
  2. 还蕴含一项信息就是 常量池表 Constant Pool Table),包含 各种字面量和对类型、域和办法的符号援用

为什么须要常量池?

  1. 一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码须要数据反对,通常这种数据会很大以至于不能间接存到字节码里,换另一种形式,能够存到常量池
  2. 这个字节码蕴含了指向常量池的援用。在动静链接的时候会用到运行时常量池,之前有介绍

比方:如下的代码:

public class SimpleClass {public void sayHello() {System.out.println("hello");
    }
}
  1. 尽管上述代码只有 194 字节,然而外面却应用了 String、System、PrintStream 及 Object 等构造。
  2. 如果不应用常量池,就须要将用到的类信息、办法信息等记录在以后的字节码文件中,造成文件臃肿
  3. 所以咱们将所需用到的构造信息记录在常量池中,并通过 援用的形式,来加载、调用所需的构造
  4. 这里的代码量其实很少了,如果代码多的话,援用的构造将会更多,这里就须要用到常量池了。

常量池中有啥?

  1. 数量值
  2. 字符串值
  3. 类援用
  4. 字段援用
  5. 办法援用

常量池代码举例

  • 代码

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;}
}
  • 来看下最简略的 test1() 办法,带 # 的字节码指令,就应用了常量池的援用
  • 通过字节码指令能够看出,* 拼接字符串时,编译器帮咱们造了个 StringBuilder 对象,而后调用其 append() 办法实现了字符串的拼接
  public void test1();
    descriptor: ()V
    flags: 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 20: 0
        line 21: 3
        line 22: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcom/atguigu/java/MethodInnerStrucTest;
            3      26     1 count   I

</init>
  • 常量池
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            // &#x6D4B;&#x8BD5;&#x65B9;&#x6CD5;&#x7684;&#x5185;&#x90E8;&#x7ED3;&#x6784;
  #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               &#x6D4B;&#x8BD5;&#x65B9;&#x6CD5;&#x7684;&#x5185;&#x90E8;&#x7ED3;&#x6784;
  #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
</init></ljava></clinit></init></init></init>

常量池总结

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

运行时常量池

  1. 运行时常量池(Runtime Constant Pool)是办法区的一部分
  2. 常量池表(Constant Pool Table)是 Class 字节码文件的一部分,用于寄存编译期生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中
  3. 运行时常量池,在加载类和接口到虚拟机后,就会创立对应的运行时常量池。
  4. JVM 为每个已加载的类型(类或接口)都保护一个常量池。池中的数据项像数组项一样,是通过索引拜访的
  5. 运行时常量池中蕴含多种不同的常量,包含编译期就曾经明确的数值字面量,也包含到运行期解析后才可能取得的办法或者字段援用。此时不再是常量池中的符号地址了,这里换为实在地址
  6. 运行时常量池,绝对于 Class 文件常量池的另一重要特色是:具备动态性。
  7. 运行时常量池相似于传统编程语言中的符号表(symbol table),然而它所蕴含的数据却比符号表要更加丰盛一些。
  8. 当创立类或接口的运行时常量池时,如果结构运行时常量池所需的内存空间超过了办法区所能提供的最大值,则 JVM 会抛 OutofMemoryError 异样。

5、办法区的应用举例

办法区图解示例

  • 代码

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);
    }
}

图解字节码指令执行流程

  • 字节码执行过程展现:初始状态

  • 首先将操作数 500 压入操作数栈中

  • 而后操作数 500 从操作数栈中取出,存储到局部变量表中索引为 1 的地位

  • 而后操作数 100 从操作数栈中取出,存储到局部变量表中索引为 2 的地位

  • 将操作数 100 从

  • 读取本地变量 1,压入操作数栈

  • 读取本地变量 2,压入操作数栈

  • 两数相除,计算结果放在操作数栈顶,之后执行 istore_3 指令,将计算结果从操作数栈中弹出,存入局部变量 3 中

  • 将操作数 50 压入操作数栈

  • 将操作数 50 从栈顶弹出,保留在局部变量 4 中

  • 获取 System.out 输入流的援用(我不太确定)

  • 将本地变量 3 的值取出,压入操作数栈中,筹备进行加法运算

  • 执行加法运算后,将计算结果放在操作数栈顶

  • 调用静态方法 println(),输入加法后果

  • main() 办法执行完结

对于【符号援用 –> 间接饮用】的了解

  1. 下面代码调用 System.out.println() 办法时,首先须要看看 System 类有没有加载,再看看 PrintStream 类有没有加载
  2. 如果没有加载,则执行加载,执行时,将常量池中的符号援用(字面量)转换为间接援用(真正的地址值)

对于程序计数器的阐明

程序计数器始终计算的都是以后代码运行的地位,目标是为了不便记录办法调用后可能失常返回,或者是进行了 CPU 切换后,也能回来到原来的代码进行执行。

6、办法区演进细节

6.1、永恒代演进过程

对于永恒代的阐明

  1. 首先明确:只有 Hotspot 才有永恒代。
  2. BEA JRockit、IBMJ9 等来说,是不存在永恒代的概念的。原则上如何实现办法区属于虚拟机实现细节,不受《Java 虚拟机标准》管教,并不要求对立
  3. Hotspot 中办法区的变动:

JDK 版本演变细节 JDK1.6 及以前有永恒代(permanent generation),动态变量存储在永恒代上 JDK1.7 有永恒代,但曾经逐渐 “ 去永恒代 ”,
字符串常量池,动态变量移除,保留在堆中

JDK1.8 无永恒代,类型信息,字段,办法,常量保留在本地内存的元空间,但字符串常量池、动态变量依然在堆中。

JDK6

办法区由永恒代实现,应用 JVM 虚拟机内存

JDK7

办法区由永恒代实现,应用 JVM 虚拟机内存

JDK8

办法区由元空间实现,应用物理机本地内存

6.2、元空间呈现起因

永恒代为什么要被元空间代替?

官网文档

http://openjdk.java.net/jeps/122

  1. 官网的牵强解释:JRockit 是和 HotSpot 交融后的后果,因为 JRockit 没有永恒代,所以他们不须要配置永恒代
  2. 随着 Java8 的到来,HotSpot VM 中再也见不到永恒代了。然而这并不意味着类的元数据信息也隐没了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。

因为类的元数据调配在本地内存中,元空间的最大可调配空间就是零碎可用内存空间,这项改变是很有必要的,起因有:

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

    • 在某些场景下,如果动静加载类过多,容易产生 Perm 区的 OOM。比方某个理论 Web 工

程中,因为性能点比拟多,在运行过程中,要一直动静加载很多类,经常出现致命谬误。Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space

  • 而元空间和永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存

因而,默认状况下,元空间的大小仅受本地内存限度

  1. 对永恒代进行调优是很艰难的。

    • 办法区的垃圾收集次要回收两局部内容:常量池中废除的常量和不再用的类型,办法区的调优次要是为了升高 Full GC
    • 有些人认为办法区(如 HotSpot 虚拟机中的元空间或者永恒代)是没有垃圾收集行为的,其实不然。《Java 虚拟机标准》对办法区的束缚是十分宽松的,提到过能够不要求虚拟机在办法区中实现垃圾收集。事实上也的确有未实现或未能残缺实现办法区类型卸载的收集器存在(如 JDK11 期间的 ZGC 收集器就不反对类卸载)。
    • 一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄。然而这部分区域的回收有时又的确是必要的。以前 Sun 公司的 Bug 列表中,曾呈现过的若干个重大的 Bug 就是因为低版本的 HotSpot 虚拟机对此区域未齐全回收而导致内存透露

6.3、字符串常量池

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

  1. JDK7 中将 StringTable 放到了堆空间中。因为永恒代的回收效率很低,在 Full GC 的时候才会执行永恒代的垃圾回收,而 Full GC 是老年代的空间有余、永恒代有余时才会触发。
  2. 这就 导致 StringTable 回收效率不高 ,而咱们开发中会有大量的字符串被创立,回收效率低,导致永恒代内存不足。 放到堆里,能及时回收内存

6.4、动态变量地位

动态变量寄存在那里?

代码示例 1

  • 代码

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

    public static void main(String[] args) {System.out.println(StaticFieldTest.arr);
    }
}
  • 运行环境 JDK8,JVM 参数
-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
  • 通过 GC 日志能够看出:动态变量援用对应的对象实体始终都存在堆空间(arr 数组对象间接怼到老年区去了)
[B@4554617c
Heap
 PSYoungGen      total 59904K, used 5171K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 10% used [0x00000000fbd80000,0x00000000fc28ceb0,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 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

代码示例 2

  • 代码

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();}
}
  • 能够应用 JHSDB.exe,在 JDK9 的时候才引入的
  • 剖析:staticObj 随着 Test 的类型信息寄存在办法区,instanceObj 随着 Test 的对象实例寄存在 Java 堆,localObject 则是寄存在 foo()办法栈帧的局部变量表中。
  • 测试发现:三个对象的数据在内存中的地址都落在 Eden 区范畴内,所以论断:只有是对象实例必然会在 Java 堆中调配

  • 接着,找到了一个援用该 staticObj 对象的中央,是在一个 java.lang.Class 的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,能够分明看到这的确是一个 java.lang.Class 类型的对象实例,外面有一个名为 staticobj 的实例字段:

  • 从《Java 虚拟机标准》所定义的概念模型来看,所有 Class 相干的信息都应该寄存在办法区之中,但办法区该如何实现,《Java 虚拟机标准》并未做出规定,这就成了一件容许不同虚拟机本人灵便把握的事件。JDK7 及其当前版本的 HotSpot 虚拟机抉择把动态变量与类型在 Java 语言一端的映射 Class 对象寄存在一起,存储于 Java 堆之中,从咱们的试验中也明确验证了这一点

7、办法区的垃圾回收

办法区垃圾收集

  1. 有些人认为办法区(如 Hotspot 虚拟机中的元空间或者永恒代)是没有垃圾收集行为的,其实不然。
  2. 《Java 虚拟机标准》对办法区的束缚是十分宽松的,提到过能够不要求虚拟机在办法区中实现垃圾收集。事实上也的确有未实现或未能残缺实现办法区类型卸载的收集器存在(如 JDK11 期间的 ZGC 收集器就不反对类卸载)。
  3. 一般来说这个区域的回收成果比拟难令人满意,尤其是类型的卸载,条件相当刻薄。然而这部分区域的回收有时又的确是必要的。以前 sun 公司的 Bug 列表中,曾呈现过的若干个重大的 Bug 就是因为低版本的 HotSpot 虚拟机对此区域未齐全回收而导致内存透露。
  4. 办法区的垃圾收集次要回收两局部内容:常量池中废除的常量和不再应用的类型

办法区常量的回收

  1. 先来说说办法区内常量池之中次要寄存的两大类常量:字面量和符号援用

    • 字面量比拟靠近 Java 语言档次的常量概念,如文本字符串、被申明为 final 的常量值等
    • 而符号援用则属于编译原理方面的概念,包含上面三类常量:

      • 类和接口的全限定名
      • 字段的名称和描述符
      • 办法的名称和描述符
  2. HotSpot 虚拟机对常量池的回收策略是很明确的,只有常量池中的常量没有被任何中央援用,就能够被回收。
  3. 回收废除常量与回收 Java 堆中的对象十分相似。(对于常量的回收比较简单,重点是类的回收)

办法区类的回收

断定一个常量是否 ” 废除 ” 还是绝对简略,而要断定一个类型是否属于 ” 不再被应用的类 ” 的条件就比拟刻薄了。须要同时满足上面三个条件:

  1. 该类所有的实例都曾经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器曾经被回收,这个条件除非是通过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
  3. 该类对应的 java.lang.Class 对象没有在任何中央被援用,无奈在任何中央通过反射拜访该类的办法。

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

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

8、运行时数据区总结

  • 线程公有构造:程序计数器、虚拟机栈、本地办法栈
  • 每个虚拟机栈由由具体的栈帧组成,在栈帧的动静链接中,保留至对办法的援用
  • 办法区在 JDK7 之前,应用永恒代实现,在 JDK8 之后,应用元空间实现
  • Minor GC 针对于新生区,Major GC 针对于老年区,Full GC 针对于整个堆空间和办法区

9、大局面试题

  1. 百度

    • 三面:说一下 JVM 内存模型吧,有哪些区?别离干什么的?
  2. 蚂蚁金服:

    • Java8 的内存分代改良
    • JVM 内存分哪几个区,每个区的作用是什么?
    • 一面:JVM 内存散布 / 内存构造?栈和堆的区别?堆的构造?为什么两个 survivor 区?
    • 二面:Eden 和 survior 的比例调配
  3. 小米:

    • jvm 内存分区,为什么要有新生代和老年代
  4. 字节跳动:

    • 二面:Java 的内存分区
    • 二面:讲讲 vm 运行时数据库区
    • 什么时候对象会进入老年代?
  5. 京东:

    • JVM 的内存构造,Eden 和 Survivor 比例。
    • JVM 内存为什么要分成新生代,老年代,长久代。新生代中为什么要分为 Eden 和 survivor。
  6. 天猫:

    • 一面:Jvm 内存模型以及分区,须要具体到每个区放什么。
    • 一面:JVM 的内存模型,Java8 做了什么改
  7. 拼多多:

    • JVM 内存分哪几个区,每个区的作用是什么?
  8. 美团:

    • java 内存调配
    • jvm 的永恒代中会产生垃圾回收吗?
    • 一面:jvm 内存分区,为什么要有新生代和老年代?

你只管学习,我来负责记笔记???? 关注公众号!, 更多笔记,等你来拿,谢谢



正文完
 0