一 运行时数据区域
1. 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器,线程私有,生命周期与线程一致。
作用:如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器值则应为(Undefined)。此内存区域是唯一 一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
2. java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编 译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量, 虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一 个变量槽,这是完全由具体的虚拟机实现自行决定的事情。
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
HotSpot虚拟机的栈容量是不可以动态扩展的,所以在HotSpot虚拟 机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的
.. 栈的内部结构-栈帧
Ⅰ 局部变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
- 局部变量表建立在线程的栈上,因此是线程私有的数据,不存在数据安全问题
- 局部变量表的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小的(这里的大小指的是局部变量槽的数量)。
局部变量槽(Slot):
局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽
Ⅱ 操作数栈
- 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据(入栈/出栈)
- 主要用于保存计算过程的中间结果,同时作为计算过程中间变量的临时存储空间
- 另外,我们说的java虚拟机的解释引擎是基于栈的执行引擎,其中的栈就是操作数栈
Ⅲ 动态链接
- 概念:指向运行时常量池中该栈帧所属方法的引用
- 这个引用的目的就是为了支持当前方法的代码能够实现动态链接
.. 方法的调用
① 动态链接和静态链接
- 静态链接:编译时就确定了(非虚方法)
- 动态链接:运行时才能确定(虚方法)
② 方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象实际类型,记作C。
- 依次往上寻找父类
③ 虚方法表
- 为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找
- 每个类中都有一个虚方法表,存放各个方法的实际入口
- 类加载的链接阶段创建完毕并开始初始化,类变量初始化完毕的时候该方法表也初始化完毕。
Ⅳ 方法返回地址
存放调用该方法的pc寄存器的值,作为返回地址,即调用该方法的指令的下一条指令的地址
方法要么正常结束,要么抛出异常
Ⅴ 一些附加信息
例如:对程序调式提供支持的信息
3. 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,所以甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
4. java堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例
堆中的Eden区还可以划分私有的缓冲区(TLAB),实现线程私有。
数组和对象永远不会存储在栈上,栈帧保存的引用指向堆中的位置
-Xms 用于设置堆的起始内存 默认大小:物理电脑内存/64
-Xmx 用于设置堆的最大内存 默认大小:物理电脑内存/4
-NewRatio 设置 老年代:新生代 默认值2 (一般不会修改)
-Xmn 用于设置新生代的最大内存 (一般不会修改)
注意:只能设置新生代和老年代的大小
Ⅰ TLAB
什么是TLAB?
JVM为每个线程在Eden区分配了一个私有的缓存区域,是JVM内存分配的首选
有什么作用?
多个线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,提升内存分配的吞吐量。因此称为快速分配策略
其他:
TLAB空间的内存非常小,仅有整个Eden的1%,可设置
一旦对象在TLAB空间分配失败,JVM会尝试使用加锁机制确保数据操作的原子性
Ⅱ 堆分配对象存储是唯一选择吗?
如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上的分配
代码优化:
- 能使用局部变量就不要定义在方法外
- 同步省略(锁消除):在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问,如果是,编译器在编译的时候就可以取消这部分代码的同步。
- 分离对象或标离替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。解释:在JIT阶段,如果经过逃逸分析发现一个对象不会被外界访问的话,经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替,这个过程就叫标量替换
5. 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把 方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区 分开来。
JDK 8之前,HotSpot虚拟机设 计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区。
到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
方法区的大小决定了系统可以保存多少个类,类太多同样会抛出内存溢出异常 java.lang.OutOfMemoryError
〇 栈、堆、方法区的交互关系
Ⅰ 运行时常量池
.class文件中的常量池
存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后放到方法区的运行时常量池中
为什么要有常量池?
java字节码需要数据支持,通常这种数据很大不能直接存到字节码中,可以存到常量池,字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池
运行时常量池
运行时常量池包含多种不同的常量,包括编译期就一级明确的数值字面量, 也包括到运行期间解析后才能够获得的方法或者字段引用。此时不再时常量池中的符号地址了,这里换为真实地址。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。
Ⅱ 永久代为什么被元空间替换
- 永久代难以确定设置的大小,元空间使用本地内存
- 永久代调优困难
Ⅲ StringTable为什么移到堆中
因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足或永久代空间不足时才会触发。这就导致回收效率不高。
6. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常。
- 通过MaxDirectMemorySize设置直接内存大小
- 如果不指定,默认与堆的最大值-Xmx参数值一致
二 HotSpot虚拟机对象探秘
1. 对象的实例化
Ⅰ对象创建的方式
Ⅱ 对象创建的步骤
2. 对象的内存布局
3. 对象访问定位
Ⅰ直接指针(Hotspot采用)
Ⅱ 句柄访问
三 StringTable
-XX:StringTableSize
设置StringTable长度,默认60013,最小只能设置1009
1. String的基本特性
- 创建方式
String s1 = "hello";
//字面量的定义方式,”abc”存储在字符串常量池中String s2 = new String("hello");
// new
- String声明为final,不可被继承
- 实现了Serializable接口:String 支持序列化
实现了Comparable接口:String 可比较大小
jdk8及以前内部定义final char[] value 用于存储字符串数据,jdk9时改为byte[]
字符串常量池不会存储相同内容的字符串
2. String的内存分配
jdk6字符串常量池放在永久代
jdk7及往后字符串常量池放在堆
3. 字符串的拼接操作
- 字符串常量池中不会存在相同的内容的常量
- 常量与常量的拼接结果在字符串常量池,原理是编译优化
String s1 = "a" + "b" + "c";
String s2 = "abc";
s1 == s2; //true
- 只要拼接操作中有一个是变量,结果就在堆中,不会放在字符串常量池。变量拼接的原理是
StringBuilder
String s1 = "hello";
String s2 = s1 + "world"
s1 == s2; //false
- 如果拼接的结果主动调用
intern()
方法,- 常量池中没有该结果:则将结果放入常量池,并返回地址。
- 常量池中有该结果:则直接返回该字符串地址。
String s1 = "hello";
String s2 = s1 + "world"
String s3 = "helloworld"
String s4 = s2.intern();
s3 == s4; //true
4. 字符串拼接优化
String s1 = "joint";
StringBuilder s2 = new StringBuilder("joint");
StringBuilder s3 = new StringBuilder(1000000);
for (int i = 0; i < 1000000; i++) {
s1 += i; // 由于每次都需要new StringBuilder 和 new String,效率最低,浪费内存
s2.append(i); // 只需要创建一次 StringBuilder 和 String,效率大大提高
/*
因为底层用的是char[] 数组维护字符串,所以在初始化时提供合适的数组大小。
可以减少数组扩容的次数,从而提高拼接效率
*/
s3.append(i);
}
5. 面试题详解
public static void main(String[] args) {
String s1 = new String("11");
String s2 = s1.intern();
String s3 = "11";
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // true
}
上面代码对应内存图:
public static void main(String[] args) {
String s1 = new String("1") + new String("1");
String s2 = s1.intern();
String s3 = "11";
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s2 == s3); // true
}
上面代码对应内存图:
总结:
只要做了拼接,那么字符串就不会存入常量池,当调用**intern()**
的时候,常量池保存该字符串实例的引用
只要不做拼接,那么字符串就会存入常量池,即存的是哈希码和该字符串字面量
6. G1垃圾收集器的String去重操作
实现:用hashtable来维护String对象使用的char数组。
四 执行引擎
1. 概述
执行引擎时Java虚拟机核心组成部分之一,主要做翻译工作
“虚拟机”是一个相对于“物理机”的概念,两种都具有执行代码的能力
- 区别:
- 物理机执行引擎直接建立在处理器、缓存、指令集和操作系统层面上。
- 虚拟机执行引擎是由软件自行实现的,因此可以不受物理条件制约定定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
JVM的主要任务是负责装载字节码到其内部,但并不能执行运行在操作系统上。如果要运行java程序,执行引擎的任务就是将字节码指令解释/编译为对应平台的本地机器指令才可以。
2. 工作过程
- 执行引擎在执行过程中,依赖于程序计数器来确定执行什么样的字节码指令
- 通过对象访问定位,访问对象的信息
- 所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处 理过程是字节码解析执行的等效过程,输出的是执行结果
即时编译器
3. Java代码编译和执行过程
什么是解释器(Interpreter),什么是JIT编译器?
- 解释器(边翻译,边执行)
当Java虚拟机启动时会根据定义的规范对字节码采用逐行解释的方式执行,对每行字节码“翻译”为本地机器指令执行
- JIT(Just In Time Compiler) 编译器(全部翻译,后执行)
就是虚拟机将源代码直接编译成本地机器平台相关的机器语言
为什么说Java时半编译半解释型语言?
在java1.0的时候,java还是解释型语言。后来加入了JIT编译器,二者结合起来执行
4. 为什么解释器与JIT编译器要并存
- 程序启动后,解释器可以马上发挥作用,减少响应时间
- 随着程序运行时间的推移,即时编译器发挥作用,把越来越多的热点代码编译成本地代码,获得更高的执行效率
- 同时,编译器进行激进优化不成立的时候,可以切换为解释器执行
5. 热点代码探测方式
Ⅰ 什么是热点代码?
一个被多次调用的方法,或者循环多次的循环体都可以被称为“热点代码”。因此都可以通过即时编译器编译为本地代码,由于这种编译方式发生在方法执行过程中,被称为栈上替换,或简称OSR(On Stack Replacement)编译
Ⅱ 探测方式
关闭热度衰减
- -XX:CounterHalfLifeTime
设置半衰周期,单位是秒
6. 切换执行方式
- -Xint
解释器模式
- -Xcomp
即使编译器模式
- -Xmixed
混合模式(默认)
7. 编译器分类
HotSpot 中有两个JIT编译器:Client、Server,大多数时候称为:C1编译器、C2编译器
可通过下面两种命令选择,x64下默认使用server
- -client
- -server
C1编译器:对字节码进行简单可靠的优化,耗时短
C2编译器:对字节码进行耗时较长的优化,以及激进优化
优化策略
C1
- 方法内联:把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免发生真实的方法调用
- 去虚拟化:对唯一的实现进行内联
- 余消除:在运行期间把一些不会执行的代码折叠掉
C2
- 标量替换:用标量值代替聚合对象的属性
- 栈上分配:对于未逃逸的对象分配对象在栈,而不是堆
- 同步消除:消除同步操作,通常指synchronized
8. 分层编译策略
程序解释执行时,即可以不开启性能监控触发C1编译器,也可以开启性能监控触发C2编译器。这两种编译器相互协作共同来执行编译任务,就叫分层编译策略
9. 展望:Graal编译器和AOT编译器
Graal编译器:从jdk10加入了的全新的即时编译器,目前还在实验阶段
AOT编译器jaotc:静态提前编译器,在程序运行之前进行编译
AOT编译器
优点:
不必等待即时编译器的预热,减少java应用给人带来“第一次运行慢”的不良体验
缺点:
破坏了“一次编译,到处运行”
降低了java链接过程的动态性,加载的代码在编译期就必须全部已知