运行时数据区
程序计数器
是一块较小的内存空间,线程私有,互不影响,独立存储,生命周期随着线程的启动开始,线程的销毁结束;一个时间点上,一个处理器都只能执行一个线程中的指令,程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复都会用到,也是唯一没有OOM的地方;
如果执行的是java方法,则存储当前线程所执行的字节码的行号指示器,通过改变计数器选取下一条需要执行的字节码指令;
如果执行的是本地方法,则为空undefined;
程序计数器的值是字节码执行引擎进行修改的。
存在的意义:当线程被挂起后,要再执行的话,需要用到程序计数器的值来进行定位,定位继续从哪行代码开始执行。
虚拟机栈
线程私有,生命周期与线程相同,用来描述Java方法执行的线程内存模型;栈内部由栈帧组成,一个方法对应一个栈帧内存区域,每个方法执行时,Java虚拟机都会同步创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完毕的过程,就是一个栈帧在虚拟机栈中入栈出栈的过程。
java运行的时候,每一个主线程某个方法,该方法存在局部变量,因此会从内存中取一小块栈区域,到该主线程下;
栈帧中放着该方法的局部变量,如果该方法执行完之后,这部分内存会释放掉,放入的过程叫入栈,销毁的过程叫出栈。
java虚拟机中采用先进后出原则(FILO),先调用的方法先分配内存,后调用的方法后分配内存,因为执行从上向下,因此,先调用的方法会后结束,后调用的方法会先结束。
异常:
- 请求的栈深度大于所允许的栈深度
- 动态扩展时无法申请到足够的空间,Hotspot不会动态扩展
局部变量表
存放该方法的局部变量(存放编译期可知的Java基本数据类型和对象引用类型,并不是对象,而是指向对象起始地址的引用指针)。
数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。
局部变量表所需空间在编译时就已经确定,进入一个方法,该方法所需的局部变量表空间是完全确定的,运行期间不会改变槽的数量。
局部变量:放的是对象的地址,可以根据内存地址,指向堆中的对象,而局部变量在栈中,相当于栈指向堆
操作数栈
是程序运行过程中,临时存放运算时的常量
动态链接
因为方法是放在方法区中的,所以会有一个关于方法区的地址,而动态链接这块存的就是方法区对应方法的地址
方法出口
将上一句调用的方法的信息保存到方法出口中,方法执行完之后,知道应该返回main方法的哪一行代码继续执行
本地方法栈
有native修饰的方法叫本地方法,线程私有,与虚拟机栈作用相似,区别在于虚拟机栈为Java方法(字节码)服务,本地方法栈为虚拟机用到的本地(Native)方法服务。Hotspot将虚拟机栈和本地方法栈合二为一,与虚拟机栈一样。
private native void start0();
线程中调用了本地方法的话,会从本地方法栈中取一小块空间。
异常:
- 请求的栈深度大于所允许的栈深度
- 动态扩展时无法申请到足够的空间,Hotspot不会动态扩展
堆
是虚拟机管理内存中最大的一块,是所有线程共享的垃圾收集器管理的内存区域,又称gc堆。虚拟机启动时创建,用来存放对象实例。
从分配内存角度看,堆中可以划分出多个线程私有的分配缓冲区,以提升分配对象的效率。Java堆既可被实现为固定大小的,也是可扩展的。
异常:
在Java堆中没有内存完成实例分配,且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
java堆从GC的角度细分为:新生代和老年代
如果eden放不下之后,java虚拟机的字节码执行引擎会开启垃圾回收线程,收集垃圾对象;
年轻代
Eden区,to survivor,from survivor
老年代
1.默认分代年龄达到15,还没有被回收的话,会挪到老年代,分代年龄被放在对象头中
2.对于年轻代放不下的大对象,也会放到老年代
3.对象动态年龄判断也会挪到老年代
对象动态年龄判断
如果当前放对象的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了;
例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。
这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。
对象存储布局
HotSpot对象在堆内存中的存储布局:对象头(header),实例数据(instance data),对齐填充(padding)。
对象头包含两部分信息:
(1)存储对象自身的运行时数据,哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳,数据长度在32位和64位的虚拟机中分别占32比特和64比特。Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
(2)类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例,但是查找对象的元数据信息并不一定要经过对象本身,因此并不是所有的虚拟机实现都必须在对象数据上保留类型指针
方法区
jdk1.8之前叫永久代,jdk1.8叫元空间,各个线程内存区域共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的class文件缓存等数据,又称非堆。
JDK1.8之后将最初的永久代取消了,由元空间取代,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
方法区中的静态变量:放的是堆中的地址,静态变量在方法区中,因此相当于方法区指向堆
方法区中类信息:字节码的类信息会加载到方法区。
垃圾收集行为在这个区域较少出现,但并非数据进入了方法区就如永久代的名字一样“永久”存在。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
元空间和永久代之间最大的区别在于:
永久代使用的jvm的堆内存,但是元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小受本地内存的限制,类的元数据放入native memory,字符串和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
运行时常量池:是方法区的一部分。存放的内容:
1.存放编译期生成的各种字面量与符号引用,在类加载后放入。
2.因其动态性的特性,运行期间也可以将新的常量放入池中,使用较多的便是String类的 intern()方法。
异常:常量池无法申请到内存时
方法区垃圾收集
废弃的常量:对于常量池中的常量a,如果没有任何对象引用该常量的话,就表示它是一个废弃常量。
不再使用的类:
1.Java堆中没有任何该类的实例;
2.该类的类加载器已被回收;
3.该类的Class对象没在任何地方被引用,且无法在任何地方通过反射访问该类的方法。
创建对象流程
类加载机制
当虚拟机遇到一个字节码new指令时,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已被加载,解析,初始化过,如果没有就需要先去走类加载机制进行加载
分配内存
如果加载之后,虚拟机为该对象分配内存,对象所需的内存大小在加载的时候便已经确定;
如果内存区域规整,则使用指针碰撞(下面详细介绍);
如果内存区域不规整,则使用空闲列表(下面详细介绍);
而内存区域是否规整是根据垃圾收集器中的算法决定,看其是否具有压缩整理的能力
初始化零值
内存分配完之后,虚拟机会将分配到的内存空间都初始化为零值,如果使用了tlab(下面详细介绍),则会提前到和tlab(下面详细介绍)一起执行,这步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,使程序可以访问到这些字段所对应数据类型的零值
设置对象头
之后java虚拟机还要对对象进行必要设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(object header)中。
执行init方法
执行init方法,即对象按照程序员的意愿进行初始化,对应到语言层面就是为属性赋值,(这里的赋值与上面的赋零值不同,是由程序员赋值)和构造方法。
多线程创建对象内存分配问题
在多线程情况下,虚拟机给A对象分配了一块内存,但是还没来得及移动指针,B对象进来也分配了相同的区域的解决方案
1.在分配内存时进行同步处理,虚拟机是采用CAS的方式来保证原子性
2.设置-XX:+UseTLAB开启TLAB(默认是开启的)
TLAB
TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 内存分配的一个概念,JVM 会针对每一个线程在 Java 堆中预留一个内存区域,专门在该区域为该线程创建的对象分配内存。在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作。
主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。
没有启用 TLAB的话,多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。
通过-XX:+UseTLAB决定是否开启;
通过-XX:TLABSize指定TLAB大小。
指针碰撞
若内存区域规整,已被使用过的内存在一边,未被使用过的内存在其后紧跟,中间放着一个指针作为分界点指示器,如果需要分配内存,就移动需要内存大小的指针,这种分配方式称为指针碰撞
空闲列表
若内存区域不规整,使用和未使用的内存空间交杂在一起,那虚拟机会维护一个列表,记录哪些内存块可用,在分配的时候从列表中找一块足够大的内存空间分配给对象实例,并更新列表记录,这种分配方式称为空间列表