JVM运行时内存

整体结构

程序计数器

  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
  • 在JVM规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者如果是在执行native方法,则是未指定值(undifined)。
  • 它是程序控制流的指示器,分支/循环/跳转/异常处理/线程恢复等基础功能都需要依赖这个计数器完成。
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它时唯一一个在Java虚拟机规范中没有规定的任何OutOfMemoryError情况的区域

虚拟机方法栈

  • 局部变量表

    存储基本数据类型、对象引用、returnAddress类型。

    以变量槽存储,其中long和double类型占用两个变量槽,其他的均占一个。

    运行时,局部变量表大小不变

  • 操作数栈

    操作数栈(Operand Stack),每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为 表达式栈(Expression Stack)

  • 动态链接

  • 方法出口

本地方法栈

native方法

Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。它是 JVM 管理的最大一块内存空间,是线程共享的。

是否所有的实例对象均在堆上创建?

以下两种方式可以实现不在堆上创建。

  • 栈上分配

    JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

    逃逸分析的基本行为就是分析对象动态作用域:

    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如:作为调用参数传递到其他地方中。
  • 标量替换

    在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替,这个过程就是标量替换。

    举例:

    public static void main(String args[]) {
        alloc();
    }
    class Point {
        private int x;
        private int y;
    }
    private static void alloc() {
        Point point = new Point(1,2);
        System.out.println("point.x" + point.x + ";point.y" + point.y);
    }
    

    经过标量替换后,就会变成

    private static void alloc() {
        int x = 1;
        int y = 2;
        System.out.println("point.x = " + x + "; point.y=" + y);
    }
    

    当时这个个人理解为算一种内存占用的优化,因为,最终并没有创建Point对象,所以不能说标量替换导致对象不在堆上创建。

方法区

  • 类型信息

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

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

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

    域信息包括:

    • 域名称

    • 域类型

    域修饰符(public、private、protected、static、final、volatile、transient的某个子集)

  • 方法信息

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

    • 方法名称

    • 方法的返回类型(或void)

    • 方法参数的数量或者类型(按顺序)

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

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

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

  • 即时编译器编译后的代码缓存

面试题

变量的分类以及初始化时间

  • 按数据类型分:基本数据类型、引用数据类型。
  • 按类中声明的位置分:成员变量(类变量,实例变量)、局部变量。
    • 类变量:linking的preparetion阶段,给类变量默认赋值,init阶段给类变量显示赋值,即静态代码块。
    • 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值。
    • 局部变量:在使用前必须进行显式赋值,不然编译不通过。

为什么需要TLAB

TLAB:Thread Local Allocation Buffer,也就是为每个线程单独分配了一个缓冲区。

由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的,为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。因此,如果每个线程均有一个私有的缓冲区域,就可以再该区域快速创建出一个对象,而避免了繁重的加锁机制。

同时,由于TLAB的存在,对于堆空间来说,并不是全部是共享的了。

公众号:慢行的蜗牛

posted @ 2022-04-23 14:21  woniu4  阅读(70)  评论(0编辑  收藏  举报