1. 程序计数器
作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空),即用于保存JVM中下一条所要执行的指令的地址
原理:
- JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
- 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号
特点:
- 是线程私有的
- 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM (内存溢出) 的区域,所以这个空间不会进行 GC (垃圾回收)
2. 虚拟机栈
Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存
-
每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧)
-
Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的
-
虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:
- 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
- 动态链接:也叫指向运行时常量池的方法引用
- 方法返回地址:方法正常退出或者异常退出的定义
- 操作数栈或表达式栈和其他一些附加信息
虚拟机栈特点:
-
栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据
-
栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
-
方法内的局部变量是否线程安全:
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
异常:
- 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常
3. 堆
Heap 堆:是 JVM 内存中最大的一块,通过new关键字创建的对象都会被放在堆内存,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题
存放哪些资源:
- 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
- 字符串常量池:
- 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
- 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
- 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
- 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率
内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常
4. 本地方法栈
本地方法栈是为虚拟机执行本地方法时提供服务的
JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 ( 带有native关键字的方法 )
-
不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
-
虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
-
本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
-
当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 直接从本地内存的堆中分配任意数量的内存
- 可以直接使用本地处理器中的寄存器
5. 方法区
方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)
方法区是一个 JVM 规范,永久代(jdk1.8以前)与元空间(jdk1.8以后)都是其一种实现方式
方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)
方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现
为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中
类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表
常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池
- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等
- 符号引用:类、字段、方法、接口等的符号引用
运行时常量池是方法区的一部分
- 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
- 类在解析阶段将这些符号引用替换成直接引用
- 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()
- 常量池是.class文件中的,当该*类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6. 直接内存
直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。
除了堆内存可以存放对象数据以外,我们也可以申请堆外内存(直接内存) ,也就是不受JVM管控的内存区域,这部分区域的内存需要我们自行去申请和释放,实际上本质就是JVM通过C/C++调用malloc函数申请的内存,当然得我们自己去释放了。不过虽然是直接内存,不会受到堆内存容量限制,但是依然会受到本机最大内存的限制,所以还是有可能抛出OutOfMemoryError异常。这里我们需要提到一个堆外内存操作类: Unsafe ,就像它的名字一样,虽然Java提供堆外内存的操作类,但是实际上它是不安全的,只有你完全了解底层原理并且能够合理控制堆外内存,才能安全地使用堆外内存。注意这个类不让我们new,也没有直接获取方式。
- 属于操作系统,常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高,因为直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
- 直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放