什么是JVM
定义:
Java Virtual Machine -java 程序的运行环境(Java二进制字节码的运行环境)
好处:
- 一次编写 到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
比较:
JVM JRE JDK
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cc6sPU80-1640008611699)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211215224157480.png)]
学习JVM有什么用
-
面试
-
理解底层的实现原理
-
中高级程序员的必备技能
内存结构
1.程序计数器
定义
Program Counter Register 程序计数器(寄存器)
作用
记住下一条JVM指令的执行地址
特点
-
是线程私有的
-
不会存在内存溢出
2.虚拟机栈
Java Virtual Machine Stacks(Java虚拟机栈)
定义
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
1.垃圾回收是否涉及栈内存?
不涉及,栈内存无疑是一次次的方法调用,等方法执行完成后就会自动弹窗栈内存。
2.栈内存分配越大越好吗?
JVM虚拟机可以通过 -Xss size 参数来指定栈内存的大小,默认情况下Linux、macOs默认情况下为1024K,Windows的默认大小取决于虚拟内存的大小。
栈内存越大,线程数会越少
物理内存是一定的,可以简单理解为 物理内存 = 栈内存 * 线程数
一般采用系统默认的栈内存就可以
3.方法内的局部变量是不是线程安全的?
- 如果方法内局部变量没有逃离方法的作用区域,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用区域,需要考虑线程安全
// 线程安全
public static void func1(){
int a = 10;
int b = 20;
System.out.println(a + b);
}
// 线程不安全
public static void func2(StringBuilder sb){
sb.append(1);
sb.append(2);
sb.append(3);
}
// 线程不安全
public static StringBuilder func3(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
栈内存溢出
栈帧过多导致栈内存溢出
Exception in thread “main” java.lang.StackOverflowError
最常见的情况是方法的递归调用
第三方的库也会导致栈内存溢出
栈帧过大导致内存溢出
线程运行诊断
案例1:CPU占用过多
定位(Linux)
- 用top定位哪个进程对CPU的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是由哪个线程引起的CPU占用过高)
- jstack 进程id
- 可以根据线程ID找到有问题的线程,进一步定位到问题代码的行数
案例2:程序运行很久没有出结果
通过使用jstack来查看是否出现死锁
3.本地方法栈
Native Method Stacks 本地方法栈
不是由Java编写的代码
java有时候不能直接和操作系统底层进行交流
4.堆
定义
通过new关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆内存溢出
java.lang.OutOfMemoryError: Java heap space
可以通过 JVM参数 -Xmx size 来修改堆内存的大小
堆内存诊断
1.jps工具
- 查看当前系统中有哪些Java进程
2.jmap工具
- 查看堆内存占用情况,只能查看那一刻的快照 jmap -heap 进程id
3.jconsole 工具
- 图形界面的,多功能的检测工具,可以连续检测
案例
- 垃圾回收后,内存占用率仍然很高
使用 jvisualvm工具 堆转储 dump 可以进行查看
5.方法区
2.组成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJZef2xa-1640008611700)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211216195745061.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7zMBMlJj-1640008611701)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211216200452621.png)]
3.方法区内存溢出
-
1.8以前会导致永久代内存溢出
java.lang.OutOfMemoryError:PermGen space
修改永久代空间内存大小 -XX:MaxPermSize=8m
-
1.8之后会导致元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
修改元空间内存大小 -XX:MaxMetaspaceSize=8m
4.运行时常量池
- 常量池 就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
5.StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理时StringBuilder (1.8)
- 字符串常量拼接的原理时编译期优化
- 可以使用intren方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回。
为什么把StringTable移到堆中?
永久代的内存回收效率很低,永久代需要full gc的时候才会触发永久代的垃圾回收,full gc只有等待老年代的空间不足才会触发,触发的时机就会优点忘,导致StringTable回收不及时,StringTable中保存的字符串常量使用比较频繁,如果回收不及时,将占用大量的内存,导致永久代内存空间不足。
6.StringTable垃圾回收
-Xmx10m 设置虚拟机堆内存
-XX:+PrintStringTableStatistics 打印字符串表的统计信息
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收的信息
7.StringTable性能调优
StringTable底层是一个哈希表,哈希表的性能和bucket有关,bucket越多,哈希冲突越低。
StringTable的调优主要是调整哈希桶的个数
-XX:StringTableSize=(20000)size
-Xms:表示初始化Java堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
考虑将字符串对象是否入池
将重复的字符串对象入池来减少堆的内存占用
6.直接内存
1.定义
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
传统IO:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZvwBH3XY-1640008611701)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217094341505.png)]
NIO:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WBnSEaxG-1640008611701)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217094615943.png)]
2.直接内存溢出
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
3.分配和回收原理
long base = unsafe.allocateMemory(_1GB);
System.out.println(base);
unsafe.setMemory(base,_1GB,(byte)0);
// 释放内存
unsafe.freeMemory(base);
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的内部实现类,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的方法调用freeMemory来释放直接内存。
-XX:+DisableExplicitGC 禁止显示的垃圾回收
推荐使用 Unsafe来手动管理直接内存
垃圾回收
1.如何判断对象可以回收
1.1 引用计数法
定义
只要一个对象被其他变量引用则计数加一,又被其他的变量引用则计数再加一。
如果变量不再引用该对象则计数减一。
引用计数为零,则可以作为垃圾回收
弊端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3HXMsAUq-1640008611702)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217103202605.png)]
这两个对象互相引用,虽然这两个对象不再使用,但由于引用计数不能归零,导致无法被垃圾回收,造成了内存上的泄漏。
1.2可达性分析算法
定义
可达性分析算法的基本思路就是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为”引用链“(Reference),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象时不可能再被使用。
哪些对象可以作为GC Root
jps 查看进程pid
jmap -dump:format=b,live,file=1.bin 进程id
live参数可以主动触发一次垃圾回收
局部变量引用的对象都可以作为GC Root;方法参数引用的对象也可以作为GC Root
- 在虚拟机栈中引用的对象,例如当前正在运行的方法所用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,例如Java类的引用行静态变量
- 在方法区中常量引用的对象,例如字符串常量池(StringTable)里的引用
- 在本地方法栈中引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointException、OutOfMemeoryError)等。
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存
1.3五种引用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cab9ZM3b-1640008611702)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217113815259.png)]
强引用
强引用是最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj = new Object( )"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远回收不掉被引用的对象。
比如 new 一个新的对象就是强引用
沿着GC Root 引用链能找到该对象,强引用
没有GC Root直接或者间接的引用该对象,就可以被垃圾回收
软引用
软引用是用来描述一些还有用,但非必须的对象。软引用需要一个由GC Root对象指向一个专门的软引用对象(SoftReference对象),然后再由软应用对象指出。仅有软引用对象引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收如上图所示的A2对象;而软引用对象自身则配合引用队列释放自己。
弱引用
弱引用是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生成到下次垃圾收集发生为止。当垃圾回收器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
虚引用
虚引用也称为"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被垃圾收集器回收收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。
终结器引用
Java中所有的类都继承自Object类,在Object类中有一个finalize()方法,如果某类重新了这个方法,那么当没有强引用引用时,虚拟机会创建一个终结器引用指向这个对象,把终结器引用加入到引用队列,再由一个优先级很低的线程Finalizer去调用A4的finalize()方法,第二次GC时才能回收被引用的对象。
2.垃圾回收算法
2.1 标记——清除算法
算法分为”标记“和”清楚“两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象,也可以发过来,标记存活的对象,统一回收所有未标记的对象。使用可达性分析来判定对象是否可回收。
缺点
- 执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要回收的,这时必须进行大量的标记和清除的动作,导致标记和清楚两个过程的执行效率随对象数量增长而降低
- 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
2.2 标记——整理算法
首先标记出所有需要回收的对象,在标记完成后,让所有存活的对象都向内存空间一端移动,然后清理掉边界以外的内存。
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动必须全称暂停用户程序才能进行。
2.3标记——复制算法
标记复制算法将可用内存按容量分为大小相等的两块,每次只使用其中的一块,这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用其中的内存空间一次清理掉。
优点
实现简单,运行高效,不会产生空间碎片。
缺点
这种算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了,而且会产生大量的内存复制开销。
3.分代垃圾回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gV0viZ34-1640008611702)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217200327317.png)]
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 Minor GC,伊甸园和 From 存活的对象使用COPY复制到 TO 中,存活的对象年龄加1并且交换From TO
- Minor GC 会引发Stop the world,暂停其他用户的线程,等待垃圾回收结束,用户线程才能恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15 (4bit)
- 当老年代空间不足,会尝试触发Minor GC,如果之后空间仍不足,那么触发Full GC,Stop the world时间更长
新生代收集 Minor GC : 指目标只是新生代的垃圾收集
老年代收集 Major GC : 指目标只是老年带代的垃圾收集
混合收集 Mixed GC : 指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集 Full GC : 收集整个Java堆和方法区的垃圾收集
相关 JVM 参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或者 -XX:NewSize=size + -XX:MaxNewSize=size |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRation=ration |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
4.垃圾回收器
1.串行
- 单线程
- 堆内存较小,适合个人电脑
2.吞吐量优先
- 多线程
- 堆内存较大,多核CPU来支持
- 让单位时间内,Stop the world的时间最短(时间长 次数短)
3.响应时间优先
- 多线程
- 堆内存较大,多核CPU
- 尽可能让单词Stop the world 的时间最短(时间短 次数多)
1.串行
-XX:+UseSerialGC = Serial + SerialOld
Serial 运行在新生代,使用的回收算法为复制算法
Serial 运行在老年代,使用的回收算法为标记——整理算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jt60I6zE-1640008611703)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217213441676.png)]
2.吞吐量优先
JDK1.8默认使用的就是这个
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jr56BbUd-1640008611703)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217220710934.png)]
-XX:+UseParallelGC
新生代的垃圾回收器,使用的是复制算法
-XX:+UseParallelOldGC
老年代的垃圾回收器,使用的是标记——整理算法
垃圾回收线程数默认是和CPU核数相关的,比如4核CPU就开4个垃圾回收器
-XX:ParallelGCThreads=n
可以控制ParallelGC的线程数
-XX:+UseAdaptivePolicy
动态的调整伊甸园、整个堆内存的大小等
-XX:GCTimeRatio=ratio
1/(1+ratio) ratio默认值为99 ,就是工作100分钟,只能有一分钟用于垃圾回收,如果达不到这个要求,一般会提高堆内存
-XX:MaxGCPauseMillis=ms
默认值是200ms
3.响应时间优先(CMS)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZlqT43OY-1640008611704)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217221947467.png)]
基于标记——整理算法实现
初始标记、重新标记这两步仍然需要"Stop The World"
-XX:+UseConcMarkMarkSweepGC ~ -XX:+UseParNewGC ~SerialOld
可以和用户线程并发执行
-XX:ParallelGCThreads=n
并行的线程数,和CPU默认相同
-XX: ConGCThreads=threads
一般为设置为并行线程数的四分之一
-XX:CMSInitatingOccupancyFraction=percent
执行CMS垃圾回收的内存占比 比如老年代的内存占用到80%的时候就执行一次CMS垃圾回收
-XX:+CMSScavenageBeforeRemark
在重新标记之前,先做一次新生代的垃圾回收
4.G1
定义:Garbage First
- 2004论文发布
- 2009 JDK 6u14体验
- 2012 JDK 7u4官方支持
- 2017 JDK 9默认
使用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记+整理算法,两个区域之间的复制算法
相关 JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1.G1垃圾回收阶段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jigf0vRb-1640008611704)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211217230136419.png)]
类文件结构
1.字节码指令
bipush 10
- 将一个byte压入操作数栈(其长度会补齐4个字节,整数用0补齐),类似的指令还有
- sipush 将一个short压入操作数栈(其长度会补齐4字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是8个字节)
- 这里小的数字都是和字节码指令存在一起,超过short范围的数字存在入了常量池
istore_1
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1 (一号槽位)
ldc #3
- 从常量池加载 #3 数据到操作数栈
- 注意Short.MAX_VALUE 是 32767,所以32768 = short.MAX_VALUE + 1 实际在编译期间计算好的
istore_2
- 从栈顶弹出数据存入局部变量表 slot 2 (二号槽位)
iload_1
- 将一个局部变量(一号槽位)加载到操作数栈
iload_2
- 将一个局部变量(二号槽位)加载到操作栈
iadd
- 对操作数栈上的两个值进行某种特定的运算,并把结果重新存入到操作数栈顶。
istore_3
- 从操作数栈顶弹出数据存入局部变量表 slot 3(三号槽位)
getstatic #4
- 从堆内存中找到类成员变量的引用值放入操作数栈中
iload_3
- 将一个局部变量(三号槽位)加载到操作数栈
invokevirtual指令
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式
invokevirtual #5
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V方法
- 生成新的栈帧(分配locals、stack等)
- 传递参数、执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内存
分析 a++ ++a
- 注意 iinc 指令是直接在局部变量 slot 上进行运算
- a++ 和 ++a的区别是先执行 iload 还是 先执行 iinc
stack 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度
args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
locals: 局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
iinc 1, 1
有两个参数 第一个参数对那个槽位自增,第二个参数是要自增几
条件判断指令
指令 | 助记符 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 != 0 |
0x9b | iflt | 判断是否 <0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
0x9f | if_icmpeq | 两个int是否相等 == |
0xa0 | if_icmpne | 两个int是否 != |
0xa1 | if_icmplt | 两个int是否 < |
0xa2 | if_icmpge | 两个int是否 >= |
0xa3 | if_icmpgt | 两个int是否> |
0xa4 | if_icmple | 两个int是否<= |
0xa5 | if_acmpeq | 两个int是否== |
0xa6 | if_acmpne | 两个引用是否 != |
- byte,short,char都会按 int 比较,因为操作数栈都是4个字节
- goto用来进行跳转到指定行号的字节码
构造方法
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
编译器会按从上至下的顺序,收集所有static静态代码快和静态成员赋值的代码,合并为一个特殊的方法 ()V:
()V
2.编译器处理
所谓的语法糖,其实。就是指Java编译器把 .java源码编译为
class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担。
3.类加载器
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 将类的字节码载入方法区中,内部采用C++ instaceKlass描述 Java类,他的重要域= field;
- _java_mirror即Java的类镜像,例如对String来说,就是String.class,作用就是把Klass暴露给 Java 使用
- _super即父类
- _fields即成员变量
- _methods即方法
- _constants即常量池
- _class_loader即类加载器
- _vtable虚方法表
- _itable接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
验证
- 文件格式验证
- 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:
- 是否已魔数 0xCADEBABE开头
- 主、次版本号是否在当前Java虚拟机接收的范围之内
- 常量池的常量中是否有不被支持的常量类型
- 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:
- 元数据验证
- 第二阶段是对字节码描述信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段可能包括的验证点如下:
- 这个类是否有父类(除了Java.lang.Object之外,所有的类都应当有父类)
- 这个类的父亲是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 。。。
- 第二阶段是对字节码描述信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段可能包括的验证点如下:
- 字节码验证
- 字节码验证是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法、符合逻辑的。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现在操作数栈放置了int数据的类型,使用时却按long类型来加载入本地变量表中
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换总是有效的
- 字节码验证是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法、符合逻辑的。
- 符合引用验证
- 最后一个阶段的效验行为发生在虚拟机将符号引用转化为直接引用的时候,主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出Java.lang.InCompatibleClassChangeError的子类异常,比如Java.lang.NoSuchFieldError、Java.lang.NoSuchMethodError、java.lang.IllegalAccessError等
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
- static变量在 JDK7之前存储于instanceKlass末尾(方法区),从 JDK7开始,存储于_java_mirror末尾(堆中)
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量是final,但属于引用类型,那么赋值也会在初始化阶段完成
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Sysmbolic Reference): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧异地地位到目标即可。
- 直接引用(Direct Reference): 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。
初始化
初始化阶段就是执行类构造器()方法的过程。
- ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块可以赋值,但是不能访问。
- ()方法和类的构造方法函数(即在虚拟机视角中的实例构造器()方法)不同,它不需要显示地调用父类构造器,Java虚拟机会保证()方法执行前,父类的()方法已经执行完毕。因为在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object
- ()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也就么有对变量的赋值操作,那么编译器可以不为这个类生成()方法
- Java虚拟机必须保证()方法的线程安全
初始化发生的时机
概括的说,类初始化是 懒惰的
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发父类的初始化
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致初始化
- 访问类的static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName的参数2为false时
4.类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超于类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类加载器本身一起确立其在Java虚拟机的唯一性,每个类加载器,都有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否”相等“,只有在这两个类是同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器相同,那这两个类必定不相等。
启动类加载器
启动类加载器(Bootstrp Class Loader)是由C++实现的,这个类加载器负责加载存放<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机可以能够识别的(按照文件名识别,如rt.jar,tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库到虚拟机中。
扩展类加载器
扩展类加载器(Extension Class Loader)这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。这是一种Java系统类库扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能。
应用程序类加载器
应用程序类加载器(Application Class Loader): 这个类加载器由sun.misc.Launcher$AppClassLoader()方法来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为"系统类加载器"。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELxCuPVG-1640008611704)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211220093600953.png)]
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传输到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个所需要的类)时,字加载器会尝试自己去完成加载。
自定义类加载器
什么时候需要自定义类加载器
- 想加载非classpath随意路径中的类文件
- 都是通过接口使用来实现,希望解耦时,常用于框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤
- 继承ClassLoader父类
- 要遵从双亲委派机制,重写findClass方法
- 注意不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
运行期优化
即时编译器
Java程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这个代码编译成本地机器码,并以各种手段尽可能地进行优化,运行时完成这个任务的后端编译器被称为即使编辑器。
HotSpot虚拟机内置了两个(或三个)即使编译器,其中两个编译器存在已久,分别为"客户端编译器"(Client Compiler)和"服务端编译器"(Server Complier),或者称为C1编译器和C2编译器。
分层编译(Tiered Compilation)
- 第0层。程序纯解释执行,并且解释器不开性能监控功能(Profiling)
- 第1层。使用客户端编译器将字节码编译为本地方法来运行,进行简单可靠的稳定优化,不开启性能监控功能
- 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等优先的性能监控功能
- 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息
- 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
逃逸分析(Escape Analysis)
栈上分配
标量替换
同步消除
方法内联
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
运行期优化
即时编译器
Java程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这个代码编译成本地机器码,并以各种手段尽可能地进行优化,运行时完成这个任务的后端编译器被称为即使编辑器。
HotSpot虚拟机内置了两个(或三个)即使编译器,其中两个编译器存在已久,分别为"客户端编译器"(Client Compiler)和"服务端编译器"(Server Complier),或者称为C1编译器和C2编译器。
分层编译(Tiered Compilation)
- 第0层。程序纯解释执行,并且解释器不开性能监控功能(Profiling)
- 第1层。使用客户端编译器将字节码编译为本地方法来运行,进行简单可靠的稳定优化,不开启性能监控功能
- 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等优先的性能监控功能
- 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息
- 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
逃逸分析(Escape Analysis)
栈上分配
标量替换
同步消除