JVM全文
直接内存
- Direct Memory
- 元空间使用的本地的直接内存
- 直接内存不是java虚拟机的规范中的一部分
- 是java堆外的,直接向系统申请的内存空间
- 来源NIO,通过存在堆中的
DirectByteBuffer
操作Native内存
- 访问直接内存的速度优于java堆,读写频繁的场合使用直接内存
- NIO库允许java程序直接使用直接内存,用于数据缓冲区
-XX:MaxDirectMemorySize
设置直接内存大小,默认与堆的最大值参数一致
- 缺点
- 分配回收成本较高
- 不受JVM内存回收管理
- NIO,New IO / Non-Blocking IO,java代码可以直接访问操作系统划出的直接缓存区,直接访问物理磁盘,适合对大文件的读写操作
- 通过Buffer传输
- Channel来传输
- IO,读写文件,需要与磁盘交互,需要由用户态切换到内核态,需要将内容保存两份,通过虚拟机地址再访问内核地址
异常
- 导致
OOM: Direct buffer memory
异常 - 大小不会直接受限于
-Xmx
指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和受限于操作系统能给出的最大内存
执行引擎
- 虚拟机的执行引擎是由软件自行实现的,不受物理条件制约,能够执行不被硬件直接支持的指令集格式
- jvm主要任务是负责将字节码文件装载进内部,但是字节码文件(实际上是跨平台的通用契约)不能够直接运行在操作系统之上,
字节码文件并非等价于本地机器指令
,内部包含的仅仅只是一些能够被jvm所识别的字节码指令、符号表,以及其他辅助信息 - 执行引擎的任务,将字节码指令解释/编译为对应平台上的本地机器指令,充当了高级语言翻译为本地语言的翻译者
- 工作过程
- 执行引擎在执行过程中执行的字节码指令依赖于PC寄存器
- 当执行完一项指令操作后,PC寄存器
更新下一条需要被执行的指令地址
- 执行过程中,有可能通过存储在局部变量表中的对象引用准确定位到存储在java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息
- 输入字节码二进制流,输出执行结果
- 结构
java的编译和执行
- 橙色部分由
前端编译器 javac
完成,遍历语法树,形成线性字节码指令流
- 绿色部分,解释型语言,逐行翻译
- 解释器,当java虚拟机启动时,根据预定义的规范
对字节码采用逐行解释的方式执行
,翻译成本地机器指令- 蓝色部分,编译型语言
- JIT,Just In Time Compiler编译器,虚拟机将源代码直接编译成和本地机器平台相关的机器语言
- 前端编译过程,将java从一种语言规范转换另一周语言规范的过程
- jvm执行引擎执行过程,将字节码转换成对应机器指令,被对应机器识别
- Java为半编译半解释型语言,执行引擎中既通过解释器,又通过编译器
机器码、指令和汇编语言
- 机器码,二进制编码表示的指令,与CPU紧密相关,CPU直接读取运行,执行速度最快
- 指令,
mov/inc
,把机器码中特定的0/1序列
简化成对应的指令,不同硬件平台,同一个操作,机器码可能不同,一个对应关系,需要转换成0/1序列
,才能被cpu识别 - 指令集,不同的硬件平台,各自支持的指令是有差别的,
x86/ARM指令集
- 汇编语言,用
助记符
代替机器指令的操作码,用地址符号或标号
代替指令或操作数的地址,计算机只认识指令码,必须翻译成机器指令码 - 高级语言,更接近人的语言,执行时,需要把程序解释和翻译成机器指令码
- 高级语言(C/C++)到机器指令的过程
- 编译,读取程序字符流,进行词法和语法分析,将高级语言转换为功能等效的汇编代码
- 汇编,将汇编语言翻译成目标机器指令
- 字节码,是一种中间状态的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码
- 为了实现特定软件运行和软件环境,与硬件无关
- 实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台的虚拟机器(执行引擎)将字节码转译成可以直接执行的机器指令
- 等同于汇编语言
- java语言过程
java源代码
->字节码
->机器指令
- jvm直接将字节码转译为机器指令
- 汇编不是一个必须的过程,具体看编译器
解释器
- 解释型语言,边解释边执行
- Java、C#、PHP、JavaScript、VBScript、Perl、Python、Ruby、MATLAB
- 每执行一次都要翻译一次。因此效率比较低。在运行程序的时候才翻译,专门有一个解释器去进行翻译,每个语句都是执行的时候才翻译。效率比较,依赖解释器,跨平台性好
- 将字节码文件中的内容翻译为对应平台的本地机器指令执行
- 当一条字节码指令被解释执行完成后,再根据PC寄存器记录的下一条需要被执行的字节码指令执行解释操作
字节码解释器
,执行时,通过纯软件代码模拟字节码执行,效率低下模板解释器
,每一条字节码和一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码- jvm中,解释器主要由
Interpreter模块和Code模块组成
- Interpreter,实现解释器核心功能
- Code,管理hotspot vm在运行时生成的本地机器指令
JIT编译器
- 编译型语言,先编译成机器码,再执行
- C/C++、Pascal/Object Pascal(Delphi)、Golang
- 典型的就是它们可以编译后生成.exe文件,之后无需再次编译,直接运行.exe文件即可
- 避免函数被解释执行,将整个函数体编译成机器码,每次函数执行,只执行编译后的机器码即可
- 将代码翻译成机器指令,并进行缓存
- java解释器和编译器并存的架构
- 程序启动后,编译器可以马上发挥作用,立即执行,响应速度快;编译器需要编译成本地机器指令,需要一定的执行时间,但编译完成后执行效率高
- java虚拟机启动后,
解释器首先发挥作用
,不必等待JIT编译器全部编译后再执行,省去不必要的编译时间,随着程序运行,JIT编译器发挥作用
,根据热点探测功能
,将有价值的字节码编译成本地机器指令- JRockit vm不包含解释器,全部依靠即时编译器编译后执行,针对于服务端应用,启动时间并非是关注重点
- 在编译器进行激进优化不成立的时候,将解释器作为
逃生门/后备方案
- 前端编译器,将
.java文件
转换为字节码/.class文件
- 后端编译器,将
字节码/.class文件
转换为机器码
,JIT编译器,hotspot vm的C1/C2编译器 - 静态编译器,AOT,Ahead Of Time Compiler,直接把
.java文件
编译成本地机器代码的过程,GCJ/Excelsior JET - 热点代码及探测,决定是否需要启动JIT编译器将字节码直接编译成对应平台的本地机器指令
- 根据代码被调用执行的频率,进行深度优化,将
其直接编译为对应平台的本地机器指令
- 热点代码,一次被多次调用的方法,或是一个方法体内部循环次数较多的循环体;这种编译方式发生在方法的执行过程中,称为
栈上替换/OSR On Stack Replacement
- 热点探测功能,决定被调用多少次或者循环多少次才能达到这个标准,hotspot采用的是
基于计数器的热点探测
- 基于计数的热点探测,为每一个方法建立
两个不同类型的计数器
,分别为方法调用计数器
,统计方法被调用的次数;回边计数器
,统计循环体执行的循环次数;两个计数值之和是否超过阈值
- 方法调用计数器
- 默认阈值
Client
模式1500次,Server
模式下10000次,超过阈值,出发JIT编译;-XX:CompileThreshold
设置阈值- 达到阈值后,缓存热点代码
- 热点衰减(Counter Decay),如果不做任何设置,方法调用计数器统计的是一个相对的执行频率,一段时间之内方法被调用的次数;当超过一定的时间限度,调用次数仍然不足触发JIT编译,那这个方法的调用计数器就会减少一半,这段时间被称为
半衰周期 Counter Half Life Time
;进行热度衰减的动作实在虚拟机进行垃圾收集时顺便进行的-XX:-UseCounterDecay
关闭热度衰减,时间足够长,绝大部份方法都会被编译成本地代码-XX:CounterHalfLifeTime
,设置半衰周期时间,单位秒
- 回边计数器,统计循环体执行的次数,当字节码中遇到控制流向后跳转的指令被称为
回边 Back Edge
设置程序执行方式
- 控制台
java -Xint -version
完全采用解释器模式java -Xcomp -version
完全采用即时编译器模式,如果编译出现问题,解释器介入java -Xmixed -version
混合模式
- run->edit configurations,添加参数
-Xint/-Xcomp/-Xmixed
- Clint Complier C1
-client
,指定java虚拟机运行在client模式下,使用c1编译器- 会对字节码进行简单和可靠的优化,耗时短,达到更快的编译速度
- 优化策略,方法内联、去虚拟化、冗余消除
- 方法内联,将引用的函数代码编译到引用点处,减少栈帧的生成,减少参数传递和跳转
- 去虚拟化,对唯一的实现类进行内联
- 冗余消除,运行期间把一些不会执行的代码折叠
- Server Complier C2,64位只有server,不能设置client,C++编写
-server
,指定java虚拟机运行在server模式下,使用c2编译器- 进行较长的优化,以及激进优化,优化后的代码执行效率更高
- 主要是在全局层面,逃逸分析是优化的基础
- 标亮替换,用标量值代替聚合对象的属性值
- 栈上分配,对于未逃逸的对象分配对象在栈上
- 同步消除,如果一个对象被发现只能从一个线程被访问到,清除同步操作
- 分层编译
- 程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,进行简单优化
- 可以加上性能监控,C2编译根据性能监控信息进行激进优化
- C1/C2协同执行编译任务
- C2编译器启动时长比C1编译器慢,系统稳定执行后,C2编译器执行速度远快于C1编译器
其他
- 激活
Graal编译器
,目标代替C2,与C1/C2并列,-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
AOT编译器
,Ahead Of Time Compiler,静态提前编译器,
- 引用
jaotc
工具,将.class
转换为.so
,借助Graal编译器,转换为机器码,并存放至生成的动态共享库之中- JIT是在程序运行过程中,将字节码转换为机器码,并进行缓存
- AOT,在程序运行之前,将字节码转换为机器码
- 不必等待即时编译器的预热
- 但是必须为每个不同硬件,不同操作系统生成对应的发行包;降低了java链接过程的动态性,加载的代码在前端编译期就必须全部已知
面试
- 我们写的Java代码到底是如何运行起来的?
Java 程序通过 javac 编译成 .class 文件,然后虚拟机将其加载到
元数据区
,执行引擎将会通过混合模式
执行这些字节码。执行时,会翻译成操作系统相关的函数。
StringTable
- String:字符串 ""
- String s1 = "test"; //字面量定义方式,存储在字符量常量池,不允许存储相同的字符串
- String s2 = new String("test");
final
修饰,不能被继承
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
....
}
复制代码
- 底层
- 1.8及前 char[] 两个字节
- 1.8后 byte[]
- 大部分String包含的都是拉丁文,拉丁文只用一个字节就可以存储,因此改为用byte数组,并有属性
coder
记录字符集编码LATIN1/UTF16
;针对UTF-16
两个字节的字符集,底层有COMPACT_STRINGS
,默认为true 启动压缩,压缩失败就用StringUTF16.toBytes
处理- 数组+链表/红黑树
- 字符串常量池不会存储相同内容的字符串
- 字符串常量池底层是一个固定大小的
Hashtable
(存放字符串哈希值)- jdk6中 默认长度1009,如果放进的string非常多,就会造成Hash冲突
- jdk7中 默认长度60013;jdk8后,1009是可设置的最小值
1009 ~ 2305843009213693951
-XX:StringTableSize
,设置
- 不可变的字符序列,不可变性
- 对字符串重新赋值,需要重写指定内存区域赋值,不能使用原来的value进行赋值
- 对现有字符串进行连接操作,重新指定内存区域赋值,生成新的字符串
- 调用
replace()
修改指定字符或字符串,生成新的字符串
- 相同的字符串字面量,包含相同的
Unicode字符序列(包含同一份码点序列的常量)
,并且必须指向同一个String类实例 - 案例
String str = new String("hello");
//string不可变的特性,仅仅是让传入的str指向了字符串常量池的"ok",而并没有修改成员变量str的指向;传入的是成员变量str的地址
public void change(String str) {
str = "ok";
}
public static void main(String[] args) {
StrngTest st = new StrngTest();
st.change(st.str);
System.out.println(st.str); // 输出hello
}
复制代码
内存分配
- jdk6及以前,字符串常量池在永久代中
- jdk7及以后,在堆中
- 永久代默认比较小
- 永久代垃圾回收频率低
字符串拼接
- 常量与常量的拼接结果放在常量池中,编译器优化
//字节码
0 ldc #2 <abc>
2 astore_1
3 ldc #2 <abc>
5 astore_2
//源码
String s1 = "a" + "b" + "c"; //在字节码中等同于"abc"
String s2 = "abc"; //指向常量池中的"abc"
System.out.println(s1 == s2); //true
复制代码
- 只要其中有一个是变量,结果就在堆中(非常量池的部分),拼接原理是
StringBuilder
,相当于在堆空间中new String()
,具体字符串内容为拼接结果 - 如果拼接结果调用
inter()
,如果常量池中还没有这个字符,就主动将这个字符串对象放入池中,加载一份,并返回这个字符串在常量池
中的地址
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = s1 + "world";
String s6 = s1 + s2;
System.out.println(s3 == s4); //true
System.out.println(s3 == s5); //false 非常量池的堆中
System.out.println(s3 == s6); //flase
System.out.println(s5 == s6); //flase 另一个空间
String s7 = s5.intern(); //指向常量池中这个字符串
System.out.println(s3 == s7); //true
复制代码
- 拼接原理,5.0之后
StringBuilder(线程不安全,效率高)
;5.0之前StringBuffer(线程安全)
//源码
String s1 = "a";
String s2 = "b";
String s3 = "ab";
//对应 9~27 通过StringBuilder进行字符串拼接
String s4 = s1 + s2;
//字节码
9 new #8 <java/lang/StringBuilder>
12 dup
13 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
16 aload_1
17 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
20 aload_2
21 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore 4
//字节码过程
StringBuilder s = new StringBuilder();
s.append("a");
s.append("b");
//类似于 new String("ab")
s4 = s.toString();
复制代码
- final 修饰,被编译器认为是一个常量,已经是确定值了;
- 针对final修饰类、方法、基本数据类型、引用数据类型的量质结构,能使用就使用;
finanl
在编译的时候就会分配,准备阶段会显示初始化;
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
复制代码
intern
- 如果字符串常量池中没有该字符串,则在常量池中生成,返回刚字符串在
字符串常量池
中的地址 - 确保字符串在字符串常量池中只有一份拷贝
- 保证s指向的是字符串常量池中的数据
- String s = "hello"; //字面量
- String s = .....intern(); //让前面的字符串对象都调用
intern()
返回常量池中的地址
- new String("ab"); 常量池中有
"ab"
String s1 = new String("ab");
//一个对象是new关键字创建的Sttring对象;一个对象时常量池中"ab"对象
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <ab>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
复制代码
- new String("a") + new String("b") 常量池中只有
"a"和"b"
String s2 = new String("a") + new String("b");
//对象1 - StringBuilder
10 new #5 <java/lang/StringBuilder>
13 dup
14 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
//对象2 - String
17 new #2 <java/lang/String>
20 dup
//对象3 - "a"
21 ldc #7 <a>
23 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
26 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//对象4 - String
29 new #2 <java/lang/String>
32 dup
//对象5 - "b"
33 ldc #9 <b>
35 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
38 invokevirtual #8 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
//对象6 - 在返回时调用了toString中,也new String对象
41 invokevirtual #10 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
44 astore_2
复制代码
- StringBuilder的
toString()
创建了一个对象,在字符串常量池中不存在ab
0 new #41 <java/lang/String>
3 dup
4 aload_0
5 getfield #42 <java/lang/StringBuilder.value : [C>
8 iconst_0
9 aload_0
10 getfield #43 <java/lang/StringBuilder.count : I>
13 invokespecial #44 <java/lang/String.<init> : ([CII)V>
16 areturn
复制代码
- intern的改版
- jdk6中,在永久代的字符串常量池中,当没有这个字符串,将这个字符串对象复制一份放入串池,有了新的地址
- jdk7/8中,字符串常量池在堆中,为了节省空间,当没有这个字符串,将对象的引用地址复制一份在池中;当使用了
s3.intern();
,此时在字符串常量池中保存的是"11"对象在字符串常量池外的堆区地址
,常量池中并没有创建
//返回堆空间的对象
String s =new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2); //jdk6/7/8 false
//执行完后,字符串常量池中不存在"11"
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11"; //指向上一行代码在字符串常量池生成的"11"
System.out.println(s3 == s4); //jdk6 false jdk7/8 true
复制代码
- 对于程序中大量存在的字符串,尤其是存在很多重复的字符串,使用
intern()
可以节省内存空间
- intern的优化,当重复生成字符串对象时,可以让字符串的引用执行字符串常量池中的对象
- 原先生成的对象随着执行时间,会被垃圾回收器回收
int[] ints = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = new String(String.valueOf(ints[i % ints.length])).intern();
}
复制代码
垃圾回收
-XX:+PrintStringTableStatistics
打印字符串常量池统计信息
StringTable statistics:
//哈希表的个数
Number of buckets : 60013 = 480104 bytes, avg 8.000
//条目的个数
Number of entries : 58357 = 1400568 bytes, avg 24.000
//字面量的个数
Number of literals : 58357 = 3277192 bytes, avg 56.158
Total footprint : = 5157864 bytes
Average bucket size : 0.972
Variance of bucket size : 0.769
Std. dev. of bucket size: 0.877
Maximum bucket size : 5
复制代码
- G1的String去重操作
- 去重操作针对
非字符串常量池中堆空间的相同字符的String对象
- 堆存活数据集合里面的String对象占了25%,重复的String对象有13.5%,平均长度是45
- 垃圾回收器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选要去重的String对象
- 如果是,则把这个对象的一个引用插到队列中等待处理,去重的线程在后台运行,处理这个队列,意味着从队列删除这个元素,然后去重的它引用的String对象
- 使用一个hashtable记录所有被String对象使用的不重复的
char数组
,去重的时候,查这个hashtable
,如果存在一模一样的char数组
,String对象会被调整引用的那个数组,释放对原来数组的引用,最后被垃圾回收器收集;如果不存在,char数组
就会被插入到hashtable
-XX:+UseStringDeduplication
开启String去重,默认不开启-XX:+PrintStringDeduplicationStatistics
打印详细的去重统计信息-XX:StringDeduplicationAgeThreshold=15
达到这个年龄的String对象会被认为是去重对象