抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

JVM 内存布局

内存机制

HotSpot 为主.

Object 角度

对象有自己的引用指针.

  1. (Heap)也称作为 GC Heap, 由 Java GC 管理.
  2. 实例数据在堆(Heap)中.
  3. 元数据(类的信息, 字段, 方法签名, 注解), 是在堆中还是堆外, 看jvm的实现, 像 HotSpot 在jdk1.7之后就已经移除 永久代 了, 方法区的实现用元空间代替了.
  4. 编译后的代码不在堆中.
  5. 常量也不放在堆中, 在常量池中, 常量池在方法区.
  6. 对象和操作系统的内核间通过内存映射构造成缓冲区. 数据->网卡->内核空间 的拷贝可以用 DMA(Direct Memory Access) 模块完成, 要继续使用的话还要拷贝到 用户空间. 解决方案: 在内核空间的数据不拷贝了, 给用户空间一个引用. 这种方式就叫做内存映射, 而这个数据明显是不在Heap中的!!!
  7. Native对象在用户空间, 不在JVM堆中, 也就是堆外内存.

Method AreaJVM的标准, 所有JVM实现都有, Hotspot 的实现是元空间

线程角度

C 编译器在划分内存区域的时候, 经常将管理的区域划分为数据段代码段. 数据段包括 堆, 栈 以及 静态数据区.

  • 线程私有 : Program Counter, JVM Stack, Native Method Stack
  • 所有线程共享 : MetaSpace, Heap

Program Counter Register

程序计数器

  • 当前线程所执行的字节码行号指示器(逻辑), 即在虚拟机字节码指令的地址.
  • 改变计数器的值来选取下一条需要执行的字节码指令(包括分支, 循环, 异常处理, 跳转, 线程恢复等).
  • 和线程是一对一的关系, 即 线程私有 .
  • 对于 Java 方法计数, 如果是 Native 方法, 则计数器值为 Undefined.
  • 不会发生内存泄漏.

逻辑计数器, 而非物理计数器.

JVM Stack

方法运行的基础结构, 由 栈帧 组成, 而 栈帧 包括 局部变量表, 操作栈, 动态链接, 返回地址.

  • Java 方法执行的内存模型
  • 每个方法执行时都会创建栈帧

局部变量表和操作数栈

  • 局部变量表 : 包含方法执行过程中的所有变量(包括this引用, 所有方法参数, 其它局部变量), 为操作数栈提供数据的支撑.
  • 操作数栈 : 入栈, 出栈, 复制, 交换, 产生消费临时变量. 在执行字节码指令的时候使用到.

举例子说明

package org.lgq.interview.jvm;
/**
 * @author DevLGQ
 * @version 1.0
 */
public class ByteCodeSample {
    public static int add(int a, int b){
        int c = 0;
        c = a + b;
        return c;
    }
}

使用 javap -v xxx.class 指令查看

Classfile /E:/WorkSpace/my-code-base/Java/Demo/javabasic/target/classes/org/lgq/interview/jvm/ByteCodeSample.class
  Last modified 2021年3月11日; size 467 bytes
  MD5 checksum dfabd22e79db0754d121317613546d70
  Compiled from "ByteCodeSample.java"
public class org.lgq.interview.jvm.ByteCodeSample
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // org/lgq/interview/jvm/ByteCodeSample
  super_class: #3                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
// 常量池的信息
Constant pool:
   #1 = Methodref          #3.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // org/lgq/interview/jvm/ByteCodeSample
   #3 = Class              #22            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lorg/lgq/interview/jvm/ByteCodeSample;
  #11 = Utf8               add
  #12 = Utf8               (II)I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               b
  #16 = Utf8               c
  #17 = Utf8               MethodParameters
  #18 = Utf8               SourceFile
  #19 = Utf8               ByteCodeSample.java
  #20 = NameAndType        #4:#5          // "<init>":()V
  #21 = Utf8               org/lgq/interview/jvm/ByteCodeSample
  #22 = Utf8               java/lang/Object
{
  public org.lgq.interview.jvm.ByteCodeSample();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/lgq/interview/jvm/ByteCodeSample;

  public static int add(int, int);
    // 对方法的描述 接收 2个int 返回值 int
    descriptor: (II)I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      // 操作数栈 深度是2 本地变量表 容量是3 入参是2个
      stack=2, locals=3, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_0
         3: iload_1
         4: iadd
         5: istore_2
         6: iload_2
         7: ireturn
      LineNumberTable:
        // 代码的9行, 对应字节码指令第0行 源码和JVM指令的映射关系
        line 9: 0
        line 10: 2
        line 11: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0     a   I
            0       8     1     b   I
            2       6     2     c   I
    MethodParameters:
      Name                           Flags
      a
      b
}
SourceFile: "ByteCodeSample.java"

