第3章:运行时数据区概述及线程(1)
1、运行时数据区总览
- 方法区和堆区 ,一个进程一份 ,而一个JVM就是一个进程,程序计数器,本地方法栈,虚拟机栈,是一个线程一份。
2、程序计数器(PC寄存器)
-
作用:PC寄存器用来储存指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
-
常见问题:
-
使用PC寄存器储存字节码指令地址有什么用?
- 因为CPU需要不停的切换各个线程,这时候切换回来,就要知道接着从哪开始继续执行
-
为什么使用PC寄存器记录当前线程的执行地址
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
-
3、虚拟机栈
1、虚拟机栈的概述:
-
由于跨平台的设计,Java的指令都是根据栈来设计的。不同的平台CPU架构不同,所以不能设计为基于寄存器的。
-
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,同样的功能需要更多的指令。
2、栈是运行时单位,堆是存储单位
3、Java虚拟机栈是什么:
-
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个一个的栈帧,对应着一次次的Java方法调用。
-
是线程私有的
4、生命周期:
- 生命周期和线程一致
5、作用:
- 主管Java进程的运行,他保存方法的局部变量(八种基本数据类型,对象的引用地址)、部分结果,并参与方法的调用和返回
6、栈的特点(优点)
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
-
JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈工作
-
对于栈来说不存在垃圾回收问题,即:没有GC但是又OOM
7、栈中储存什么?
-
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
-
在这个线程上正在执行的每个方法都各自对应一个栈帧。
-
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信
8、栈帧的内部结构
局部变量表:
-
局部变量表也被称为局部变量数组或本地变量表
-
定义一个数字数组,主要用于储存方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用,以及returnAddress类型
-
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据的安全问题
-
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
-
槽:
-
参数值的存放总是在局部变量表数组的index0开始,到数组长度-1的索引结束。
-
局部变量表中,基本的存储单元是Slot(变量槽)
-
局部变量表中存放编译器可知的各种基本数据类型(8种),引用数据类型(reference),returnAddress类型变量。
-
在局部变量表里,32为以内的数据类型只占用一个slot(包括returnAddess类型),64位的数据类型占用两个solt
-
byte、short、char在储存前被转换为int,boolean也被转换为int 0 表示 false,1 表示true
-
long、double则占用两个 slot
-
如果当前栈帧是由构造器方法或者实例方法创建的,那么该对象引用this将会存放在index0的slot处,其余的参数按照参数表顺序排列,这就是静态方法种不能用this的原因,因为静态方法的局部变量表没有this变量,因为,静态方法可以不通过对象调用,this就是当前对象
-
slot也可以重复利用
-
操作数栈(数组)
-
每个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈。
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据和提取数据,即入栈、出栈
-
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果加入栈。
- 比如:执行赋值、交换、求和等操作。
-
操作数栈,主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
-
一个操作数栈都会拥有一个明确的栈深度用于储存数值,其所需要的最大深度在编译期就定义好了,保存在方法的Code属性中,为 max_stack的值。
-
栈中的任何一个元素都是可以任意的Java数据类型。
-
32bit的类型占用一个栈单位深度
-
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。
-
动态链接
-
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属的方法引用。包含这个引用的目的是为了支持当前方法的代码能够实现动态链接。
-
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
- 比如:描述一个方法调用了另外的其他的方法时,就是通过常量池中指向方法的符号引用来表示的,那末动态连接的作用就是为了将这些符号引用转换为调用的方法的直接引用。
-
静态链接
- 当一个字节码文件被转载进JVM内部时,如果被调用的目标方法在编译期间可知 ,且运行期保持不变。这种情况下将调用方法的符号引用转换为直接引用的过程为:静态链接。
-
动态链接
- 如果被调用的方法在编译期无法确定下来,也就是说,只能在运行期将调用方法的符号引用转化为直接引用,由于这种引用转换过程具备动态性,因此也就是被称为:动态链接
-
方法的调用
- 普通调用(字节码)指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本(非虚方法)
- invokespecial:调用方法、私有方法,解析节点确定唯一方法版本(非虚方法)
- invokevirtual:调用所有虚方法(虚方法)
- invokeinterface:调用接口方法(虚方法)
- 动态调用指令
5. invokedynamic:动态解析出需要调用的方法,然后执行。(虚方法)\
- 普通调用(字节码)指令:
-
虚方法表
- 在面向对象的编中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率,因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现。使用索引表来代替查找。
-
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
-
虚方法表的创建时间
- 虚方法表会在类加载恶的链接阶段被创建并初始化,类的变量初始值准备完成后,JVM会把该类的方法表也初始化完毕。
方法返回地址
-
存放改调用方法的pc寄存器的值
-
一个方法的结束,有两种形式:
-
正常执行完成
- 一个方法在正常调用完成后,返回指令还需要根据返回值的实际类型而定。
- 在字节码指令中,返回指令包含,ireturn 当返回值为boolean、byte、char、short、int类型时使用,lreturn、freturn、dreturn、areturn 另外还有return是没有返回值得方法,实例化方法,类和接口的初始化方法使用。
-
出现未处理的异常,非正常退出
-
-
无论是通过那种方式退出,在方法退出后都返回到该方法的被调用的位置。方法正常退出时,**调用者的pc寄存器的值作为返回地址,即调用该方法指令的下一条指令的地址。**通过异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息。
附加信息(可选)
- 调试的一些信息等
9、栈的常见面试题
- 举例说明栈溢出的情况?
StaticOverflowError
:无限递归,方法调用次数过深
- 调整栈大小,就能保证不出现溢出吗?
- 不能。因为调整栈的大小只会让栈溢出出现的更晚,而不会保证不出现溢出。
- 分配的栈内存越大越好吗?
- 不是。栈分配的内存变大了,挤占的空间变多,线程数变少。
- 垃圾回收是否会涉及到虚拟机栈?
- 不会,虚拟机栈只有溢出,没有GC
- 方法中定义的局部变量是否线程安全?
4、本地方法栈
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者可动态扩展的内存大小。
- 本地方法是使用C语言实现的。
- 它的具体做法是Native Method Stack 中登记native方法,再Execution Engine 执行时加载本地方法库
5、堆
1、堆的核心概述:
-
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
-
Java堆区再JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
-
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该视为连续的。
-
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区
-
数组和对象可能永远不会储存在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
-
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
-
堆,是GC(垃圾回收器)执行垃圾回收的重点区域
-
堆的内存细分(逻辑上)
- jdk7之前:新生区 + 养老区 + 永久区
- jdk8之后:新生区 + 养老区 + 元空间
-
设置堆区的大小:
-Xms600m -Xmx600m
: 表示初始 600 MB,最大600MB
2、年轻代和老年代
-
存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
-
Java堆区进一步细分的话,可以划分为年轻代和老年代
-
其中年轻代又可以划分为 Eden空间、Survivor0空间、Survivor1空间(有时也叫做from区、to区)
-
在HotSpot中,Eden空间和另外两个Survivor空间所占的比例是8:1:1
-
几乎所有的Java对象都是在Eden区创建的
-
绝大部分的Java对象的销毁都是在新生代进行的
-
设置新生代和老年代的比例:
-XX:NewRatio=2
,默认值为 2设置新生代中的Eden区和Survivor区的比例:
-XX:SurvivorRatio=8
,默认值为 8
3、对象内存的分配过程
-
new 的对象先放在伊甸园区,此区有大小限制。
-
当伊甸园的空间填满时程序又需要创建对象,JVM的垃圾回收器将伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区。
-
然后将伊甸园区中的剩余对象移动到幸存者0区
-
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
-
如果再次经历垃圾回收,此时会重新放到幸存者0区,接着再去幸存者1区。
-
当次数达到15(默认)次时,会进入养老区
-
在养老区,相对悠闲,当养老区不足时,会再次触发GC:Major GC,进行养老区内存清理
-
若养老区进行了Magor GC后,发现依然无法保存对象,就会产生OOM的异常
6、方法区
1、方法去的简述
- 在jdk7以及之前,都是叫做永久代或方法区,在jdk8之后,改名为元空间。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间和永久代的最大区别就是:元空间不在虚拟机设置的内存中,而是使用本地内存
- 永久代、元空间二者并不只是名字变了,内部结构也调整了。
2、设置方法区的固定大小
- jdk7之前
-XX:PermSize=xxx
来设置永久代初始分配空间。默认值是20.75M-XX:MaxPermSize=xxx
来设置永久代的最大分配空间。32位机器默认64M,64位机器默认82M
- jdk8之后
-XX:MetaspaceSize=xxx
来设置元空间的初始分配空间大小-XX:MaxMetaspaceSize=xxx
来设置最大元空间的大小
3、方法区的内部结构
-
描述:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等
-
类信息:对于每个加载的类型(类class、接口interface、枚举enum、注解annotation)JVM必须在方法区中存储一下信息:
① 这个类型的完整的有效名称(全名 = 包名.类名)
② 这个类型直接父类的完整有效名(对于 interface 或是 java.long.Object,都没有父类)
③ 这个类型的修饰符(public,abstract,final的某个子集)
④ 这个类型直接接口的一个有序列表
域信息:
① JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
② 域的相关信息包括:域名称、域类型、域修饰符(public,private、protected、static、final、volatile、transient的某个子集)
方法信息
① 方法名称
② 方法的返回类型(或 void)
③ 方法的参数数量和类型(按顺序)
④ 方法的修饰符
⑤ 方法的字节码、操作数栈、局部变量及大小(abstract、native方法除外)
⑥ 异常表 (abstract、native方法除外)
每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
补充说明
①被声明为
static final
的基本数据类型的类变量的处理方法则不同,每个全局变量在编译的时候就会被分配了。但是引用数据类型不会别分配。 -
运行时常量池
- 位置:运行时常量池在方法区中
- 几种在常量池内储存的数据类型包括
- 数量值、字符串值、类引用、字段引用、方法引用
- 常量池 是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用 ,这部分内容在类加载后存放到方法区的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组一样,是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行是解析后才能获得的方法或字段的引用。此时不再是常量池中的符号地址了,这里换为真实地址。
- 小结:
- 常量池,可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
-
永久代为什么被元空间替代
- 为永久代设置空间大小是很难确定的。
- 对永久代调优是很空难的。
-
知识点:
1、并行:同一时间内,由多个线程在指向。
2、串行:同一时间内,只有一个在执行,执行完一个之后,执行下一个。
3、并发:CPU在不同线程之间来回切换,宏观上是并发
4、本地方法接口(不属于运行时数据区)
1、为什么要使用NativeMethod
- Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
- 与Java环境外交互
- 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。Java需要与一些底层系统,如操作系统交换信息时,本地方法正是这样一种交流机制,它为我们提供了一个非常简介的接口,而且我们无需去了解Java应用之外的繁琐的细节
5、Eden代满时,就会触发 Minor GC 而幸存者区满不会触发GC
6、逃逸分析:代码优化
- 栈上分配:将对分配转化为转分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可鞥是栈分配的候选,而不是堆分配。
- 同步策略:如果一个对象被发现只能从一个线程被访问,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在栈。