运行时数据区

运行时数据区

  • JVM 运行期间使用的内存区域,官方叫法是运行时数据区,也有叫 JVM 内存模型或 JVM 内存结构
  • JMM 是 java 内存模型,不要搞混了,JMM 是规范多线程操作内存的行为

JVM 把使用的内存分成了 5 部分:

  • 程序计数器:下一条要执行的字节码指令的行号。线程私有
  • 虚拟机栈(线程栈):当前线程使用的局部变量、方法调用链等。线程私有
  • 本地方法栈:同虚拟机栈,native 方法使用。线程私有
  • 方法区:存放类的信息,比如一个类的类型是什么、有哪些方法、有哪些变量。线程共享
  • 堆:最大的一块内存空间,内部又划分为新生代(伊甸园和两个幸存区)、老年代,垃圾回收的主要区域。线程共享

程序计数器

线程私有,存储下一条字节码指令的行号

  1. 也叫 PC 寄存器,存储下一条要执行的指令行号

    • 前端编译器把 java 编译成 class 文件,后端编译器把 class 文件编译成字节码指令,字节码指令是 CPU 可以执行的内容
    • CPU 是交替执行多个线程的(上下文切换),每次切换线程前需要记录下一次要执行的指令行号,下一次接着这个行号继续执行
    • 还有条件判断、循环等操作,下一条指令是哪个条件,或者是否跳出循环等,都需要记录在线程内部
  2. 生命周期跟随线程的创建和结束

  3. JVM 中唯一一个不会出现 OOM 的区域

虚拟机栈

线程私有,存放栈帧,一个栈帧就是一个方法,栈帧里面主要存储当前方法需要操作的局部变量和操作数栈

  1. 在线程增加一个方法调用叫做栈帧入栈,调用完一个方法,这个方法就出栈

  2. 既然栈帧对应方法调用,那么栈帧里面存储的就是方法调用的相关信息

    • 局部变量表:存储方法形参和定义在方法体内部的局部变量
      • 如果是非静态方法,会有个 this 变量;非静态方法是没有的,所以静态方法中不能使用 this 关键字
      • 不会赋默认值(成员变量才会),所以方法里面定义一个未赋值的变量,在使用的时候 idea 会提示"变量未初始化"
      • 这里的对象会作为 GC Root 对象
    • 操作数栈:对局部变量进行操作的行为列表,比如读取、运算、比较、修改等操作
      • 比如修改局部变量a = a+1,操作数栈存放的内容就是:读取a、a+1、a修改为a+1
      • 每个操作都会有对应的命令:load、push、store 等
    • 动态链接:比如一个 User 类,对于 JVM 来说这只是一个符号,JVM 要找到这个符号对应的 Class 类,这个指向关系就是动态链接
    • 方法返回地址
  3. 运行速度很快,但是空间不大,jdk5 之前一个线程默认大小是 256k,jdk5 之后默认 1M(可自定义)

    • 为什么快?内部会使用 CPU 缓存(不是全都是 CPU 缓存,是部分)
    • 当内存大小超标时也会发生内存不足:StackOverflow
  4. 虽然线程私有但还是可能线程不安全,因为栈里的变量可能是一个对象,而对象可能是共享的

本地方法栈

和虚拟机栈类似,不过栈帧是 native 方法运行中使用的内容

方法区

线程共享,存放类信息,比如一个类的类型是什么、有哪些方法、有哪些变量

  1. JDK1.8 之前叫永久代,1.8 及之后叫元空间
    • 永久代使用堆内存,元空间使用直接内存
    • 方法区只是定义,定义大致为:存放类信息的地方。永久代存在堆中的,元空间存放在本地内存中
  2. 垃圾回收效率最低,回收效率从快到慢依次为:新生代(堆) > 老年代(堆) > 方法区
// User:方法区
// user:栈
// new 出来的对象保存在堆中
new Thread(() -> {
		User user = new User();
}).start();

一个jvm实例只有一个堆,所有线程共享;jvm 管理的最大的一块内存,垃圾回收的主要区域

  1. 几乎所有对象都是放在堆中的(不是所有)

    • JVM为每个线程在Eden区分配一小块私有内存(TLAB),默认大小约为Eden区的1%(可通过-XX:TLABSize调整)
    • 当 new 出来的对象使用的内存空间 <= TLAB 剩余空间,直接栈上分配(哪怕对象逃逸了,只要 TLAB 空间足够,对象也不会存放在堆中)
    • 标量替换发生时对象也不会存放在堆上(实际对象都不会创建了)
  2. 常量池存放在方法区,运行时常量池存放在堆中,【常量池】和【运行时常量池】两者别搞混了

    • 常量池是用来描述类信息的,所以放在方法区
    • 运行时常量池就是字符串字面量
    • 静态变量也是放在堆中的
  3. 对在内存结构上又分为老年代、新生代

    • 新生代又分为伊甸园区和幸存区
    • 幸存区又平均分成两块,幸存一区、幸存二区
    • 默认老年代和新生代内存大小比例为 2:1
    • 默认伊甸园和两个幸存区的内存大小比例为 8:1:1
  4. 如果是超大对象,可能直接分配在老年代,具体多大的对象视为超大对象这个是可以配置的

对象两种逃逸的方式

  1. 方法逃逸:对象作为方法返回值或传递给其他方法
  2. 线程逃逸:对象被赋值给类变量或可以被其他线程访问的实例变量
// 未逃逸:对象在方法中创建和使用,没有 return 出去(其他地方不会使用到)
void processOrder() {
    Order temp = new Order();  // 创建
    temp.addItem(item1);
    calculateTax(temp);        // 使用
}

// 方法逃逸:虽然在访达内部创建,但是 return 出去了,外部还会继续使用
Order createOrder() {
    Order order = new Order();
    return order;
}

// 线程逃逸:方法内部创建,但是赋值给了类的静态变量,其他线程能够使用到
class Shared {
    static Order sharedOrder;
    void saveOrder() {
        sharedOrder = new Order();
    }
}

标量替换

使用基本类型替换对象创建

// 原始代码
void foo() {
    Point p = new Point(1, 2);
    System.out.println(p.x + p.y);
}

// 优化后(标量替换)
void foo() {
    int x = 1, y = 2;  // 直接使用基本类型
    System.out.println(x + y);
}
posted @ 2024-10-11 12:51  CyrusHuang  阅读(43)  评论(0)    收藏  举报