深入理解JAVA虚拟机
开篇Hotspot核心图镇楼
JAVA执行流程
Java源码(xxx.java) ->
Java编译器 ->
(第一次编译生成) 字节码(xxx.class) ->
Java虚拟机 (类加载器-> 字节码校验器 -> 翻译字节码,执行引擎部分(解释器,JIT编译器和gc) (第二次编译生成机器指令) (JIT编译还会缓存到方法区中))->
操作系统
JVM架构模型
寄存器架构--如x86下安卓虚拟机
性能更好
花费更少指令
依赖硬件,可移植性差
通常一,二,三地址指令分配,需要内存地址,所以需要空间大,16位字节对齐
JVM编译器输入指令使用栈式架构(因为JAVA为了跨平台性)
设计和实现更简单,适用于资源受限系统
不需要寄存器分配,零地址指令分配,通过栈执行,需要空间小,8字节对齐,但是指令数量更多
不依赖硬件,可移植性好
JVM生命周期
启动
虚拟机启动通过引导类bootstap class loader创建初始类来实现,内部main方法后续一起加载起来
执行
Java虚拟机进程执行Java程序
退出
程序正常执行结束
程序执行中异常终止
操作系统出现错误导致java虚拟机进程终止
runtime类halt方法
等等
Hotspot热点代码探测技术
通过计数器找到具有编译价值代码,来触发即时编译或栈上替换
通过编译器和解释器协同工作,在最优化的程序相应时间和最佳执行性能中取得平衡
类加载
类加载具体过程
class file存在于本地硬盘上(可以通过class file实例化出n个一模一样实例),通过类加载器加载到jvm中,称为DNA元数据模板存在方法区
1 加载 loading
class字节码文件加载不成功会抛出异常
通过一个类全限定名获取定义此类二进制字节流
将该字节流所代表的静态存储结构转化为方法区运行时数据结构
在内存中生成一个代表这个类java.lang.class的对象,作为方法区这个类各种数据访问入口
2 链接 linking
验证(verify)
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
比如java虚拟机字节码开头都有cafebabe魔术标识
准备(prepare)
为类变量分配内存并且设置该类变量的默认初始值,即零值;
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
解析(resolve)
将常量池内的符号引用转换为直接引用的过程。加载虚方法表
符号引用就是一个类中(当然不仅是类,还包括类的其他部分,比如方法,字段等),引入了其他的类,可是JVM并不知道引入的其他类在哪里,所以就用唯一符号来代替,等到类加载器去解析的时候,就把符号引用找到那个引用类的地址,这个地址也就是直接引用
事实上,解析操作往往会伴随着jvm在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的目标。引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用就是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info等。
3 初始化 initialization
执行类构造器方法
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
如果没有静态变量,那么字节码文件中就不会有执行类构造器方法(clinit方法),但是init类的构造器任何情况下都会有
若该类有父类,JVM会保证父类clinit已经执行完毕
虚拟机会保证一个类clinit多线程下会同步加锁,这也解释了为什么单例模式内部静态类的线程安全
static{
num = 20;//这里不会报错,虽然int num在后面设定,但是在类加载器prepare阶段类变量已经分配内存且设定默认值为0
System.out.println(num);//这里会报错,非法前向引用
}
private static int num = 10;
类加载器
分为两种
引导类类加载器
自定义类加载器(继承classloader的都为自定义类加载器,双亲委派下扩展类加载器,系统类加载器都为自定义类加载器,因为他们也间接继承了classloader)
系统核心库如String类都是引导类加载器,用户自定义类默认系统类加载器加载
如String类这种通过引导类加载器的,通过反射获得classloader会返回null
自己实现的class普通类等,获得的classloader是系统类加载器
启动类加载器(也是引导类加载器,C/C++编写保存在jvm内部,用来加载java核心库)
ExtClassLoader:
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
派生于ClassLoader类,其父类加载器为启动类加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。
如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
AppClassLoader:
java语言编写,由sun. misc. LaunchersAppClassLoader实现,其派生于ClassLoader类
它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库,
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载,
通过ClassLoader.getSystemClassLoader ()方法可以获取到该类加载器。
自定义ClassLoader:Java编写,定制化加载
为什么需要自定义类加载器
隔离加载类
修改类加载方式
扩展加载源
防止源码泄露
用户自定义类加载器
继承java.lang.classloader
jdk1.2之前重写loadClass但是写的比较复杂,1.2后重写findClass方法,内部通过获取字节码流转数组和defineClass方法(把字节数组转换为Java类的方法)配合使用
如果没有太复杂需求,可以直接继承URLClassLoader类,可以避免编写findClass方法和获取字节码流方式
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将她的class文件加载到内存生成的class对象。而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
我们自己实现一个String的class类,外部调用String时因为双亲委派机制不会调用我们实现的String类
并且我们自己实现的String类内部写main方法时会报错,因为String类会双亲委派机制找引用类加载器,而引用类加载器没有main方法,main方法在系统类加载器中
第三方jar使用,通常接口在引用类加载器中,第三方接口的具体实现在系统类加载器中
双亲委派机制优势
避免类重复加载
保护程序安全,防止核心API被随意篡改(比如java.lang下自己乱实现一个类,启动类加载器会识别出并报错)
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制.
类的主动使用和被动使用
判断两个类是否为同一个类通过两个方式
类的完整类名,包括包名,必须完全一致
类加载器实例对象必须一致
JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证两个类型的加载器是相同的。
java程序对类的使用方式分为:主动使用和被动使用
主动使用,分为七种情况
创建类的实例
访问某各类或接口的静态变量,或者对静态变量赋值
调用类的静态方法
反射 比如Class.forName(com.dsh.jvm.xxx)
初始化一个类的子类
java虚拟机启动时被标明为启动类的类
JDK 7 开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
运行方法区-线程私有模块
运行方法区结构
hotspot运行方法区
线程私有:
程序计数器
虚拟机栈
栈中包含多个栈帧,每个帧包含
局部变量表
操作栈
动态连接
方法返回地址
本地方法栈
线程公有:
堆外内存:方法区,后被放入本地内存的元空间替代
常量池
方法元信息
类元信息
堆
年轻代
Eden
S0
S1
老年代
JIT编译产物,代码缓存
JVM线程
Hotspot JVM内每个线程和操作系统线程直接映射
操作系统负责所有线程安排调度到任何一个可用CPU上,一旦操作系统本地线程初始化成功,它则会调用java线程中run方法
当普通线程即非守护线程全都执行完毕回收,虚拟机则可以回收
JVM系统线程
虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
编译线程:这种线程在运行时会将字节码编译成到本地代码。
信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
程序计数器
JVM中PC寄存器是对物理PC寄存器的抽象模拟
存储下一条指令的地址,由执行引擎读取下一条指令
如果当前执行naive本地方法,则当前程序计数器为undefined
唯一没规范OOM的区域
指令偏移地址和操作指令 -> PC寄存器 -> 执行引擎 -> 操作局部变量表,操作数栈 -> 机器指令 -> CPU
虚拟机栈
跨平台,指令集小,编译器容易实现
但是性能会下降,同样功能需要更多指令
栈管运行,堆管存储
一个栈帧对应一个方法调用,栈顶正在调用的当前栈帧就是当前方法,定义该方法类就是当前类
方法结束方式有2中:
正常return结束
发生异常,递归的捕获异常,没捕获到则报错
作用
每个栈帧保存每个方法的局部变量(8种基本数据类型,对象引用地址),部分结果,并参与方法的调用和返回
局部变量 -- 成员变量
基本数据变量 -- 引用类型变量
优点
只有进栈,出栈,效率仅次于程序计数器。且不存在垃圾回收问题
Java栈大小可以自己设定动态或固定
动态会OOM,固定会*
虚拟机栈大小,linux默认1mb,windows取决于实际内存大小
一个线程中栈帧是不可能引用另一个线程中栈帧,因为是线程私有的
虚拟机栈内部结构
不存在垃圾回收
一个栈帧包含
局部变量表
一个二维数字数组,保存方法参数和定义在方法体内的局部变量
局部变量所需要大小在编译时期决定,运行时不会改变,线程私有不会有安全问题
局部变量表基本储存单元是slot变量槽,32位为一个索引单位,32位以内占一个slot(包括returnAddress类型),64位(long/double)占两个slot
64bit局部变量使用前一个索引即可
局部变量表中每一个slot分配一个访问索引,通过访问索引找到指定的局部变量值
如果当前栈帧是构造方法或实例方法创建的,则该对象引用this会存放在index为0的slot中
如果某个局部变量出了作用域,则后续新变量可以回收复用前面销毁的局部变量slot位置
成员变量:使用前都有默认初始化赋值
类变量:链接的准备阶段,类变量默认赋值,再initialization具体赋值
实例化变量:随着对象的创建会在堆空间中分配实例变量空间,并进行默认赋值
局部变量:使用前必须显示赋值,否则编译不通过
局部变量表是性能调优的重点,其中变量是重要的垃圾回收根节点,只要被局部变量表直接或间接引用都不会被垃圾回收
操作数栈(表达式栈)
通过数组实现,比如执行赋值,交换,求和等操作
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
比如存一个int值为8,8先进操作数栈,然后从操作数栈转移到局部变量表
相当于JVM执行引擎的工作区,栈帧刚创建时,操作数栈是空的
在编译时期确定了数组栈的深度
32bit占一个栈深度,64bit则两个
栈顶缓存技术
栈是零地址指令,指令小,数量更多
所以将栈顶元素全部缓存在物理CPU寄存器中,降低对内存读写次数,提高执行引擎效率
动态连接(指向运行时常量池方法引用)
每个栈帧内部包含一个指向运行常量池中该栈帧所属方法的引用
这个引用目的就是为了当前方法能够实现动态连接
在java源文件被编译到字节码文件中时,所有变量和方法引用都作为符号引用保存在class文件常量池中。
比如一个方法调用了另外的其他的方法时,就是通过常量池中指向方法的符号引用来表示的。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
常量池作用:为了提供一些符号和常量,便于指令识别。通过符号替代,减小文件开销,直接调用即可,更方便
当一个字节码文件被装入jvm中,被调用的目标方法在编译器可知且运行期不变,该情况下符号引用转直接引用为静态链接。编译期间不可知,运行期间符号引用转直接引用为动态链接。
动态链接也是接口的实现,运行时才知道调用哪个实现,多态的实现
非虚方法:在编译器就确定了调用版本,这个版本运行时不可变的方法
静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法
打破了多态使用前提:1类继承 2方法重写
invokedynamic指令使得Java从静态类型语言具备了动态类型语言特性,使得可以支持json,js等
Java7中通过ASM产生该指令,8后lamda表达式可以直接生成
对于类型检查在编译器则为静态类型语言,运行时则动态
虚方法表
每次动态分配都需要元数据中搜索,为了解决面向对象的频繁动态分配,提高效率,JVM在类方法区建立了虚方法表,使用索引表来查找,每个类中都有一个虚方法表,避免了类似父类一层一层向上判断找直接引用,表中存放各个方法的实际入口。
虚方法表在类加载链接过程中创建
方法返回地址(方法正常或异常退出的定义)
正常退出下,保存调用该方法指令的下一条指令地址
异常退出通过异常表来确定,栈帧中一般不保存信息,不会给上层调用者产生任何返回值
一些附加信息
不确定有,比如对程序调式提供支持的信息
本地方法接口,本地方法库
非java代码接口
本地方法栈和虚拟机栈类似
当某个线程调用一个本地方法时,它就进入了全新且不受虚拟机限制的环境,它和虚拟机有相同的权限
并不是所有JVM都支持本地方法
运行方法区-堆
堆
JVM启动时被创建且大小被确定,是JVM管理最大一块内存空间
堆可以在物理内存上不连续,大小可以自己修改设定
堆中也有划分线程私有的缓冲区TLAB,用来提高并发性
方法结束后,堆对象不会立刻回收,会在GC时回收,堆是GC重点对象
内存细分
JDK8中变化 永久代->元空间
年轻代 (回收频率最高)
Eden 默认占4/5
几乎所有对象都在eden区new出来
Eden区内部还有线程私有的TLAB缓冲区,来解决线程不安全问题,提高吞吐量,称为快速分配策略
JVM将TLAB作为内存分配首选,默认情况TLAB占eden区百分之1
当TLAB分配失败,JVM会用加锁机制保证数据操作原子性,直接在eden空间中分配内存
S0 默认1/5
S1 默认1/5
复制算法的特殊情况
Eden区放不下了,则young gc
young gc能放下了,继续照常。如果仍然放不下,则直接放入老年代
如果老年代仍然放不下,则full gc
如果老年代仍然放不下,则oom。前提是jvm不允许自动调整空间,默认自动会调整
young gc会触发stop the world,会暂停用户线程
老年代
目前只有CMS有单独old gc老年代回收行为,old gc不是full gc。
full gc是整个堆和方法区的垃圾回收
G1还包含 mixed gc,收集整个新生代和部分老年代
full gc会触发stop the world暂停时间更长
触发full gc情况
System.gc()调用,系统会建议执行full gc但是不是必然
老年代空间不足
方法区空间不足
young gc后进入老年代平均大小大于老年代可用内存
eden区放不下,youg gc后仍放不下,然后老年代也放不下
-Xms 设置堆起始内存
-Xmx 设置堆最大内存
一般Xms和Xmx设置相同,目的为了java垃圾回收机制清理完堆后不需要重新分隔计算堆区大小,提高性能
默认下,Xms为电脑可用物理内存/64,Xmx为电脑可用物理内存/4
查看设置的参数 jps -> jstat -gc 进程id
逃逸技术
随着JIT编译器发展和逃逸分析技术的成熟,栈上分配和标量替换优化技术将使得所有对象分配到堆上变得不绝对
经过逃逸分析后发现,一个对象没有逃逸出方法的话,则可能优化成栈上分配,无需堆上分配内存,也无需垃圾回收,这是常见的堆外存储技术
此外TaoBaoVM,其中创新了GCIH(GC invisible heap),将生命周期较长的java对象放入heap外,并且gc不能不能管理GCIH内部java对象来降低GC频率
public void my method(){ V v = new V(); ...... v = null; //在方法内部v即变成null没有发生逃逸,那么可以把该对象分配到栈上,随着方法移除,栈空间也移除 } 比如使用StringBuffer sb时,方法结束时返回sb时候 return sb会逃逸,b.toString()表示返回一个新的string,StringBuffer则不会逃逸
如何快速判断是否发生逃逸
new的实体只在方法内部有效不发生逃逸
jdk7开始hotspot默认开启逃逸技术
使用逃逸分析,编译器可以对代码优化
栈上分配
JIT编译器分析没有逃逸则栈上分配
同步省略
JIT分析如果一个锁对象被发现只能从一个线程被访问到,那么对于这个对象操作可以考虑不同步,也就是锁消除
Object o = new Object();
synchronized(o){
//对o业务操作
}
在这里每个线程来都会各自创建一个新的o,所以会发生锁消除
分离对象或标量替换
有的对象可以不需要连续的存储结构存储也可以访问到,那么这个对象的部分或者全部可以不存在内存,而是存在CPU寄存器中
标量:指无法在分割成更小的数据,Java中原始数据类型就是标量
聚合量:还可以分割成其他标量和聚合量的数据,如Java对象
JIT逃逸分析后,发现没有逃逸,就可以把一个对象分解成两个聚合量并存放在栈中,不需要在堆中再分配内存
比如
private static void alloc(){
Point point = new Point(1,2);
}
class Point{
int x;
int y;
}
这里会被优化成,直接栈中分配x,y不需要new对象放入heap中
private static void alloc(){
int x = 1;
int y = 1;
}
在hotspot中目前对象没有分配使用逃逸分析在分配到栈上, 所有对象分配仍然都在堆上
运行方法区-方法区
Person(在方法区) person (在栈中)= new Person() (在堆中)
Java虚拟机规范中要求方法区逻辑上存在堆中,但是hotspot中独立于堆的内存空间
方法区在JVM创建时创建,大小可固定可扩展
方法区决定了系统可以保存多少类,系统定义太多类会导致OOM
大量第三方jar包,Tomcat 30-50个过多工程部署,大量动态生成反射类
关闭JVM会释放方法区内存
jdk6时完全永久代
jdk7是永久代开始过度改革,将StringTable和静态变量放入堆中,永久代在虚拟机设置的内存内,
jdk8使用本地内存的元空间,完全取消永久代,运行时常量池放入堆中
永久代和元空间是hotspot对方法区规范的实现
永久代默认大小20.75mb,可设置最大空间32位机器64mb,64位机器82mb
元空间默认大小21mb,可设置最大空间值为-1,表示没有限制
在元空间默认21mb下,一旦触及21mb会触发full gc回收没有用的类,然后该21mb值会根据释放多少元空间重新设定
方法区内存结构
jdk1.8后静态变量和运行时常量池放入堆中
类型信息
该类全名 = 包名.类名
该类直接父类的完整有效名(对于interface和java.lang.Object都没有父类)
该类的修饰符(public,abstract,,final某个子集等)
该类直接接口的有序列表
域(变量)信息
域名,域类型,修饰符(public,private,protected,static,volatile,final,transient某个子集等)
方法信息
方法名称,方法返回类型,方法修饰符,方法字节码,操作数栈,局部变量表及大小,异常表
运行时常量池
字节码文件也有常量池,classfile内有魔术,最小最大版本,常量池表,方法信息,执行编译过程等等
在classfile这里的常量池表包含了编译时生成的字面量(字符串""内的值),对类型,域和方法的符号引用,
常量池像数组一样通过索引访问
编译时常量池经过字节码文件类加载后称为运行时常量池,此时符号引用已经变成真实直接引用
运行常量池相比于编译时常量池,具有动态性。比如String在常量池中没有,则动态放入String到常量池中
创建类或接口的运行时常量池所需内存空间超过方法区大小,会OOM
静态变量
non-final的类变量,随着类加载而加载,可以不需要类实例就可以使用
全局常量final static,每个全局常量在编译时即分配,正常static实在类加载链接和初始化时分配的
即时编译器后的代码缓存
永久代为什么要被元空间替换?
JRockit没有方法区,Hotspot和JRockit进行了合并
永久代设置空间大小很难确定,方法区中主要垃圾回收常量池中废弃常量和不在使用的类,
而如何判断类不在使用及其复杂,永久代很难调优
元空间不在虚拟机中,而是用本地内存,元空间仅收本地内存限制,减少full gc频率
为什么jdk1.7将StringTable字符串常量池放入堆中
full gc频率低,开发中有大量String创建销毁,放入堆中能及时内存回收
方法区垃圾回收
JAVA虚拟机规范对方法区回收要求很宽松,可以类不回收,类回收判断条件很复杂,ZGC就不支持类卸载
常量池常量没有被任何地方引用就可以回收
判断类回收
所有类及其子类实例已经回收,
类加载器已经回收,
该类对象没有被任何地方引用,无法任何地方反射访问该类方法
才允许回收
JAVA对象
创建对象步骤
new Object();
其中new是运行常量池中找new直接引用,Object调用构造器
1 判断对象对应的类是否加载,链接,初始化
已加载过则直接使用
未加载过则双亲委派下使用类加载器+包名+类名为key进行查找对应的class文件,如果没找到抛异常,找到则生成class对象
2 为对象分配内存空间
如果内存规整
指针碰撞(用指针分割已用空间和未用空间,指针后未用空间进行分配内存)
Serial,ParNew因为使用标记整理算法,碎片化小,内存规整
如果内存不规整
空闲列表分类,列表记录哪些内存可用和不可用,分配后再更新空闲列表
CMS标记清除算法
3 处理并发安全问题
采用CAS失败重试,区域加锁保证更新的原子性
每个线程预先分配TLAB
4 初始化分配到的内存
所有属性设置默认值,保证对象不设值可以直接使用
5 设置对象的对象头
6 执行init方法进行初始化
类构造器真正赋值
对象内存布局
普通对象
对象头(8字节)
哈希值,GC分代年龄,锁状态标志
类型指针(4字节)
指向元空间类元信息
实例数据(根据具体数据判断,一个int4字节,一个long8字节推类)
对齐填充 (当整个字节不能被8整除时,则填冲到被8整除)
数组对象
对象头(8字节)
类型指针(4字节)
数组长度
对齐填充 (当整个字节不能被8整除时,则填冲到被8整除)
一般Java 类型指针64位,但是JVM默认开启compressedPointer会压缩到4字节。compressedOops也会压缩普通对象字节,如String默认是8字节,但是String会被压缩到4字节
例题
User{
int id;
String name;
}
整个类占多少字节?
对象头8字节,类型指针4字节,int4字节,string4字节,对齐4字节(因为前面只有20字节不能被8整除,自动填充4字节)。总共24字节
对象访问定位
句柄访问
直接指针(Hotspot采用)
直接指针
句柄访问
直接内存
在Java堆外的,直接向操作系统申请的内存空间
基于NIO,通过存在堆中DirectByteBuffer操作native内存
访问直接内存速度大于JAVA堆,读写性能高
所以在读写频繁场合考虑使用直接内存
JAVA的NIO库允许程序使用直接内存,用于数据缓冲区
零拷贝,读写速度快的原理
执行引擎
由软件自己实现,能够执行不被硬件直接支持的指令集格式
执行引擎目的是将字节码解释/编译为对应平台上的本地机器指令
执行什么字节码依赖于PC寄存器
JAVA代码编译过程
源代码,词法分析器,token流,语法分析器,抽象语法树,语义分析器,注解抽象语法树,字节码生成器,JVM字节码
JAVA执行引擎过程
JVM字节码使用JIT编译器或字节码解释器都可以,二选一
JIT编译器生成机器指令(虚拟机将源代码先完整编译成和本地机器相关的机器语言,缓存机器指令,然后再执行)
字节码解释器(通过PC计数器对字节码采用逐行解释字节码文件中内容翻译为对应平台本地机器指令执行)
解释器优点
因为字节码逐行执行,响应速度快
JIT编译器优点
编译完后执行速度快
所以两者同存,可以互补
虚拟机启动时,解释器先发挥作用,随着程序运行时间推移,JIT热点探测功能将有价值的字节码编译成本地指令,发挥作用
可以人为设定只用解释器模式,或者只用JIT编译器模式,或者混合模式
热点代码探测何时确定JIT
前端编译器: java源码生成class字节码文件
后端运行期编译器:JIT编译器中包含C1,C2编译器
静态提前编译器:AOT编译器,直接把java源码编译成机器代码过程
执行频率高的代码叫热点代码,JIT对其直接编译成本地机器指令来提升JAVA性能
HotSpot基于技数器的热点探测
方法调用计数器
用于统计方法的调用次数,
client默认1500次,server默认1000次阈值,超过阈值则触发JIT,也可以人为设定。
然后超过一定时间(半衰周期)限度调用次数不足以触发JIT,会触发热度衰减,统计次数减少一半
回边计数器
统计循环体执行的循环次数,
和方法调用次数一起累加,当超过阈值,触发JIT
JIT中C1和C2编译器
客户端编译器
对字节码简单可靠的优化,耗时短
方法内联:将引用函数代码编译到引用点处,减少栈帧生成,参数传递和跳转过程
去虚拟化:对唯一实现类进程内联
冗余消除:把运行期间不会运行的代码折叠掉
服务端编译器
对字节码较长的优化和激进优化,但优化代码执行效率更高
标量替换:标量值代替聚合对象
栈上分配
同步消除:锁消除
默认服务端编译器
AOT编译器
运行前源码直接编译成机器代码
JDK10后Hotspot加入Graal编译器,性能直追C2编译器,未来可能替代C1C2,AOT借助了Graal
AOT优点
避免了第一次运行慢的预热编译
缺点
违背了一次编译到处运行
降低了JAVA链接过程的动态性,加载代码再编译时期就需要全部已知
还需要继续优化
StringTable
String基本特性
声明final的不能被继承
jdk8以前内部定义 final char[] value用于存字符串数据,jdk9改为byte[]来节约空间
实现了Serializable和Comparable接口,表示String支持序列化和比较大小
不可变,所谓修改都是新开辟空间的新值,旧值是不变的
字符串常量池是一个hashTable存储的,为了避免hash冲突,map长度不能太小
jdk6默认长度1009,jdk7默认60013,jdk8开始最小值是1009,一个map所以不会保存重复的常量池字符串
String内存分配
jdk6再永久代中,7,8以后在堆中
字符串拼接操作
String s = "a"+"b"+"c" 等同于 String s = "abc"放入常量池,因为这是编译器的优化
但是如果拼接变量相当于是new存入堆中 String s1 = "a" String s2 = s1 + "bc" 不等同于 String s3 = "abc"
拼接原理是StringBuilder
intern()使用
返回常量池的地址,如果常量池中没有,创建然后再返回常量池地址
StringTable垃圾回收
G1的String去重操作
未完待续
本文地址:https://blog.csdn.net/weixin_40503364/article/details/107652027
上一篇: ACCESS转化成SQL2000需要注意的几个问题小结
下一篇: 诸葛恪为何能成为吴国的托孤大臣呢?