执行 add(1, 2) 大致流程:

内存角度

package org.lgq.interview.jvm;

/**
 * @author DevLGQ
 * @version 1.0
 */
public class HelloWorld {
    private String name;
    public void sayHello(){
        System.out.println("Hello " + name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        int a = 1;
        HelloWorld hw = new HelloWorld();
        hw.setName("test");
        hw.sayHello();
    }
}

当类加载进来的时候

  • 元空间
    • Class : HelloWorld - Method : sayHello\setName\main - Field : name
    • Class : System
  • Java Heap
    • Object : String(“test”)
    • Object : HelloWord
  • 线程独占
    • Parameter reference : “test” to String Object
    • Variable reference : “hw” to HelloWorld Object
    • Local Variable : a with 1, lineNo(行号)

StackOverflowError 异常

方法每次调用, 都会创建对应的栈帧, 并压入JVM栈中, 当方法执行完毕就会出栈. 由此可知, 线程当前执行的方法所对应的栈帧必定位于JVM栈的顶部, 而递归不断调用函数(自身).

  • 第一, 每次调用一次方法, 就会创建栈帧;

  • 第二, 会保存当前方法的状态, 将它放在JVM栈中;

  • 第三, 栈帧切换上下文的时候, 会切换到最新的方法栈帧中. 由于每个线程的虚拟机栈大小是固定的, 递归实现会导致栈深度的增加, 每次递归, 都会往栈里压栈帧, 如果超出了最大允许的深度, 就会抛出java.lang.StackOverflowError异常了.

递归过深, 栈帧数超过了虚拟机栈深度.

虚拟机栈过多还会引发 java.lang.OutOfMemoryError 异常. 当虚拟机栈可以动态扩展时, 如果无法申请足够多的内存, 就会抛出此异常.

public void StackLeakByThread(){
    while(true){
        new Thread(() -> {while(true){}}).start();
    }
}

虚拟机栈也是JVM自动管理的. 类似集合, 但是它是有固定容量的, 是由栈帧组成的. 每调用一个方法, JVM就会自动在内存中分配对应的一块空间, 而这块空间就是栈帧, 而当方法调用结束后, 对应的栈帧就会被自动释放掉. 所以说, 栈的内存不需要GC来回收, 因为会自动释放. 可以使用 jstack 来查看当前线程的对应JVM的所有虚拟机栈描述, 包括每个线程的状态等.

Native Stack

与虚拟机栈相似, 主要是作用于标注了 native 的方法.

MetaSpace 与 PermGen

元空间(MetaSpace) 与 永久代(PermGen) 都是方法区的实现.

运行时常量池是在方法区分配的. Class 文件中的常量池(编译器生成的各种字面量符号引用)会在类加载后被放入这个区域.

jdk8的方法区是元空间, jdk7以前是放在永久代的. 而永久代和Heap相连的, 会给GC回收带来不必要的复杂性.

元空间存在于本地内存(Native Memory)中, 都是用来存储 class 信息的, 包括 methodfield. 这两个其实就是方法区的实现.

方法区 是 JVM 的一种规范, 抽象定义, 存储每一个类的结构信息. 而元空间是HotSpot的一种具体实现技术.

  • 元空间使用本地内存, 而永久代使用的是 jvm 的内存. 最直接的好处就是java.lang.OutOfMemoryError: PermGen space这个异常不存在了, 因为默认的元空间内存分配只受本地内存的影响, 但是也不会无限大, 会动态设置.

MateSpace 相比 PermGen 的优势

