JVM内存结构
按照Java虚拟机规范的规定, JVM自动管理的内存将包括以下几个运行时的数据区域:
下面分别对几个数据区域进行说明:
1.程序计数器
程序计数器是JVM中一块较小的内存区域, 保存着当前线程执行的虚拟机字节码指令的内存地址.
Java多线程的实现, 其实是通过线程间的轮流切换并分配处理器执行时间的方式实现的, 在任何时刻, 处理器都只会执行一个线程的指令. 在多线程场景下, 为了保证线程切换回来后, 还能恢复到原来的状态, 找到原先执行的指令, 所以每个线程都会设立一个程序计数器, 并且各个线程之间互不影响, 程序计数器为"线程私有"的内存区域.
若当前线程正在执行的是Java方法, 则程序计数器保存的是虚拟机字节码的内存地址, 若正在执行的Native方法(非Java方法), 则程序计数器为空.
程序计数器是唯一一个在Java规范中没有规定任何OutOfMemory(内存耗尽)场景的区域
2.虚拟机栈
虚拟机栈和线程是紧密联系的, 每创建一个线程就会对应创建一个Java栈, 生命周期和线程相同, 所有虚拟机栈也是"线程私有"的内存区域.
这个栈中对应多个栈帧, 每调用一个方法就会往栈中创建并压入一个栈帧, 栈帧是用来存储方法数据和部分过程结果的数据结构, 每一个方法从调用到最终返回结果的过程, 就对应一个栈帧从入栈到出栈的过程.
线程运行过程中, 只有一个栈帧处于活跃状态, 被称为"当前活动帧栈", 当前活动帧栈始终是虚拟机栈的栈顶元素.
在Java虚拟机规范中, 对这个区域规定了两种异常情况:
- 若线程请求的栈深度太深, 超出了虚拟机所允许的深度, 就会出现StackOverFlowError(比如无限递归)
- 虚拟机栈可以动态扩展, 若扩展到无法申请足够的内存空间, 就会出现OOM
3.本地方法栈
本地方法栈和虚拟机栈的作用相似, 只是虚拟机栈是为Java方法服务的, 而本地方法栈是为Native方法服务的.
4.方法区
方法区是用来存储类结构信息(包括常量池、静态变量、构造函数等)的地方, 类型信息是由类加载器在类加载是从类文件中提取出来的
方法区存在着垃圾回收, 因为用户通过自定义加载器加载的一些类同样会成为垃圾, JVM会回收一个未被引用类所占的空间, 以使方法区的空间最小
方法区是线程共享的.
5.堆
堆是存储java实例或者对象的地方, 是GC的主要区域, 同样也是线程共享的内存区域.
下面举几个例子来说明:
上面的main方法中运行过程如下:
- 用户创建了一个Student对象, 运行时JVM首先到方法区寻找该对象的类型信息, 没有则使用类加载器将Student.class字节码文件加载至6内存中的方法区, 并将Student类的类型信息存放至方法区
- 然后JVM在堆中为新的Student实例分配内存空间, 这个实例持有着指向方法区的Student类型信息的引用(类型信息在方法区中的内存地址)
- 在运行的JVM进程中, 会首先运行一个线程执行此用户程序, 而创建线程的同时也创建了一个虚拟机栈, 虚拟机栈用来跟踪线程运行中的一系列方法调用过程, 每调用一个方法就会创建一个栈帧并压入栈中. 上面的stu是对Student的引用, 存放在栈中, 指向堆中Student实例的内存地址.
- JVM根据stu引用持有的堆中对象地址, 定位到堆中的Student实例, 由堆中实例指向的方法区Student类型信息引用, 获得add()方法的字节码信息, 然后就可以执行add()方法的指令了
总结
一、方法区、堆是线程共享的。 虚拟机栈、本地方法栈和程序计数器是线程私有的
二、栈中:
- 每个线程有一个虚拟机栈, 栈中保存基础数据类型的对象和自定义对象的引用, 对象实例存放在堆区中
- 方法的形式参数, 方法调用玩后从栈空间收回
- 引用对象的地址, 引用完后, 栈空间地址立即被回收, 堆空间等待GC
三、堆中:
- 存储的全是对象实例, 每个对象包含一个与之对象的class信息, 存放在方法区
- JVM只有一个堆区, 被所有线程共享, 堆区中不存放基本类型和对象引用, 只存放对象实例
四、方法区中:
- 存放线程执行的字节码指令
- 被所有线程共享, 其中包含所有的class和static变量
- 常量池位于方法区中, 例如String的常量池