一直以来,对于JVM的内部结构不是很清楚,最近看了一篇博客,写的很好,这里简单总结下。
JVM内存模型
JVM内存可以划分为线程共享的区域和线程私有的区域。
共享区域有 方法区 和 堆 ,私有区域有 虚拟机栈 、本地方法栈 和 程序计数器。
线程私有的数据区
程序计数器
为解决线程切换后能恢复到正确的执行位置的问题,每条线程都需要一个独立的程序计数器去记录正在执行的字节码指令地址。
程序计数器是线程私有的一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。
如果正在执行java代码,则记录的是正在执行的字节码指令的地址,如果是Native方法,计数器的值为空。
程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。
虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。其中,局部变量表主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和 对象句柄,它们可以是方法参数,也可以是方法的局部变量。
StackOverflowError
一个线程拥有一个自己的栈,这个栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小),若线程请求的栈深度大于虚拟机允许的深度,则抛出 StackOverFlowError 异常。
OutofMemoryError
栈的大小可以是固定的,也可以是动态扩展的,若虚拟机栈可以动态扩展(大多数虚拟机都可以),但扩展时无法申请到足够的内存(比如没有足够的内存为一个新创建的线程分配栈空间时),则抛出 OutofMemoryError 异常。下图为栈帧结构图:
本地方法栈
本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
线程共享的数据区
Java堆
也可以称为GC堆,我们new的对象都在这个区域,所有线程共享此区域。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。而且,Java堆在实现时,既可以是固定大小的,也可以是可拓展的,并且主流虚拟机都是按可扩展来实现的(通过-Xmx(最大堆容量) 和 -Xms(最小堆容量)控制)。如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError 异常。
TLAB
TLAB可以提升内存分配效率,每个线程都要一块独立的空间TLAB,在TLAB上分配对象时不需要加锁(相对于CAS配上失败重试方式),因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
虚拟机为新生对象分配内存时,需要考虑修改指针(该指针用于划分内存使用空间和空闲空间)时的线程安全问题,因为存在可能出现正在给对象A分配内存,指针还未修改,对象B又同时使用原来的指针分配内存的情况。TLAB的存在就是为了解决这个问题:每个线程在Java堆中预先分配一小块内存TLAB,哪个线程需要分配内存就在自己的TLAB上进行分配,若TLAB用完并分配新的TLAB时,再加同步锁定,这样就大大提升了对象内存分配的效率。
方法区
方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池
运行时常量池(RuntimeConstantPool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。
Java堆 与 方法区的区别
Java堆是 Java代码可及的内存,是留给开发人员使用的;而非堆(Non-Heap)是JVM留给自己用的。
方法区、JVM内部处理或优化所需的内存 (如JIT编译后的代码缓存)、每个类结构 (如运行时常量池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
方法区的回收
常量池的回收 和 对类型的卸载。
对象在虚拟机中的创建过程
1.检查虚拟机是否加载了所要new的类,若没加载,则首先执行相应的类加载过程
2.在类加载检查通过后,对象所需内存的大小在类加载完成后便可完全确定,虚拟机就会为新生对象分配内存。
内存的分配有两种方式:
- 指针碰撞:如果Java堆中内存绝对规整
- 空闲列表:如果Java堆中内存并不规整
修改指针 (该指针用于划分内存使用空间和空闲空间)时的线程安全问题:
- 对分配内存空间的动作进行同步处理:采用CAS+失败重试的方式保证更新操作的原子性;
- 把内存分配的动作按照线程划分的不同的空间中:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在自己的TLAB上分配,如果TLAB用完并分配新的TLAB时,再加同步锁定。
3.内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
4.在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,此时会执行方法把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。
对象在虚拟机中的访问定位
句柄访问和直接指针访问
参考链接
https://blog.csdn.net/justloveyou_/article/details/71189093
——文中图片引用自CSDN