企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 第一节、Java运行时数据区 **注意!!!** JVM运行时的数据区域和JVM的内存模型不一样,这两个概念别搞混了!!! JVM运行时的数据区域主要包括:程序计数器、虚拟机栈、堆、方法区、直接内存、本地方法栈。 ## 一、程序计数器 :-: ![image-20211213191728163](https://s2.loli.net/2021/12/30/noc7Qe4SBNhtgmL.png) JVM为每个线程维护了一个独立的程序计数器,因此程序计数器属于线程私有的,在线程之间不共享。 程序计数器有如下两个特性: 1. 在该区域中不会出现内存溢出错误。 2. 线程私有。 程序计数器举例: 对java的Class文件使用反汇编命令 ~~~  javap -v xxx.class ~~~ 可以得到类似下面图片的内容,程序计数器可以简单看成指向下一条要执行指令的地址编号,与操作系统的程序计数器寄存器(PC)的作用一样,都是保存下一条运行指令的地址。 :-: ![](https://img.kancloud.cn/57/01/57017d5ff80482bef06e82e4b6e165d0_1344x519.png) &nbsp; ## 二、虚拟机栈 虚拟机栈,简称栈,是JVM运行程序的关键内存模型。与虚拟机栈相关的两个概念为: * 栈:线程运行时所需的内存空间,每个线程运行时都有一个属于自己的虚拟机栈,一个线程默认分配的空间为1M。 * 栈帧:栈中的存储单元,运行一个方法所需要的内存空间,栈帧里面包含`方法参数、局部变量、返回值、返回地址等。`每一个方法的被调用到执行完毕对应着一个栈帧入栈到出栈的过程。 ![](https://img.kancloud.cn/dd/15/dd15fdd5c14db9bda1d7ec9a3d073da3_462x583.png) 虚拟机栈特点: * 每个栈(Stack)由栈帧(Stack Frame)组成,每个栈帧对应着线程中每次方法调用时所需的内存。 * 每个线程同一时刻只有一个活动的栈帧,即栈顶元素,对应正在执行代码的方法。 * 栈是线程私有的。 * 有可能会导致栈溢出错误:StackOverFlowError。 :-: ![](https://img.kancloud.cn/61/4a/614a3a653223bf9d376df62d85753e17_387x593.png) ❤️可以使用IDEA的Debug模式查看栈和栈帧的内容: ![image-20211115202740961](https://s2.loli.net/2021/12/30/FYXdqRxcI8TMJ5C.png) 在虚拟机栈中存在个**局部变量表**的结构,该结构存放了编译期可知的各种基本参数(boolean、type、char、int、float、long、double)、对象引用和returnAddress类型。同时这些类型在局部变量表中存储空间以槽(Slot)来表示,局部变量表所需要的内存空间在编译期的时候就完全确定了,因此每个方法在运行时能给栈帧分配的局部变量表的槽位的数量也是完全确定的,不可以再改变。 **与虚拟机栈的相关问题❓️** 1. 垃圾回收是否会涉及虚拟机栈的区域? 答: 不会涉及!如果简单的对JVM运行时数据区域进行划分的话可以划分为堆和栈结构(这也是传统的C/C++的划分方式),而垃圾回收器作用的位置主要位于堆区域,对栈结构没有影响。 另外一方面,虚拟机栈对应的一个线程运行所需要的临时空间,其栈元素栈帧(Stack Frame)的入栈和出栈对应着一次方法的调用和结束,因此会自动释放所占用的空间,而不用垃圾回收器进行回收。 2. 栈内存分配越大越好吗? 答: 需要看实际情况进行调优分配,例如在多线程的场景下应该尽可能的减少每个虚拟机栈的大小,但是同时也要确保其不会出现“StackOverFlowError”的错误。可以使用虚拟机参数`-Xss=size`进行修改。 &nbsp; 补充:可能会出现“StackOverFlowError”错误的场景! `对象转化为Json格式数据的时候出现递归转化!` ~~~  @Data  class Emp{      private String name;      @JsonIgnore // 加上这个注解,不然会递归转化      private Dept dept;      }  ​  @Data  class dept {      private String name;      private List<Emp> emps;  } ~~~ 3. 方法内的局部变量是否是线程安全的? 答:如果局部变量逃离了方法的作用范围,则需要考虑线程安全的问题,否则不需要考虑。 ![](https://img.kancloud.cn/72/1d/721df3cb0302a8415563ddb70b50477f_941x841.png) &nbsp; **线程诊断🧮** 案例1:CPU被占用过多! 1. 先定位 * 使用top命令定位哪个进程对cpu占用的资源比较高。 ~~~  top ~~~ ![image-20211116230232481](https://s2.loli.net/2021/12/30/fW98iJKaA5g2QB3.png) * 使用PS命令进一步定位该进程中哪个线程对cpu占用过高。 ~~~  ps -H -eo pid,tid,%cpu | grep pid ~~~ 2. 使用JDK自带工具jstack分析详细的线程信息 ~~~  jstack pid ~~~ 案例2:线程发生死锁 仍然使用jstack命令,如果发生死锁的话会有死锁的提示。 &nbsp; ### 2.1 运行时栈帧 参考:《深入理解Java虚拟机》第八章。 Java虚拟机把方法作为最基本的执行单元,“栈帧”就是支持JVM进行方法调用的数据结构,主要包括如下几部分的内容:`局部变量表`、`操作数栈`、`动态连接`、`方法返回值地址`等。 #### 2.1.1 局部变量表 局部变量表是方法内一组变量值的存储空间(可以看成一个数组),用变量槽来表示变量的存储单位(可能是32位也可能是64位),局部变量表在编译阶段就可以确定出来了,该值会被记录到Code属性的max\_locals项中。 局部变量表中的会保存方法参数、对象的this引用和方法内部的局部变量。局部变量表的第0号索引就表示调用该方法对象的“this”引用。局部变量表也是作为GC Roots的一部分,如果在局部变量表的槽位中仍然有对某一个变量的引用,则垃圾回收作用不到,经常的做法是会将该变量手动的设置为null,这样局部变量表就没有对于该变量的引用了,垃圾回收器也就可以起作用。 > 备注:周志明老师并不推荐这种做法,具体可以查看《深入理解Java虚拟机》第298页的内容。 #### 2.1.2 操作数栈 操作数栈与局部变量表一样,其最大深度在编译阶段即可确定出来,每一个元素是32位字节。操作数栈与精简指令集中的栈起到的作用类似,用于代码的执行过程中变量的存取。 &nbsp; ## 三、堆 Java堆是虚拟机管理的最大的一块内存空间。《Java虚拟机规范》对堆的描述是:“所有的对象实例以及数组都应当在堆上分配。”但是现在随着逃逸分析、栈上分配、标量替换等优化手段的发展,对象实例都分配在堆上开始变得不那么绝对了。同时值得一提的是在有些描述中说:“堆可以划分为新生代、老年代、永久代...”等等这些描述是和特定的垃圾回收器相关的概念,对堆进行细分只是为了更好的进行垃圾回收;但是有些垃圾回收器这可能就没有这些概念,《Java虚拟机规范》并没有对堆进行进一步的划分。 堆内存有如下两个特点: 1. 堆内存对于所有线程都共享,因此存放在堆中的对象实例需要考虑线程安全及线程可见性问题。 2. 堆内存是垃圾回收器主要作用的地方。 与堆大小分配相关的两个虚拟机参数为: ~~~  -Xmx:堆的最大大小  -Xms:初始化堆的大小 ~~~ 堆内存溢出: 当源源不断的有对象产生,并且产生的对象还在使用(垃圾回收器无法作用),或者一次分配的对象过大导致堆内存放不下的时候可能会出现堆内存溢出:`OutOfMemoryError` &nbsp; ### 对象在堆中的创建过程 :-: ![](https://img.kancloud.cn/d2/68/d2682d888c9da659ffa1643c8afaf0a2_961x656.png) &nbsp; **对象的内存布局** * 对象头 * 实例数据:填充顺序为double/long、int、short/char、bytes/boolean、ordinary object pointers; * 对齐填充:对象大小为8字节的整数倍。 &nbsp; **对象的访问方式** * 句柄方式 ![](https://img.kancloud.cn/63/6c/636cb0496bf5e0e78bf2ffc950f41c95_925x576.png) * 直接访问方式:HotSpot用这种方式更多! ![](https://img.kancloud.cn/ac/43/ac4301725a10dba8d44dc93d3a754941_858x591.png) &nbsp; ## 四、方法区 方法区与Java堆一样是一块线程共享的区域,在《Java虚拟机规范》中把方法区描述成堆的一个逻辑部分,但是还是得将方法区和堆分开来。方法区在虚拟机启动的时候被创建! 方法区中保存的数据有:`被虚拟机加载的类型信息`、`常量`、`静态常量`、`JIT编译后的代码缓存`。 HotSpot虚拟机在JDK1.8之前将方法区称之为“永久代”,这个名称与堆中的“新生代”,“老年代”的称呼一样,只是HotSpot的垃圾回收器的分代设计的沿用,在其他虚拟机实现中并没有这个称呼。而在JDK1.8及其之后的版本,弃用了“永久代”这个概念,将方法区称为“元空间(Meta-space)”,同时元空间使用的是本地内存。 ![](https://img.kancloud.cn/97/7e/977e9438be0e5b66ee3db64806e4a860_1522x519.png) 方法区特点: 1. 线程共享。 2. 使用本地内存(HotSpot在1.8版本之后)。 3. 也可能出现内存溢出错误。 **方法区内存溢出举例 -> 1.6版本** ~~~  public class Demo3 extends ClassLoader{      public static void main(String[] args) {          int i = 0;          try {              Demo3 test = new Demo3();              for (int j = 0; j < 10000; j++, i++) {                  // 生成类的二进制字节码                  ClassWriter classWriter = new ClassWriter(0);                  // 参数:版本号,作用域,类名,包名,父类,接口                  classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + j, null, "java/lang/Object", null);                  byte[] code = classWriter.toByteArray();                  // 只执行了类的加载                  test.defineClass("Class" + j, code, 0, code.length);             }         } finally {              System.out.println(i);         }     }  } ~~~ 由于1.8版本是使用本地内存空间,和本机的虚存管理有关,一般不会出现方法区的内存溢出,测试时可以配置大小。 ~~~  方法区大小配置参数:  `-XX:MaxMetaspaceSize=10m`  ​  1.8以前的配置参数为:  `-XX:MaxPermSize=10m` ~~~ **方法区可能出现的问题** Spring和Mybatis等框架底层使用了cglib这种基于字节码的技术进行类的动态代理生成,可能就会导致方法区的OutOfMemoryError。 &nbsp; ## 五、直接内存 直接内存(Direct Memory)是操作系统管理的内存,实际上不属于运行时的数据区域。在JDK1.4版本之后引入了NIO类,这是一种基于通道与缓冲区的IO方式,NIO类可以使用Native库函数直接分配堆外内存,然后通过存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用。这样可以提高一些性能,避免在Java堆和Native堆之间进行数据的复制。 特点: * 不被JVM规范所限制,由操作系统管理。 * 分配回收成本高,但是读写性能高,采用零拷贝的方式减少数据的复制。 * 会出现`OutOfMemoryError`错误。 实现原理: 1. Java底层的Unsafe类用来分配和释放直接内存,NIO中的ByteBuffer对象会和Unsafe对象相关联。 2. 虚拟机中有个Cleaner类,表示虚引用对象,当关联的对象被回收时,调用Unsafe类中的方法清空直接内存分配的区域。 :-: ![](https://img.kancloud.cn/3c/d7/3cd7988b2817613236089b2124549364_729x412.png) Linux每个进程都有代码段、数据区、堆、栈等布局,受内存管理单元统一管理。JVM中的堆即在进程中的堆(linux heap)分配一部分空间,由JVM再进一步管理,而这里的直接内存指的是在当前进程中堆(linux heap)的一部分。 &nbsp; ## 六、本地方法栈 本地方法栈是给本地方法接口提供运行时所需要的空间,本地方法接口一般都会使用native关键字修饰,具体的源码得去查看openJDK。本地方法一般都是由C/C++编写。