  • 字符串常量池存在永久代中, 容易出现性能问题和内存溢出问题.
  • 类和方法的信息大小难易确定, 给永久代的大小指定带来困难.
  • 永久代会为GC带来不必要的复杂性, 并且回收效率偏低. HotSpot中的各种GC都要特殊处理永久代. 分离出来后, 可以简化 Full GC 和 以后并发隔离元数据等方面进行优化.
  • 方便HotSpot与其他 JVM 如 Jrockit 的集成.

Java Heap

是被线程共享的内存区域, 在 JVM 启动的时候创建. JVM 规范中, Java Heap 可以处于物理内存上不连续的内存空间中. 只要逻辑上连续就可以了. 当前主流虚拟机都是按照可扩展来实现的, 可以通过 -Xmx, -Xms 进行设定. 如果堆中没有内存完成实例分配, 并且堆也无法扩展时, 将会抛出 java.lang.OutOfMemoryError 异常.

  • 对象实例(所有)的分配区域.
  • GC 管理的主要区域, 所以很多时候也被称为 GC 堆. 由于现在垃圾回收大多数都是使用分代收集算法, 所以Java堆中可分为 新生代(Eden, survivor) 和 老年代(tenured).

内存分配策略

  • 静态存储 : 编译时确定每个数据目标在运行时的存储空间需求. 要求程序代码中不允许有可变数据结构的存在, 也不允许有嵌套或者递归的结构出现, 因为都会导致编译时无法准确计算出存储空间.
  • 栈式存储 : 动态的, 数据区需求在编译时未知, 运行时模块入口前确定, 按照FILO的原则.
  • 堆式存储 : 动态分配, 编译时或运行时模块入口都无法确定, 比如, 可变长度串, 对象实例.

Java 内存模型中堆和栈的区别.

联系 : 引用对象, 数组时, 定义变量保存中目标的首地址, 就是说栈中的变量就是数组对象引用变量, 就可以使用该引用变量来访问堆中的数据了.

引用变量, 定义时在栈中分配, 在运行时到其作用域之外之后就会被释放掉, 而数组对象在堆中分配, 即使程序运行到使用new产生数组或对象语句所在的代码块之外, 数组和对象本身占据的内存不会被释放掉, 在没有引用变量指向的时候才会变成垃圾, 在随后的不确定时间被垃圾回收器回收。

  • 管理方式 : 栈自动释放, 堆需要 GC.
  • 空间大小 : 栈比堆小.
  • 碎片相关 : 栈产生的碎片远小于堆.
  • 分配方式 : 栈支持静态(由编译器分配好)和动态分配, 而堆仅支持动态分配.
  • 效率 : 栈的效率比堆高(内存的结构本来就是一个栈结构, 使栈空间与底层结构更加符合, 而且操作简单, 出栈和入栈), 堆空间最大优点在于动态分配.

JMM

Java 内存模型JMM(Java Memory Model)本身就是一个抽象的概念, 并不是真实存在的, 它描述了一组规范或规则, 通过这组规范定义了程序中各个变量(包括实例字段, 静态字段和构成数组对象的元素)的访问方式.

JVM 运行程序的实体是线程, 而每个线程创建时 JVM 都会为其创建工作内存, 用来存储线程私有的数据. JMM 中规定, 所有变量都存储在主内存中. 主内存是共享内存区域, 所有线程都可以访问. 对变量的操作必须在工作内存中进行, 工作内存中存在着共享变量副本.

JMM 中的主内存

  • 存储Java实例对象
  • 包括成员变量, 类信息, 常量, 静态变量
  • 属于共享数据的区域, 多线程并发操作时会引起线程安全问题

JMM 中的工作内存

  • 存储当前方法的所有本地变量信息, 本地变量对其它线程不可见
  • 字节码行号指示器, Native 方法信息
  • 属于线程私有区域, 不存在线程安全问题

JMM 与 Java 内存区域划分是不同概念层次

  • JMM 描述的是一组规则, 围绕原子性, 有序性, 可见性展开
  • 相似点 : 存在共享区域私有区域

主内存与工作内存的数据存储类型以及操作方式归纳

  • 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
  • 引用类型的本地变量, 引用存储工作内存中, 实例存储主内存
  • 成员变量, static变量, 类信息均会被存储在主内存
  • 主内存共享的方式是线程各拷贝一份数据到工作内存, 操作完成后刷新回主内存

评论