【JVM】 内存管理
读《深入理解 Java 虚拟机》的一些思考之对 JVM 内存管理的一些思考~
1. JVM 内存区域划分
1.1 程序计数器
- 平时 Java 开发并没有什么概念,它的主要作用就是用来记录「当前线程」运行的字节码行号;
- 每一个线程独占空间
1.2 虚拟机栈、本地方法栈(Stack)
无论是虚拟机栈、还是本地方法栈,都是栈空间,区别只是在于是用来记录本地方法产生的栈帧还是 Java 方法产生的栈帧;
和我们理解的栈类似,拥有先进后出的特性;每一个线程独占的空间
A. 栈帧
- 栈内元素称为「栈帧」,JVM 每执行一个方法,就会往栈内压入一个「栈帧」,栈帧内记录着方法执行需要的一些元素,例如局部变量表;局部变量表记录在编译期可以确定的一些变量,简单可以理解成 Java 方法内的临时变量;
- JVM 在执行方法的时候,就会往栈中压入一个栈帧,方法执行完成则将栈帧弹出;
- 局部变量表所占用的空间往往都是确定的,因为一个方法内的局部变量的数量总是确定的;
- 注意👀,局部变量表占用空间的大小是确认的,不代表其所占用的内存大小是固定,因为存在「递归」调用的情况,以及各种逻辑判断的存在,有些方法是不会执行~
1.3 堆(heap)
堆是 JVM 内存管理中最大的一块区域,Java 所有的对象实例数组都诞生于此,相较于程序计数器、栈为线程私有的空间,堆空间为所有线程共享的空间;堆空间的大小是 JVM 启动的时候就确定的,可以通过 Xms
和 Xmx
参数设定堆空间大小
一般 Xms 和 Xmx 都是设定的相同大小,即一开始就明确堆空间大小,避免在运行期间发生堆空间的伸缩,对服务性能造成影响。
堆空间是垃圾收集器的主要区域,因为占用空间最大,「垃圾」产生最多,所谓「垃圾」即方法在运行过程中创建出来的作用域仅在方法内的对象实例,在方法运行结束,该对象实例就已经没有用了,其所占用的内存空间应该会清理回收,以提供给到新的对象实例创建。
对象实例
每一个对象实例在堆空间所占用的空间,都有以下几部分组成
A. 对象头
存储的数据主要包含两部分:
- 运行时数据,例如锁信息、GC 年龄(超过一定年龄后进入老年代)、Hashcode
- 执行实例类信息的指针,类信息存储在方法区(后续会讲到)
B. 实际存储的数据
存储非静态(非 static)的实例变量相关值,主要是指基本类型(int、long、char等)和引用类型
C. 填充部分
为了提高CPU访问速度,JVM 可能会根据不同平台的要求进行字节对齐。
1.4 方法区(Method Area)
与堆空间一样,为所有线程共享的空间,主要是用来存储类型信息、常量、静态变量、运行时常量池等数据;简单来讲就是「类级别」的相关数据都在方法区中,实例级别的数据则存储在堆中。
- 类型信息:不同的类有不同的
Class
类,包含了这个类的所有元数据信息 - 常量:
final static
修饰的变量 - 静态变量:
static
修饰的变量 - 运行时常量池
运行时常量池
字符串常量池在 Java 7 之前是存储在永久代,在 Java 7 之后移到了堆空间中
经过编译后生成的 Class 文件除了包含类的版本、字段、方法、接口等描述信息,还包含一个静态常量池表,其中就存储在编译期间就能够确认的各种字面量和符号引用;在运行阶段会讲静态常量池的字面量加载到运行时常量池中,符号引用则是在经过链接、解析之后转化成直接引用,也会被加载到运行时常量池中。
A. 字面量
简单来理解就是编译期可以确定的「常量」值;不会随着运行变化的值
- 字符串字面量:如 “Hello, World!”。
- 数值字面量:如整数 42 或浮点数 3.14。
- 字符字面量:如 ‘A’。
- 布尔字面量:如 true 或 false。
- 类或接口的字面量:如使用 Class.forName(“java.lang.String”) 时,字符串 “java.lang.String” 就是一个类字面量。
B. 符号引用
符号引用简单来讲就是用来在编译期用来唯一标识类、接口、方法、字段;因为编译期还没有实际分配内存,所以用符号引用来「暂替」,在经过解析之后就会转化成实际指向内存的地址,称为「直接引用」,而直接引用也会被存储在运行时常量池中,直接进行使用。
- 类和接口的符号引用:描述了类或接口的完全限定名,例如 java/lang/String
- 字段的符号引用:包括字段所属的类或接口的符号引用、字段名称和字段描述符(即字段的类型签名)。
- 方法的符号引用:类似于字段,它包括方法所属的类或接口的符号引用、方法名称和方法描述符(即参数列表和返回类型)。