运行时数据区

程序计数器
程序计数器(ProgramCounter),也叫PC寄存器,每个Java虚拟机线程都有自己的程序计数器,会记录正在执行的字节码指令的地址或者行号(如果执行的是Native方法,则为undefined)
程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来时能够知道该线程上次运行到哪里。
Java虚拟机栈
每个Java虚拟机线程都有自己私有的Java虚拟机栈,这个栈和线程同时创建,用于存储栈帧;
用来存储当前线程运行方法所需要的数据、指令、返回地址(栈帧)
可以通过-Xss 这个虚拟机参数来指定每个线程的Java 虚拟机栈内存大小,-Xss2M;在JDK 1.4 中默认为256K,而在JDK1.5+ 默认为1M;
异常:
- 线程请求分配的栈容量超过Java虚拟机栈的最大容量时,会抛出StackOverflowError 异常;一般无限递归会出现这个错误
- 如果Java虚拟机栈可以动态扩展,并且动态扩展时没有申请到足够的内存;或者创建新的线程时没有足够的内存去创建一个新的Java虚拟机栈,或抛出OutOfMemoryError
在做多线程开发时,当创建很多线程时,容易出现OOM(OutOfMemoryError),这时可以通过具体情况,减少最大堆容量,或者栈容量来解决问题,如下:
线程数*(最大栈容量)+最大堆值+其他内存(忽略不计或者一般不改动)=机器最大内存
当线程数比较多时,且无法通过业务上削减线程数,那么再不换机器的情况下,你只能把最大栈容量设置小一点,或者把最大堆值设置小一点。
本地方法栈
本地方法栈和Java虚拟机栈相似,本地方法栈是为Native方法服务的
Java堆
Java虚拟机中,堆是所有线程共享的内存区域,所有类实例对象和数组对象分配内存的区域,在Java虚拟机启动时创建;是垃圾回收的主要区域(GC堆)
方法区
方法区也是所有线程共享的内存区域,用来存储已被虚拟机加载的类信息、常量(字面量)、静态变量、即时编译器编译后的代码等数据(动态代理动态生成的class);
方法区是一个JVM 规范,永久代与元空间都是其一种实现方式。在JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。方法区是一个概念上的东西,在1.8之后被拆分成两部分;元空间存储类的元信息,静态变量和常量池等放入堆中。
永久代和方法区的关系:《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它;在jdk1.7之前,JVM通过永久代实现方法区;所以当时==永久代=方法区==;
在jdk1.7之前,永久代是存放在堆中老年代;和老年代共享堆空间,永久代的垃圾收集是和老年代捆绑在一起的;因此无论谁满了都会触发GC;
运行时常量池
在class字节码文件中,Java会把所有需要使用的数据和方法提取出来,然后运行的时候放入常量池
运行时常量池是方法区的一部分,用于存储编译器生成的各种字面量和符号引用;
除了在编译期生成的常量,还允许动态生成,例如String 类的intern()。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,在JDK1.4加入了NIO类,引入了基于通道(Channel)和缓冲区(Buffer)的IO方式,可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样在一些场景中能显著提高性能,避免了在Java堆和Native堆中来回复制数据;
栈帧
栈帧是JVM虚拟机进行方法调用和方法执行的数据结构;是运行时数据区中Java虚拟机栈或者本地方法栈的栈元素。栈帧存储了局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行结束的过程,都对应一个栈帧在虚拟机栈中从入栈到出栈的过程
简单来说,当线程调用某个方法时,就会把这个方法的相关数据压入一个虚拟机栈(或者本地方法栈),然后这个方法的相关数据就是一个栈帧(局部变量表、操作数栈、动态链接和方法返回地址)
局部变量表
局部变量表是一组变量值存储空间,用来存储方法的参数和方法内部定义的局部变量;
操作数栈
每个方法对应一个栈帧,那方法中指令执行的容器就是操作数栈,方法执行过程中,每个指令会往操作数栈中写入和读取数据,这个过程就是指令的入栈和出栈操作
动态链接
在Class文件中的常量持中存有大量的符号引用。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类的加载阶段**(解析)**或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
方法返回地址
一个方法开始执行,有两种方式退出这个方法,一种是执行引擎遇到返回指令,另一种是执行过程中遇到了异常
Java对象
对象的创建
-
类加载检查:首先检查当前类是否被加载到JVM中,如果没有,先执行类的加载过程
-
分配内存:类加载完成后,通过类对象获取到类信息,可以确定类所需的内存大小;然后分配所需的内存
内存分配的两种方式:指针碰撞和空闲列表
-
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
-
设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
-
执行init方法:执行构造方法,为变量赋值
对象的访问定位
Java运行过程中通过栈上的对象引用地址来访问对应的对象;存在两种方式:使用句柄和直接指针
句柄:使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;需要跳转两次访问对象

直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。GC中的复制算法,标记整理都需要考虑对象引用的修改

使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。