JVM学习
概述
java虚拟机——java运行环境
优势
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
概念比较
- JRE:JVM+基础类库
- JDK:JVM+基础类库+编译工具
- JAVASE:JDK+IDE工具
- JAVAEE:JDK+应用服务+IDE工具
基本结构
面试
- 理解底层的实现原理
- 中高级程序猿的必备技能
JVM内存结构
程序计数器——Program Counter Register
概述
-
java源代码->二进制字节码->解释器->机器码->CPU
-
记住下一条JVM指令的执行地址(解释器中)
特点
- 线程私有的
- 线程切换,各个线程的程序计数器记住下一行指定的地址
- 唯一不会存在内存溢出
虚拟机栈——Java Virtural Machine Stacks
概述
- 每个线程运行需要的内存空间
- 栈帧:一个栈由多个栈帧组成
- 每个方法运行时需要的内存
- 参数
- 局部变量
- 返回地址
- 每个方法运行时需要的内存
- 每个线程只能有一个活动的栈帧,对应正在执行的方法
- 方法执行完成,栈帧出战,对应方法的执行完成
特点
- 线程私有
问题辨析
-
垃圾回收是否涉及栈内存?
- 不需要,方法调用完成,栈帧弹出,对应的栈帧内存就会自动回收
-
栈内存分配越大越好吗?
- 内存划分:
-Xss size
- 默认:1024k
- 不是。
- 栈内存更大,只能够增加方法递归调用的数量,对方法的执行效率没有帮助。
- 由于物理内存的大小固定,栈内存分配的越大,可运行的线程数就会减少。
- 内存划分:
-
方法内的局部变量是否是线程安全的?
-
是线程安全的
-
如果方法内局部变量没有逃离方法的作用返回,是线程安全的
-
如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全(可能被其他线程访问修改)
-
每个线程都有自己的方法栈
-
作用于栈帧内局部变量是线程私有的
-
static变量不是——不是存在栈中?
-
-
栈内存溢出stackOverFlow
- 栈帧过多导致栈溢出——递归调用没有正确结束
- 栈帧多大,大于栈内存——不容易出现
线程运行诊断
- CPU占用过多
- 定位
- 使用
top
定位到哪个进程对CPU占用过高 - 使用
ps H -eo pid,tid,%cpu |grep 进程id
,定位哪个线程英气的cpu占用过高 - 使用
jstack 进程id
- 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行数
- 使用
- 定位
- 运行了很长时间不返回结果——死锁
- 使用
jstack
- 使用
本地方法栈——Native Method Stacks
- 调用不是由java编写的方法
- 线程私有
堆——Heap
概述
通过new关键字,创建对象都会使用堆内存
特点
-
线程共享,所以堆中的对象要考虑线程安全问题
-
有垃圾回收机制
内存溢出
OutOfMemoryError
- 大小修改
Xmxsize
堆内存诊断
jps
- 查看当前系统中有哪些java进程
jmap -heap pid
- 查看堆内存占用情况(非实时)
jconsole
- 图形界面的,多工能的检测工具,可以连续监测
案例
-
垃圾回收
jconsole GC
后,堆内存占用率还是很高-
可能相互引用的无法回收
-
jvisualvm
可视化工具(JDK8后不自带自行下载)- 监视->堆dump——获取堆内存快照
- 各个实例对象的占用大小
-
方法区
-
所有java虚拟机线程共享
-
虚拟机启动时被创建
-
逻辑上属于堆的一部分(不同厂商不同)
-
永久代——JDK1.6堆中
-
元空间——操作系统内存1.8
-
存储内容
- Class相关信息
- 成员变量
- 成员方法
- 构造器方法代码
- 方法数据
- 。。。
- 常量池
运行时常量池
-
常量池:常量池是字节码文件的一部分,运行时,常量池中的信息会被加载到运行时常量池,此时各种变量都只是符号,只有运行到的时候才会变成对象
-
注意:s1+s2是使用stringBuffer利用new创建的对象,变量存在于堆中,而s= ‘xxx’,s存在于字符串池中,所以两种变量的地址不同,内容相同
-
s = ‘a’+‘b’ 和 s1 = ”ab“都是直接存在在字符串值,地址相同,内容相同
-
延迟加载对象:字符串对象加载的时候,只有执行到了才会加载字符串内容,并且如果有,则不会再穿件
-
new的字符串对象和string穿件的对象不是同一个,两者存储位置一个在堆(不是引用串,是字符串就在堆中,s.intern可以将堆中的字符串放入串池(有则不会放入——返回串池对象,但不会修改堆对象应用、没有则放入——返回串池对象并引用串池中的对象)),一个在串池
StringTable
特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量的拼接原理是StringBuilder(1.8)——堆中
- 字符串常量拼接的原理是编译期优化——串池中
- 可以使用inter方法,主动将串池中还没有的字符串放入串池
- 1.8将这个字符串对象放入串池,如果有则不会方法,如果没有则会放入,并把串池对象返回值
- 1.6将这个字符串放入串池,如果有则不会方法,如果没有会把此对象复制一份放入串池,把串池对象返回
- 都返回串池对象,但1.8堆中的字符串对象会转入串池,1.6堆中的对象还是堆中的对象
位置
- 1.6存放在永久代常量池中
- 1.8存放在堆中
垃圾回收机制
- 工具
-XX:+PrintStringTableStatistics
:查看串池统计信息-XX:+PrintGCDetils -verbose:gc
:打印垃圾回收信息
- 程序运行完成,字符串还是存在于串池中,当串池达到容量时,会触发GC自动回收,将未被使用的字符串空间回收
stringtable性能调优
- stringtable底层时一个hashtable(数组+链表)
- 数组的长度就是桶数量,使用
-XX:StringTableSize
设置 - 调优思想
- 如果字符串数量多,可以增加StringTableSize大小,防止Hash冲突,降低时间因为要检查是否存在该字符串,Hash冲突的话需要对链表注意检查
- 考虑将字符串对象的是否放入池中
s.intern
推中有很多重复的字符串,入池能够减少内存的使用
元空间内存溢出
- 演示创建多个类占用方法区
ClassLoader
——可以用来加载类的二进制字节码(动态定义类)
OutOfMemoryError:Metaspace
- 元空间内存大小修改
-XX:MaxMetaspaceSize=8m
- 场景
- cglib代理,动态生成代理类
直接内存
概述
- 不属于JVM内存,是操作系统内存
特点
-
常见于NIO操作时,用于数据缓冲区不是传统的阻塞IO操作
-
为什么更快——直接内存Java和系统都能访问,不用多次复制
-
分配回收成本较高,但读写性能高
-
不受JVM内存回收管理
- 内存溢出:
OutOfMemoryError:Direct buffer memory
- 释放原理:由
ReferenceHandler
线程通过Cleaner
的clean
方法使用unsafe.freeMemory()
释放,实际对象被java回收后触发前面直接内存的回收的过程。gc
只能释放java内存 - 注意
-XX:+DisableExplicitGC
禁用显示的垃圾回收System.GC()
(是一种FULL GC),这样也会禁用直接内存的释放,可以直接使用unsafe.freeMemory()
释放直接内存,不用手动GC回收java内存从而触发直接内存回收
- 内存溢出:
垃圾回收
如何判断对象可以回收?
- 引用计数法
- 某个对象被引用则计数+1,计数为0则可以被回收
- 缺点:无法解决循环引用——循环对象引用计数无法为0
- 可达性分析算法(java使用)
- 判断一个对象是否直接或者间接被根对象引用,没有则可回收
- 扫描堆中的对象,看是否能够沿着
GC ROOT
对象为起点的引用链找到对象 - 如何确定
GC ROOT
?哪些对象可以成为GC ROOT
- 使用
Memory Analyzer分析堆快照
- 系统类相关对象
- 本地方法相关对象
- 活动线程相关对象
- 加锁的对象相关对象
- 使用
- 四种引用
- 强引用
- 只有所有GC ROOTs对象都不通过强引用该对象的时候,该对象才能被垃圾回收
- 软引用
-
仅有软引用引用该对象的时候,在垃圾回收后,内存仍不足时就再次触发垃圾回收,回收软引用对象
-
可以配合引用队列来释放软引用自身
ReferenceQueue<T>
-
软引用释放后,软引用对应数据会变为null,可以定义
ReferenceQueue
,当软引用对象被回收,该引用会被自动加入到队列中(用于删除无用的软引用) -
场景:存放较大的非核心数据——强引用引用软引用(
SoftReference
对象),软引用引用非核心数据
-
- 弱引用
- 仅有若引用引用该对象的时候,在垃圾回收时,无论内存是否充足,都会回收若引用对象
- 可以配合引用队列
ReferenceQueue<T>
释放弱引用自身 - 场景:
list
引用WeakReference
->byte[]
- 虚引用
- 必须配合引用队列
ReferenceQueue<T>
使用,主要配合Byte Buffer使用,被引用对象回收时,会将徐引用入队,由ReferenceHandler线程调用虚引用相关方法释放直接内存
- 必须配合引用队列
- 终结器引用
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收)再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
- 强引用
垃圾回收算法
- 在实际的JVM中,更具不同的情况使用不同的算法回收垃圾
- 标记清除算法
- 1.先标记没有被GC ROOT引用的对象(可清除的)
- 2.清除:将内存空间的起始、终止地址记录到空闲地址列表中不会清空数据
- 优点
- 速度快:不需要清除数据
- 缺点
- 容易产生内存碎片:不会做空间整理
- 标记整理算法
- 1.先标记没有被GC ROOT引用的对象(可清除的)
- 2.清除整理:将可用(存活)对象重新整理
- 优点
- 避免内存碎片
- 缺点
- 处理速度慢(对象移动、需要将所有变量的地址进行改)
- 复制算法
- 将内存纷争两个等大的空间
- 1.标记垃圾
- 2.将可用对象复制移动到另一块空白区域(占用连续空间),原空间所有的空间清空(全是无用的对象)
- 优点
- 没有内存碎片
- 缺点
- 需要双倍的空间
分代垃圾回收
-
分区结构
- 新生代
def new generation
- 伊甸园
eden
- 幸存区From
from
- 幸存区To
to
- 伊甸园
- 老年代
tenured generation
- 新生代
-
工作流程
- 1.新创建的对象首先分配在伊甸园区域
- 2.新生代空间不足,触发
minor GC
,伊甸园区和From区存活对象使用复制算法到To区中,存活对象寿命+1,并交换From区和To区引用地址 - 3.如果对象寿命超过指定阈值,该对象移至老年代 (最大15,对象头4bit存储)
- 4.当老年代空间不足,会先尝试触发minior GC,如果空间仍然不足,那么触发full GC
- 大对象直接晋升老年代:如果新生代容量肯定放不大对象,老年代可以,则直接带入老年代
- 注意:
minio GC
会引发stop the world
:暂停用户线程运行,垃圾回收线程进行垃圾回收- 线程内的内存溢出中断不会引起主线程终端
-
相关VM参数
- 堆初始大小
-Xms
- 堆最大大小
-Xms\-XX:MaxHeapSize=size
- 新生代大小
-Xmn\(-XX:NewSize= size+ -XX:MaxNewSize = size)
- 幸存去比例(动态)
-XX:InitialSurvivorRatio=ratio
和-XX:+UserAdapativeSizePolicy
- 幸存区比例
-XX:SurvivorRatio=ratio
- 晋升阈值
-XX:MaxTrnuringThreshold=threshold
- 晋升详情
-XX:+PrintTenuringDistribution
- GC详情
-XX:+PrintGCDetial -verbose:gc
- FullGC前MiniorGC
-XX:+ScavengeBeforeFullGC
- 堆初始大小
垃圾回收器
-
串行
- 特点
- 单线程回收
- 堆内存较小,适合个人电脑(CPU核少)
- 流程
-XX:+UseSerialGC=Serial+SerialOld
- Serial:新生代+复制算法
- SerialOld:老年代+标记整理
- 特点
-
吞吐量优先(JDK8默认)
- 特点
- 多线程
- 堆内存大的情况,多核CPU支持
- 单位时间内,STW的时间最短——单位时间内GC的时间总和要少
- 流程
-XX:+UseParallelGC
——新生代复制算法和-XX:+UseParallelOldGC
——老年代标记整理算法- 默认线程数和CPU核数一样的
-XX:ParallelGCThreads=n
-XX:+UserAdapativeSizePolicy
——新生代自适应调整大小- 调优参数
- (目标)
-XX: GCTimeRatio=ratio
——调整垃圾回收时间和总是简单的占比达到1/(1+ratio)
(堆增大) - (目标)
-XX:MaxGCPauseMillis=ms
——默认200ms,最大STW(堆减小)
- (目标)
- 特点
-
响应时间有限
- 特点
- 多线程
- 堆内存大的情况,多核CPU支持
- 垃圾回收的时候,单次STW时间尽可能最短——可以多GC,单每次都要小
- 流程
-
-XX: +UseConcMarkSweepGC
(老年代——标记清除——有碎片——CMS并发失败会使用SerialOld
垃圾回收器)-XX:+UseParNewGC
(新生代)() -
调优参数
XX:ParallelGCThreads=n
一般设为核数,-XX:ConcGCThreads=threads
并发回收线程数(设置为前者的1/4,其余运行用户线程)-XX: CMSInitiatingOccupancyFraction=percent
——合适进行垃圾回收(不能满之后再回收,需要流出空间用于并发时的浮动垃圾)-XX:+CMSScavengeBeforeRemark
——重新标记阶段前做先对新生代gc
-
注意:
- 老年代使用并发标记清除,会产生大量的空间碎片,导致并发失败,那么该回收器会退化为串行垃圾回收老年代——标记清除整理,这会导致STW时间很长
- 会扫描整个堆内存
-
G1
-
历程
- JDK6——体验
- JDK7——官方支持
- JDK9——默认——取代CMS
-
适用场景
- 同时注重吞吐量(Throughput)和低延迟(Lowlatency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记+整理算法,两个区域之间是复制算法
-
相关参数
-XX:+UseG1Gc
——开启G1、默认-XX:G1HeapRegionSize=size
——设置regin大小:必须设置为2^n-XX:MaxGCPauseMillis=time
——暂停目标设置,默认200ms
-
回收过程
- 新生代回收
Young Collection
- 新创建的对象被放入伊甸园分区中
- 达到伊甸园分区占比,触发新生代回收
- 新生代回收,STW,伊甸园对象被复制整理进幸存区
- 幸存区达到晋升条件的复制进老年区
- 新生代回收的跨域引用(确定GC ROOT)——老年代引用新生代
- 老年代被分为512K的卡表
- 引用了新生代对象的表被标记为脏表
- 查找GC Root的时候,从脏卡列表
remenbered Set
中查询,提高速速 - 每次引用变更的时候,通过
post-write barrier
+dirty card queue
异步进行更新
- 新生代回收
Young Collection
+并发标记Consurrent Collection
Young GC
的时候会进行GC Root
的初始标记- 当老年代区数量达到一定阈值比例,触发并发标记(不会STW)
-XX: InitiatingHeapOccupancyPercent=percent(默认45%)
- 混合回收
Mixed Collection
- 在这个过程中会对E、S、O进行全面垃圾回收
- 1.最终标记(发生STW)
- 2.拷贝存活(发生STW)
- 对老年代区域进行回收的时候,只会选取部分回收价值高的老年区进行回收(为了达到
-XX:MaxGCPauseMi11i5=ms
这一目标 )
- 新生代回收
-
-
- 特点
-
full GC
- SerialGC
- 新生代内存不足发生的垃圾收集-minorgc
- 老年代内存不足发生的垃圾收集-fiullgc
- ParallelGC
- 新生代内存不足发生的垃圾收集-minorgc
- 老年代内存不足发生的垃圾收集-fullgc
- CMS
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足
- 回收速度>用户线程产生垃圾的速度——并发垃圾收集,有STW,但很短,称为minor gc
- 回收速度<用户线程产生垃圾的速度——退化为串行收集,有STW,很慢,称为Full Gc
- 空间碎片太多——Full GC
- G1
- 新生代内存不足发生的垃圾收集-minorgc
- 老年代内存不足
- 回收速度>用户线程产生垃圾的速度——并发垃圾收集,有STW,但很短,称为minor gc
- 回收速度<用户线程产生垃圾的速度——退化为串行收集,有STW,很慢,称为Full Gc
- SerialGC
-
标记流程
- 从GC Rootk开始处理对象,检查对象是否被请引用,如果没有被引用,则会被标记为垃圾
- 在并发标记中,假设C对象已经被标记为垃圾,标记完成,但被另一个对象引用了,那么此时仍然回收C不合理
- 解决方案:当被标记的对象的 引用变更
- 1.触发写屏障指令
pre-write barrier
:将C加入队列satb_queue
,将C标记为未未处理完成 - 2.最终/重新标记:STW,重新检查队列,如果为强应用被标记为强应用——不回收
- 1.触发写屏障指令
-
G1优化
- JDK8u20字符串去重
- 优点:节省了大量内存
- 缺点:略微多占用了CPU时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
- 工作流程
String s1 = new String("hello")
底层一个char[]{}数组,s1->String->char[]- 将所有新分配的字符串放入一个队列
- 当新生代回收的时候,G1并发检查是否有字符串重复,如果值是一样的,String对象将引用同一个char[]
- 注意:与String.intern()不一样
- Stirng.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不用的字符串表
- 去重后,虽然指向了同一个char[],但是变量指向的是String对象,不是直接指向char[],即不同变量指向不同的String对象,String对象指向相同的char[]
- 所以Stirng.intern关注的是String对象,字符串去重关注的是字符串对象引用的char[]数组,纬度不同
- JDK 8u40 并发标记类卸载
- 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX: +ClassUnloadingWithConcurrentMark
——默认启用
- JDK 8u60 回收巨型对象
-
如果一个对象大于一个region一般的时候,就被称为巨型对象
-
G1不会对巨型对象进行拷贝——拷贝用时太大
-
回收时被优先考虑
-
G1会更总老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代回收时处理掉
-
老年代没有对象引用巨型对象,巨型对象就会在新生代被回收
-
- JDK9并发标记起始时间的调
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9之前需要使用-XX:InitiatingHeapoccupancyPercent
- JDK9可以动态调整
- -XX:InitiatingHeapOccupancyPercent 用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空
- JDK9更高效的回收
- 250+增强
- 180+bug修复
- https://docs.oracle.com/en/java/javase/12/gctuning
- JDK8u20字符串去重
垃圾回收调优
- 调优领域
- 内存
- 锁竞争
- cpu占用
- io
- 调优目标
- 低延迟还是高吞吐量,选择合适的回收器
- CMS、G1,ZGC——低延迟——互联网
- ParallelGC——高吞吐量——科学计算等
- 低延迟还是高吞吐量,选择合适的回收器
- 最快的GC是不发生GC
- 查看FUllGC前后的内存占用,考虑以下的问题
- 数据是不是太多?——如大量sql中的数据加载到java内存再筛选
- 数据表示是不是太臃肿?
- 对象图——用到什么就查询什么
- 对象大小
- 是否存在内存泄漏?
- static map不断放入对象
- 解决:软弱引用、第三方缓存实现
- 查看FUllGC前后的内存占用,考虑以下的问题
- 新生代调优
- 新生代的特点
- 所有new操作的内存分配非常廉价(伊甸园中)
- TLAB
thread-local allocation buffer
——每个线程都会在伊甸园中分配一个私有区域TLAB(线程局部分配缓冲区)
- TLAB
- 死亡对象的回收代价是零
- 大部分对象用过即死——新生代回收的时候,大部分对象都是死的(所以minorGC的标记时间<复制时间———少量复制时间短)
- Minior GC的时间远远低于Full GC
- 所有new操作的内存分配非常廉价(伊甸园中)
- 新生代调优
-Xmn
不是越大越好- 新生代太大,老年代越小,会引发老年代触发Full GC
- 建议25%~50%
- 并发量*(请求-响应)
- 幸存区达到能够保留当前活跃对象+需要晋升对象
- [幸存区大小]如果幸存区不够,JVM可能会提前晋升对象到老年代——需要Full GC才能清除
- [晋升阈值]同时也需要合理配置晋升阈值,让长时间存活的对象尽快晋升(否则minor GC会不断复制对象,本来该过程最占时间的就是复制对象)
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
- 老年代调优
CMS
为例- 并发标记清除:由于是并发清除,所以需要预留空间给浮动垃圾,一旦清理速度<垃圾产生速度,会触发STW,退化为串行清理FUll GC
- [老年代大小]越大越好
- 先尝试不做调优,如果没有full GC就不用,都则先尝试调优新生代
- [流出更多的空间给浮动垃圾——避免Full GC]观察full GC时老年代内存的占用,将老年代内存预设调大1/4~1/3
-XX:CMSInitaitingOccupancyFraction=percent
——老年代占多少的时候进行回收(75%~80%)预留一定空间给浮动垃圾
- 新生代的特点
类文件.class
类文件结构
- 文件信息
- magic
- 0~3字节
- 类文件类型
- minion_version
- 2字节
- 小版本
- major_version
- 两字节
- JDK版本信息
- constant_pool_cout
- 两字节
- 常量池项数:注意#0不计入,只计入从#1开始
- constant_pool
- 常量池信息
- access_flags
- 2字节
- 访问标识符
- this_class
- 2字节
- 这个类的信息
- super_class
- 2字节
- 父类信息
- interfaces_count
- 2字节
- 接口数量
- interfaces
- 2字节
- 接口
- fileds_count
- 2字节
- 成员变量数量
- fileds
- 成员连变量信息
- method_count
- 2字节
- 方法数量
- methods
- 方法信息
- sttributes_count
- 2 字节
- 类额外属性数量
- sttributes
- 类的额外信息
- magic
字节码文件可视化
- javap——可视化字节码文件
javap -v xxx.class
代码执行过程
- 1.常量池载入运行时常量池(类文件的常量池->方法区中的运行时常量池)
- 2.方法字节码载入方法区
- 简单的数据直接存放在方法区,大的数据存放在运行时常量池
- 3.main方法开始运行,分配栈帧内存(局部变量表
locals
大小+操作数栈stack
大小) - 4.执行引擎执行代码
常用指令
局部变量
- 局部变量表使用
istore_x
,x为槽位号——从操作数栈中弹出放入指定槽位 push
从方法区/运行时常量池将数押入操作数栈iload
从槽位加载数押入操作数栈
运算
- 先将需要的操作数押入操作数栈
- 执行引擎从操作数栈取出操作数,进行运算,运算结果入栈
自增运算
-
iinc
,在槽位上进行自增 -
i++
先iload
再在槽位自增iinc
——栈中不影响 -
++i
先iinc 再iload
-
x=x++/++x都是先运算右边的,最后使用操作数栈中的数赋值,所以注意时先自增还是先load
-
以上为非静态变量的自增,静态变量的自增为:
getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //执行加操作(操作数栈获取) putstatic //栈顶元素放入静态变量槽位
条件判断
- 注:byte、short、char都会按int比较,因为操作数栈都是4字节
- goto用来进行挑战到指定行号的字节码
循环控制指令
- 判断+goto实现
构造方法
<cinit>()V
——类的构造方法- static静态代码块、静态成员变量赋值代码,合并为特殊的方法
<cinit>()V
- 从上至下的顺序
- static静态代码块、静态成员变量赋值代码,合并为特殊的方法
<init>()V
——实例对象的构造方法- 收集所有的初始化代码块{}、成员变量赋值代码块、构造方法代码,形成新的构造方法 (原始构造方法内的代码总是在最后面)
- 从上至下的顺序、构造方法代码在最后
方法的调用
- 调用方法的指令类型
invokespecial
——构造方法、私有方法、final方法(类唯一确定)invokevirtual
——普通publiv方法(不唯一,动态绑定,可以重写)invokestatic
——静态方法(类唯一确定)
- 方法调用过程
new
过程——可能出现指令重排new
:堆中分配中间dup
:复制到操作数栈(两份)invokespecial
:出栈调用构造方法astore
:出栈存储到局部变量表
invokespecial
和invokevirtual
过程- 对象入栈
- 对象出栈调用方法
invokestatic
过程- 通过对象调用静态方法
- 对象入栈
- 对象出栈
- 调用方法
invokestatic
- 静态方法调用和对象无关,前两步指令多余
- 通过类调用静态方法
- 调用方法
invokestatic
- 调用方法
- 通过对象调用静态方法
invokevirtual
多态调用原理(确定应该调用哪个方法,方法的地址?)
- 当执行invokevirtual指令的时候:
- 1.先通过栈帧中的对象引用找到对象
- 2.分析对象头,找到对象的实际class
- 3.Class结构中有
vtable
(虚方法表:记录了虚方法的入口地址——该带调用哪个类的方法),在类加载的链接阶段就已经根据方法的重写规则生成好了- 连接过程:将.class文件加载到内存中,并创建对应class类
- 4.查表过的方法的具体地址
- 5.执行方法的字节码
异常处理try-catch
-
多了一个异常表
- 监控
[from,to)
的代码块,如果发生异常,和type
匹配,匹配成功执行target
代码 - 局部变量表中也会存储异常变量先istore异常变量,在catch代码
- 如果是多
catch
结构,局部变量表也只会有一个槽位(只可能发生一种catch分支) - 示例
public static void main(String[] args) { int i = false; try { i = true; } catch (Exception var3) { i = true; } }
Code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 12 8: astore_2 9: bipush 20 11: istore_1 12: return Exception table: from to target type 2 5 8 Class java/lang/Exception LineNumberTable: line 13: 0 line 15: 2 line 18: 5 line 16: 8 line 17: 9 line 19: 12 LocalVariableTable: Start Length Slot Name Signature 9 3 2 e Ljava/lang/Exception; 0 13 0 args [Ljava/lang/String; 2 11 1 i I
- 监控
fianlly
-
将
finally
中的指令分别复制到try块中和catch块中(末尾) -
还包括一个未捕获的异常(不匹配)的异常代码块在最后,finally代码块也会复制到此处
-
无论正常执行、发生匹配的异常或者发生了未捕获的异常都会执行finally
-
面试题
finally
和catch
中出现return
——无论发生异常还是不发生异常,最终都会ireturnfinally
中的数据finally
中修改需要返回的值,try
中return
返回值——return i会执行以下操作- 1.
iload_x
将返回值入栈 - 2.
istore
将返回值存入一个新的局部变量槽位固定返回值,防止后续修改 - 3.执行finally代码块
- 4.将上面istore的值iload
- 5.
ireturn
栈顶元素
- 1.
-
示例
-
java代码
public static void main(String[] args) { int i = 0; try { i = 10; }catch(Exception e){ i = 20; }finally{ i = 30; } }
-
指令
Code: stack=1, locals=4, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: bipush 30 //finally 7: istore_1 8: goto 27 11: astore_2 12: bipush 20 14: istore_1 15: bipush 30 //finally 17: istore_1 18: goto 27 21: astore_3 //发生其他未匹配异常 22: bipush 30 //finally代码块 24: istore_1 25: aload_3 //加载其他异常到操作数栈 26: athrow //抛出异常 27: return Exception table: from to target type 2 5 11 Class java/lang/Exception 2 5 21 any 11 15 21 any LocalVariableTable: Start Length Slot Name Signature 12 3 2 e Ljava/lang/Exception; 0 28 0 args [Ljava/lang/String; 2 26 1 i I
-
synchronized
代码块
-
流程
- 1.锁对象入栈
- 2.对象复制(一个给monitor Enter使用,一个给Monitorexit使用)
- 3.存储最上面的锁对象到新槽位
- 4.monitorenter消耗剩下的一个锁对象
- 5.执行业务代码
- 6.加载存储的锁对象
- 7.monitorexit使用对象结果
- 无论业务代码是否异常,或者6、7这两部发生异常,都会重复6.7这两步进行解锁(6.7实际就是finally一样的逻辑都是复制指令,只是会对6.7进行异常监控)。
-
示例
-
java代码
public static void main(String[] args) { Object o = new Object(); synchronized (o){ System.out.println("ok"); } }
-
指令
Code: stack=2, locals=4, args_size=1 //=========创建对象 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 //=============sychronized 8: aload_1 //锁对象入栈(synchronized开始) 9: dup //多对象复制 10: astore_2 //出栈,分配新的槽位(后续monitorexit使用) 11: monitorenter //监控lock,moniterenter使用剩下的锁对象 //-------业务代码 12: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #13 // String ok 17: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V //--------业务代码结束 20: aload_2 //锁对象取出 21: monitorexit //退出监控 22: goto 30 //跳转到30行结束 //=======业务代码异常的话执行 25: astore_3 26: aload_2 //取出锁对象 27: monitorexit //退出监控 28: aload_3 //加载异常对象 29: athrow //抛出 30: return Exception table: from to target type 12 22 25 any 25 28 25 any //如果发生异常,解锁阶段还是异常,重新解锁 LocalVariableTable: Start Length Slot Name Signature 0 31 0 args [Ljava/lang/String; 8 23 1 o Ljava/lang/Object;
-
编译期处理(语法糖)
编译器会在编译的时候会自动生成一些代码
默认构造器
-
当自定义的类中不存在任何构造器,java编译之后的.class会自动生成一个默认的无参构造器
-
示例
-
java代码
public class DefaultConstruct { }
-
编译器生成的代码
public class DefaultConstruct { public DefaultConstruct() { //编译自动生成的默认无参构造器 } }
-
自动拆装箱
-
在使用包装类和基本类型自动转换的时候,编译器会自动添加拆装箱的代码
-
注:Integer在-127~128的时候会复用对象,超出这个返回才会new创建
-
示例
-
java代码
public static void main(String[] args) { Integer i = 1; int y = i; }
-
编译器生成的字节码反编译
public static void main(String[] var0) { Integer i = Integer.valueOf(1); int y = i.intValue(); }
public static void main(String[] var0) { Integer var1 = 1; int var2 = var1; }
-
范型擦除
-
在编译的时候,会将范型擦除,全部当作Object类型进行编译在编译的时候不关注范型的类型,统一当作Object类
-
在取范型的时候会经历两个阶段
- 1.当作Object取范型对象
- 2.使用
checkcast
强制转换为范型类型
-
擦除的是字节码code上的范型信息,但多出了一张表LocalVariableTypeTable,signiture保存了范型的具体类型
-
通过反射只能获取返回值类型、参数类型的范型类型,而局部变量的范型类型无法通过反射获取
-
示例
-
java代码
public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(3); Integer i = list.get(0); }
-
反编译代码
public static void main(String[] var0) { ArrayList var1 = new ArrayList(); //没有范型,统一当作var类型(Object) var1.add(3); Integer var2 = (Integer)var1.get(0); //取范型对象使用强制转换 }
-
指令
Code: stack=2, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: iconst_3 10: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 13: invokevirtual #16 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z ——范型擦除,当作Object类 16: pop 17: aload_1 18: iconst_0 19: invokevirtual #20 // Method java/util/ArrayList.get:(I)Ljava/lang/Object; //取对象,还是当作Object类 //然后强制转换 22: checkcast #11 // class java/lang/Integer 25: astore_2 26: return
-
可变参数
-
方法的传入参数为
String... strs
,编译之后实际为一个String[]
-
如果不传参数,相当于传入了一个空对象
new String[]{}
,而不是null -
示例
-
java代码
public static void foo(String... strs){ String[] str = strs; System.out.println(Arrays.toString(str)); } public static void main(String[] args) { foo("hello","world"); foo(); }
-
反编译
public static void foo(String[] strs){ String[] str = strs; System.out.println(Arrays.toString(str)); } public static void main(String[] args) { foo(new String[]{"hello","world"}); foo(new String[]{}); }
-
for-each循环
-
普通数组遍历
-
foreach底层还是使用的是for-i
-
示例
-
java代码
public static void main(String[] args) { int[] nums = {1,2,3,4,5,6,7,8}; //1.创建编译后还是使用new for (int num:nums){ //2.for-each反编译还是使用for-i System.out.println(num); } }
-
反编译
public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8}; //还是使用new创建对象 for(int i = 0; i < nums.length; ++i) { //使用的还是for-i int num = nums[i]; System.out.println(num); } }
-
-
-
集合类型遍历
-
底层使用的是迭代器遍历
iterator
-
必须实现了Iterable接口的集合才能使用——有迭代器
-
示例
-
java代码
public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7); for (int num:list){ System.out.println(num); } }
-
反编译
public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7); Iterator var2 = list.iterator(); //获取迭代器 while(var2.hasNext()) { //使用迭代器的进行while循环 int num = (Integer)var2.next(); System.out.println(num); } }
-
-
switch
-
从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖
-
字符串switch
-
底层使用两个switch进行分支选择
- 1.switch通过hashcod()+.equal确定分支,确定分支后赋值临时变量(hashcode快速确定分支,.equal()防止hash冲突)
- 2.switch通过临时变量确定执行的分支
-
示例
-
java代码
public static void choose(String str){ switch (str){ case "hello":{ System.out.println("h"); break; } case "world":{ System.out.println("w"); break; } } }
-
反编译代码
public static void choose(String str) { byte x = -1; //临时变量存储执行分支 switch (str.hashCode()) { //使用hashcode初始选择 case 111111: if (str.equals("hello")) { //使用equals防止hash冲突 x = 0; } break; case 222222: if (str.equals("world")) { x = 1; } } switch (x) { //新的switch分支执行分支代码 case 0: System.out.println("h"); break; case 1: System.out.println("w"); } }
-
-
-
枚举类switch
-
自动生成一个合成类,使用int[] 存储不同的序号值
ordinal+1
-
case通过枚举类对象的xx.ordianl()确定合成类int中对应int[xx.ordinal]存储的值
-
通过值选择分支语句
-
示例
-
java
public static void foo(Sex sex) { switch (sex) { case MALE: System.out.println("男"); break; case FEMALE: System.out.println("女"); } }
-
反编译
/** * 合成类 * - 生成一个int数组,存储不同对象的值,所谓条件选择 * - 值通过枚举类对象的序号存储在数组中 * - 使用静态进行赋值*/ static class $MAP{ static int[] map = new int[2]; static { map[Sex.MALE.ordinal()] = 1; map[Sex.FEMALE.ordinal()] = 2; } } /** * 通过枚举类对象的序号确定数组中的值 * 通过值选择分支 * */ public static void foo(Sex sex){ int x = $MAP.map[sex.ordinal()]; switch (x){ case 1: System.out.println("男"); break; case 2: System.out.println("女"); break; } }
-
-
枚举类
-
底层原理
- 继承Eunm
- 静态对象,通过私有构造方法在静态代码块创建对象
- 还有一个对象数组存储所有的对象实例
- 每个枚举类实例都有你自己的枚举编号
ordinal
-
示例
-
java代码
public enum Sex { MALE, FEMALE }
-
反编译代码
public final class Sex extends Enum<Sex>{ public static final Sex MALE; public static final Sex FEMALE; private static final Sex[] $VALUES; //存储所有示例对象 //初始化赋值 static { MALE = new Sex("MALE",0); FEMALE = new Sex("FEMALE",1); $VALUES = new Sex[]{MALE,FEMALE}; } //私有构造器 private Sex(String name, int ordinal){ super(name,ordinal); } public static Sex[] values(){ return $VALUES.clone(); } public static Sex valueOf(String name){ return Enum.valueOf(Sex.class,name); } }
-
try-with-resource
-
JDK7新增堆需要关闭的资源处理的特殊方法
-
使用这个功能,资源类必须实
AutoCloseable
接口 -
编译期自动生成变比资源代码
-
示例
-
java
public static void main(String[] args) { try(FileInputStream file = new FileInputStream("1.txt")) { System.out.println(file); } catch (Exception e){ e.printStackTrace(); } }
-
反编译
public static void main(String[] args) { try { FileInputStream file = new FileInputStream("1.txt"); Throwable t = null; try { System.out.println(file); }catch (Throwable e1){ //t为业务代码异常 t = e1; throw e1; }finally { //关闭资源 if (file!=null){ //如果业务代码有异常 if (t!=null){ try { file.close(); }catch (Throwable e2){ //如果close出现异常,作为被压制异常添加 t.addSuppressed(e2); } }else { //如果业务代码没有异常,close出现的异常就是最后catch的异常 file.close(); } } } }catch (IOException e){ e.printStackTrace(); }
-
方法重写时的桥接方法(返回值多态重写)
- 重写方法语法
-
父子类的返回值完全一致
-
子类返回值可以时父类返回值的子类——合成重写方法间接调用
- 虚拟机通过
synthetic bridge
合成一个父类相同的方法(重写)只对JVM可见 - 桥接方法调用本类的子类返回类型的方法(方法返回Integer,桥接方法的返回值类型为Number——多态,可以接收)
- 虚拟机通过
-
示例
-
java
class A{ public Number m(){ return 1; } } class B extends A{ @Override public Integer m(){ //Integer是Number的子类 return 2; } }
-
反编译
class B extends A{ public synthetic bridge Number m(){ //合成父类重写方法(使用synthetic bridge) //调用Integer的m方法 return m(); } public Integer m(){ return 2; } }
-
-
*匿名内部类(引用变量为final)
-
匿名内部类在编译的时候会自动生成一个新的类,在调用的时候使用new 创建这个类的实例
-
匿名内部类引用局部变量的时候(内部类中使用了局部变量),局部变量必须是final的——即使不指定,编译中也会变异成final,反正该变量不能修改
- 局部变量会被视为额外生成类的属性,并生成一个全参构造器生成实例
- 在创建额外类的对象的时候,就已经将变量的值赋给对象的属性了,修改局部变量的值,属性值不会跟随改变
-
示例
-
无引用变量
-
java
public class Main { public static void main(String[] args) { Runnable run = new Runnable() { //匿名内部类 public void run() { System.out.println("OK"); } }; } }
-
反编译
public class Main { //额外生成的类 final static class Main$1 implements Runnable{ Main$1(){} public void run(){ System.out.println("OK"); } } public static void main(String[] args) { Main$1 runnable = new Main$1(); } }
-
-
引用局部变量
-
java
public class Main { public static void test(int x){ //此处没有指定x //====此处不能再修改x的值=========== Runnable runnable = new Runnable() { public void run() { System.out.println("OK" + x); //匿名内部类引用了x } }; new Thread(runnable).start(); } public static void main(String[] args) { test(1); } }
-
反编译JDK21
public class Main { public Main() { } public static void test(final int x) { //编译期加上了final Runnable runnable = new Runnable() { public void run() { System.out.println("OK" + x); } }; (new Thread(runnable)).start(); } public static void main(String[] args) { test(1); } }
-
底层
public class Main { //生成的额外类 final static class Main$1 implements Runnable{ //引用的局部变量当作属性值 int val$x; //局部变为属性值 Main$1(int x){ this.val$x = x; } public void run(){ System.out.println("OK" + this.val$x); } } public static void test(final int x){ //创建匿名内部类 Main$1 runnable = new Main$1(x); new Thread(runnable).start(); } public static void main(String[] args) { test(1); } }
-
-
类加载阶段(类的生命周期)
概述
- 类加载阶段可分为:加载->连接->初始化
- 加载:将字节码
.class
文件通过类加载器加入到方法区,并生成Class的实例- 通过类的全名获取二进制数据流
- 解析类的二进制数据流为方法区内的数据结构(Java类型模版)
- 创建Java.lang.Class类的实例对象,表示该类型,作为方法区这个类的各种数据的访问入口
- 整个过程由类加载器完成(类加载器只在这个阶段作用)
- 加载:将字节码
加载
概述
- 将类的字节码载入到方法区中,内部采用C++的instanceKlass(数据结构)描述java类,它的重要的字段field有:
- _java_mirror即java的类镜像,例如对String来说,String.class,作用就是把klass暴露给java使用(java使用类对象)
- _super即父类
- _fields即成员变量
- _methods即方法
- _constants 即常量池
- _class_loader即类加载器
- _vtable即虚方法表
- _itable即接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行
- 注意:
- instanceKlass这样的[元数据]是存储在方法区中的(1.8后的元空间)内,但_java_mirror存储在堆中——class对象(也是一种对象),_java_mirror引用堆中的class对象
- instanceKlass中_java_mirror持有clas s对象的地址,class对象头中也持有instanceKlass对象的地址
- 类的实例对象头中持有class类对象的地址
- 反射:对象—(实例对象头class地址)—>class对象—(class对象头的instanceKlass对象地址)—>instanceKlass对象—(字段)—>类相关的父类、成员变量、方法…
instanceKlass、class对象、示例对象的存储结构
链接
概述
- 链接可以分为三个小阶段:验证-准备-解析
- 验证:验证类文件 是否符合JVM规范
- 准备:为static变量分配空间、设置默认值
- 解析:将常量池中的符号引用解析为直接引用
- LoadClass只会加载,不会解析和初始化
- 不解析只是一个符号不知道其在内存中的位置,解析之后才会知道类具体的地址
- 不使用的时候就不会解析(懒加载)
验证
准备
-
为static变量分配空间,设置默认值
-
注意
- 1.JDK7之前,static变量存储于instanceKlass末尾,之后才存储到class对象中
- 2.static变量分配空间和赋值是两个步骤
- 无fianl修饰
- 分配空间——准备节点
- 赋值——初始化阶段
- 有final修饰
- 如果static变量是final的基本类型,以及字符串常量,那么编译阶段赋值
- 如果static变量是final,但属于引用类型,赋值在初始化阶段
- 无fianl修饰
-
示例
-
java代码
public class Test { static int i; static int j = 10; static final int k = 10; static final String s1 = "dearfriend"; static final Object o = new Object(); }
-
字节码文件
//========内存分配——链接准备阶段========= { static int i; descriptor: I flags: (0x0008) ACC_STATIC static int j; descriptor: I flags: (0x0008) ACC_STATIC static final int k; descriptor: I flags: (0x0018) ACC_STATIC, ACC_FINAL ConstantValue: int 10 //直接初始化 static final java.lang.String s1; descriptor: Ljava/lang/String; flags: (0x0018) ACC_STATIC, ACC_FINAL ConstantValue: String dearfriend //直接初始话化 static final java.lang.Object o; descriptor: Ljava/lang/Object; flags: (0x0018) ACC_STATIC, ACC_FINAL //===========构造器=========== public com.atguigu.cloud.Test(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/atguigu/cloud/Test; //=========初始化阶段============= static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=2, locals=0, args_size=0 0: bipush 10 2: putstatic #2 // Field j:I //j赋值 5: new #3 // class java/lang/Object 8: dup 9: invokespecial #1 // Method java/lang/Object."<init>":()V 12: putstatic #4 // Field o:Ljava/lang/Object; //对象赋值 15: return LineNumberTable: line 13: 0 line 18: 5 }
-
解析
*初始化
概述
- 初始化即调用
<cinit>()V
——执行static{}
,虚拟机会保证这个类「构造方法」的线程安全 - 初始化时初始化类的信息
- 此处说的
static{}
- 代码中的
static{}
- 编译时生成的
static{}
——静态成员变量
- 代码中的
发生的时机
-
类的初始化是懒惰的
- main方法所在的类,总过被首先初始化
- 首次访问这个类的静态变量或者静态方法的时候(非final)
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类初始化
- Class.forName
- new会导致初始化
-
不会导致初始化的情况
- 访问static final静态常量(基本类型、字符串类型)不会
- 类.class不会发生初始化——在加载的时候就已经生成了class对象
- 创建该类的数组不会触发
- 类加载器的loadClass方法
- Class.forName的参数2为false时(因为可以直接通过加载阶段生成的java_mirror对象找到class对象)
-
应用
-
单例模式——懒加载
-
概念辨析
- (此处说的是类加载时机——多久加载类,不是懒汉式和饿汉式——多久创建对象)
- (饿汉式在类初始化的时候对象就被创建、懒汉式在调用获取实例方法的时候才会被示例化)
-
线程安全
-
调用外部静态方法,不会初始化内部类,创建对象
-
获取实例方法的时候才会初始化内部类,创建对像
-
-
java代码
public class Main { public static void main(String[] args) { /*第一次访问singleton,singleton进行初始化,但不会初始化内部的Lazy*/ Singleton.test(); /*会初始化内部Lazy,并且创建对象*/ Singleton instance = Singleton.getInstance(); } } class Singleton{ static { System.out.println("singleton init"); } public static void test(){ System.out.println("single static method"); } /** * 私有化构造方法——单例模式*/ private Singleton(){ System.out.println("创建了对象"); } public static class Lazy{ /**内部类可以访问外部所有的内容*/ private static final Singleton SINGLETON = new Singleton(); static { System.out.println("Lazy init"); } } /** * 获取实例*/ static Singleton getInstance(){ return Lazy.SINGLETON; } }
-
输出
singleton init single static method 创建了对象 Lazy init
-
类加载器
概述
- 工作在加载过程的类的加载阶段,查找并加载二进制流数据,并生成Class对象
- 作用
- 所有.class都是通过ClassLoader进行加载
- 负责将Class信息二进制刘数据读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例
- 只影响类的加载,无法改变链接、初始化等
- Class对象和ClassLoader关系
- ClassLoader指向Class
- Class指向ClassLoader——谁加载的类
- 类加载的分类
- 显式加载
- 通过调用ClassLoader家在class对象
- Class.forName(name)——name全类名class文件
- this.getClass().getClassLoader().loadClass()
- 通过调用ClassLoader家在class对象
- 隐式加载
- 不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如果引用了另一个类的对象,也会自动加载另一个类
- 显式加载
- 了解加载器的作用
- 出现ClassNotFoundExpection、NoClassDefFoundError时根据日志快速定位
- 类的动态加载或者需要对编译后的字节码文件进行加解密
- 自定义类加载器来重新定义类的加载规则,以便实现一些自定义处理逻辑
- 命名空间
-
每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父类加载器所加载的类组成
-
同一个命名空间中,不会出现类的完整名称(包括类的包名)相同的两个类
-
在不同的命名空间中,有可能出现类的完整名称(包括类的包名)相同的两个类
-
总结
- 类的唯一性:加载它的类加载器和这个类的本身一同确定在Java虚拟机中的唯一性
- 此处说的类加载器不是指的类加载器类,而是类加载器实例。同一种类加载器不同的实例也叫不同的类加载器
- 不同的类即不同的class模版对象
- 借助这一特性,可以通行同一个类的不同版本
-
- 类加载机制的特征
- 双亲委派模型
- 可见性
- 子类加载器可以访问父类加载器
user.getClass().getClassLoader().getParent()
,反之不可。 - 可以利用这个特性实现容器的逻辑
- 子类加载器可以访问父类加载器
- 单一性
类加载器的分类
-
引导类加载器
Bootstrap ClassLoader
C/C++编写- 启动类加载器
Bootstrap ClassLoader
——获取的时候为null- 这个类加载器使用C/C++语言实现(将BootStrap类加载进来),嵌套在JVM内部
- 加载Java核心库(JAVA_HOME/jre/lib/rt.jar或者sun.boot.class.path等路径下的内容),用于提供JVM自身需要的类
- 没有父加载器(C/C++进行编写),不继承java.lang.ClassLoader
- 出于安全考虑,Bootstrap值加载包名为
java
,javax
,sun
等开头的类 - 加载扩展加载器,并指定为其父加载器
- 启动类加载器
-
自定义类加载器
UserDefine ClassLoader
java编写- 扩展类加载器
- 系统类加载器
- 用户自定义加载器
-
注意:
- 父类和子类加载器并不是严格意义上的继承关系(说的是谁加载谁),而已子类加载器引用了父类加载器(并且记录的是加载关系,不是代码上的继承关系)
- 子类加载器的类(加载器也是一个类,需要有加载器去加载)是由其父加载器进行加载的
- 打开类加载过程追踪功能
-XX:+TraceClassLoading
- Bootstrap ClassLoader时C++,其他类加载器使用的是java编写——需要加载器去加载他们
- 数组类型的类加载器和里面的元素有关(一致)
- 基本数据类型不需要类加载器加载——null
- Class.forName于ClassLoader.loadClass
- Class.forName():加载Class文件的时候会执行类的初始化
- ClassLoader.loadClass:只会将Class文件加载到内存,不会初始化
-
双亲委派模型
-
定义
- 如果一个类加载在接到加载类的请求的时候,他首先不会自己去尝试加载这个类,而是把这个请求任务委托给父加载器去完成,依次递归,如果父加载器可以加载这个类或者已经加载过,就成功返回。只有父类加载无法完成加载任务的时候,才会自己去加载,如果自己也无法加载则报错
-
本质
- 规定了类加载的顺序:引导类加载器-扩展类加载器-系统类加载器-自定义加载器
-
优势
- 避免类的重复加载,确保类的全局唯一性
- 保护程序安全,防止核心API被篡改
-
破坏双亲委派
- 1.JDK1.2之前
- 继承ClassLoader抽象类,重写loadClass()方法
- JDK1.2之后,在ClassLoader中添加了protected的findClass——推荐使用双亲委派(允许重写findClass)
- 2.线程上下文类加载器
- 父类加载器去请求子类加载器去加载类(违反了双亲委派(逆向))
- 3.为了追求程序的动态性-—热替换
- 双亲委派局限
- 如果一个类已经加载到系统中,通过修改类文件,并无法让系统在来加载并重新定义这个类
- 由于ClassLoader加载的同名类属于不同的类型,不能仙湖转换和兼容。——不同的ClassLoader加载的同一个类在虚拟机内部认为2个类完全不同
- 实现思路(不走双亲委派机制,可以创建新的ClassLoder实例来不断加载类,通过类创建对象——同一个类加载器的不同实例表示不同的加载器,并且所有类的加载都有当前类加载器加载,不由父加载器加载):
- 创建自定义的ClassLoader示例
- 加载需要热替换的类
- 创建新的额ClassLoader实例创建类对象——>被新的classLoader加载已经是新的类型
- 运行新对象的方法
- 循环以上步骤
- 双亲委派局限
- 1.JDK1.2之前
-
-
自定义ClassLoader
-
方式1:重写loadClass方法(双亲委派机制核心——可打破双亲委派模型)
- 主要定义类双亲委派的逻辑
- findClass()实现加载类的业务逻辑
-
方式2:重写findClass方法(建议)
-
主要定义了类加载的过程
- 读取class字节流
- 通过defineClass加载类
-
示例
public class MyClassLoader extends ClassLoader{ /*字节码路径*/ private String codePath; public MyClassLoader(String codePath) { this.codePath = codePath; } public MyClassLoader(ClassLoader parent, String codePath) { super(parent); this.codePath = codePath; } /** * 重写findClass加载类文件*/ @Override protected Class<?> findClass(String className) throws ClassNotFoundException { BufferedInputStream bis = null; ByteArrayOutputStream baos = null; try { //获取字节码文件完整路径 String fileName = codePath + className + ".class"; //获取输入流 bis = new BufferedInputStream(new FileInputStream(fileName)); //获取输出流 baos = new ByteArrayOutputStream(); //具体读入数据过程 int len; byte[] data = new byte[1024]; while ((len = bis.read(data)) != -1) { baos.write(data, 0, len); } //获取内存中完整的字节数组数据 byte[] bytecode = baos.toByteArray(); //调用defineClass将字节数据数据装换为Class实例 return defineClass(null, bytecode, 0, bytecode.length); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (bis!=null) { bis.close(); } } catch (IOException e) { throw new RuntimeException(e); } try { if (baos!=null) { baos.close(); } } catch (IOException e) { throw new RuntimeException(e); } } } }
-
-
作用
- 隔离加载类
- 避免类冲突
- 修改类加载的方式
- 类的加载模型并非强制,除了Bootstrp外,其他的加载并非一定要引入,或者根据实际情况在某个点进行按需进行动态加载
- 扩展加载源
- 比如从数据库、网络、甚至是机顶盒进行加载
- 防止源码泄漏
- 防止通过class文件通过反编译,对编译进行加密
- 隔离加载类
-
注意:
- 不同加载器实例加载的统一个类文件属于不同的类,赋值的时候会认为是不同类报错
-
-
JDK9新改动
- 1.扩展加载器被移除——平台类加载器
platformClassLoader
,并且可以通过ClassLoader直接获取 - 2.平台类加载器和应用程序类加载器不在继承
URLClassLoader
,继承BuiltinClassLoader
- 3.类加载器有了名称,使用getName()获取
- 4.启动类加载器是在JVM内部和java类库共同协作实现类加载器(以前是C++)
- 5.双亲委派
- 在委派给父类加载器先,先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派负责该模块的加载器进行加载
- 1.扩展加载器被移除——平台类加载器
运行期优化
即时编译
分层编译
-
JVM将执行状态分成了5个层次
- 0层:解释器执行(Interpreter)
- 1层:使用C1即时编译期编译执行(不带profiling)
- 2层:使用C1即时编译期编译执行(带基本的profiling)
- 3层:使用C1即时编译器编译执行(带完全的profiling)
- 4层:使用C2即时编译器编译执行
- 注:
- profiling是指在运行过程中收集一些程序执行状态的数据,如:「方法的调用次数」、「循环的回边次数」
- profiling是指在运行过程中收集一些程序执行状态的数据,如:「方法的调用次数」、「循环的回边次数」
-
即时编译期(JIT)和解释器的区别
- 解释器
- 将字节码解释为机器吗,下次即时遇到相同的字节码,仍然会执行重复解释
- 将字节码解释为针对所有平台都通用的机器码
- 大部分都是不常用的代码,无需耗费时间编译成机器码,仍然采用解释执行
- 即时编译器
- 将字节码编译为机器吗,并存入code cache,下次遇到相同的机器码,直接执行,无语再编译
- JIT会根据平台类型,生成平台特定的机器码
- 小部分热点代码,可以将其编译成机器码,以达到理想的运行速度
- 运行速度:Interpreter>C1>C2
- 解释器
-
优化手段
-
逃逸分析
- 分析对象动态作用域,并进行优化(C2即时编译器)
-XX:+DoEscapeAnalvsis
-
方法内联
-
如果发现热点方法,并且长度不是太长,就会进行内联——把方法内代码拷贝、粘贴到调用者位置(不用再调用方法)
-
调优参数
-XX:+UnlockDiagnosticVMOption
+-XX:+PrintInlining
——打印内联信息-XX:ComplieCommand=dontinline,包名.类.方法
——禁用内联
-
示例
private static int square(int i){ return i*i; } System.out.println(square(i)); //====================================== System.out.println(i*i);
-
-
字段优化
-
反射优化
- 通过放射调用某种方法最开始时使用的是==本地方法访问器
NativeMethodAccessorImpl
==进行调用 - 当反射调用方法次数超过阈值
ReflectionFactory.inflationThreshold()
(默认15),会替换成运行时方法访问器MethodAccessorImpl
,生成一个新的类GeneratedMethodAccessorxx
,通过类进行调用 - 通过MethodAccessorGenerator.generateMethod生成
- 通过放射调用某种方法最开始时使用的是==本地方法访问器
-
内存模型(JMM)
概述
- 前面讲的内存结构
- 此处讲的内存模型是Java Memory Model
- JMM定义了一台再多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障
总结
多线程相关
- t.join()——等待线程执行完成
- sychronized——同步(既可以保证代码块的原子性,也能保证代码块内变量的可见性)——重量级操作
- volatile
- ——直接操作主内存(不保证原子性——适合一个线程写,多个线程读)——轻量级操作
- ——禁止指令重排,修饰变量
- t.isAlive()判断t线程是否存活
- t2.interrupt()打断t2线程
- t2.isInterrupt()判断t2线程是否被打断
主内存和工作内存
- 主内存:所有线程共享的内存区域。存储Java对象示例、静态变量以及常量等数据。所有线程可访问
- 工作内存:每个线程都有自己的工作内存,用于存储该线程需要使用的数据。线程的工作内存中存储了主内存中的部分数据的拷贝。
- 线程只能直接操作工作内存中的数据,而不能直接操作主内存中的数据。
- 分离实现线程之间的独立性和并发操作的一致性。当一个线程需要操作某个变量时,它会先从主内存中将该变量的拷贝加载到自己的工作内存中进行操作,操作完成后再将结果写回主内存。这样可以确保不同线程之间的数据不会互相干扰,并且保证了线程之间的数据可见性和一致性。
- volatile修饰的变量 不会拷贝到工作内存中 直接操作的主内存(修改、查看) 但是它不是原子性的 所以要加上synchronized 和其他 同步机制
原子性
问题分析
-
单条语句的字节码指令有多条:字节码指令有多条,不具备原子性,如静态变量的自增
getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //执行加操作(操作数栈获取) putstatic //栈顶元素放入静态变量槽位
-
语句有多条
-
多线程执行的时候,不同线程的java语句或者字节码指令可能会竞争加入CPU执行
解决方法
synchronized
关键字
可见性
问题分析
public class Test {
private static Boolean run = true;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
long l = System.currentTimeMillis();
while (run) {
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
run = false; //线程并不会停下来
}
}
- 初始状态,线程开始从主内存中读取了run的值到工作内存
- 因为线程频繁从主内存中读取run的值,JIT编译期会将run的值缓存至自己的工作内存中的高速缓存中,减少对主内存run的访问,提高效率
- 主线程修改run的值,并同步至主内存,但线程是从自己的工作内存的高速缓存读取的值,所以永远是旧值
解决方法
volitile
——易变关键字- 用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量值,必须从主存中获取它的值,线程操作
volitile
变量都是直接操作主存 - 不保证原子性,适合一写多读(不会竞争修改)
- synchronized——(既可以保证代码块的原子性,也能保证代码块内变量的可见性)
- 用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量值,必须从主存中获取它的值,线程操作
有序性
问题分析
public class Test2 {
int num = 0;
boolean ready = false;
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
//对于这个函数,num和ready执行的先后顺序不影响函数返回的结果
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
- 当actor1先执行,r = 1
- 当actor2先执行,还没执行到ready,actor执行 r = 2
- 当actor2先执行,执行完ready,actor执行 r = 1
问题——指令重排
-
还有一种情况,actor2执行了ready = true,但num=2还未执行,r = 0;
-
这种现象叫做指令重排,是JIT编译器在运行时的一些优化,这个现象需要大量测试才能复现
-
java语句可能重排,字节码指令也可能重拍
-
出现原因
-
同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序——(所以可能会影响其他线程)
static int i; static int j; //某个线程内执行如下赋值 i=...;//较为耗时 j= ...;
先执行i和先执行j对结果不产生影响,所以在真正执行的时候既可以先执行j,也可以先执行i
-
案例——double-check locking实现单例模式
-
java代码
public class Singleton { private Singleton(){} private static Singleton INSTANCE = null; public static Singleton getInstance(){ //双检查锁定 //1.先检查INSTANCE是否为null(不在此处加同步,如果此处加同步,就算有实例后,每个线程还要获得锁才能判断) if (INSTANCE == null){ //2.不为null则锁定准备创建实例 synchronized (Singleton.class){ //3.第三次检查是否为null(在一个线程创建实例的时候,防止其他线程进入了第一层等待锁,创建实例完成后,获得锁继续创建) if (INSTANCE ==null){ INSTANCE = new Singleton(); } } } return INSTANCE; } }
-
上面的代码中没有考虑指令重拍的问题——INSTANCE=new Singleton(),其二进制指令为
new #2 dup invokespecial #3 putstatic #4
其中3,4步对结果时无关的,一个执行构造方法给空间赋值,一个时给变量引用地址。可能会出现如下顺序
t1 线程1执行到INSTANCE=new Singleton() t2 线程1分配空间,执行生成了引用地址入栈并复制 t3 线程1执行putstatic,将地址赋给INSTANCE,此时INSTANCE!=null t4 线程2执行getInstance方法,直接返回了INSTANCE t5 线程1执行构造方法invokespecial
t4中线程2拿到的是未初始化的单例,对其后续操作会有影响。
-
-
解决方案
- volatile修饰变量,禁用指令重拍
happens-before
概述
- happends-before规定了那些写操作对其他线程的读操作可见,是可见性和有序性的一套规则总结
规则
- 注:以下的变量都是指共享的静态变量、成员变量=
- 线程解锁前对变量的写,对接下持有相同锁的其他线程对该变量的读可见
- 线程对volatile变量的写,对其他读该变量的线程可见
- 线程start前发生变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其他线程得知该线程结束后的读可见(其他线程调用t1.isAlive()或t1.join())
- 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupt或t2.isInterrupt())——说的是线程t1写的值
- 对变量默认值(0,false,null)的写(修改),对其他线程对该变量的读可见
- 具有传递性:x hb y,y hb z,那么x hb z
CAS与原子类
概述
-
CAS(Compare and Swap),体现的是一种乐观锁的思想
-
java示例
volatile static int i = 0; //共享变量 while(true){ int last = i; //存储当前共享变量的值 int res = last+1; //执行业务逻辑 //判断last是否和当前共享变量值一致(不一致说明被改了,当前结果算错误结果,再循环) if(compareAndSwap(last,res)){ return res; } }
-
注意
-
1.获取共享变量时,为了保证变量的可见性,需要结合volatile修饰(防止获取不到最新值)
-
2.适用于竞争不激烈、多核CPU
- 竞争太激烈,共享变量被不断修改,会循环很多次(大部分都是重试的)
- 重试使用的CPU时间,如果是单核,别的CPU修改,也重试不了
-
3.因为没有使用sychronized,所以线程不会陷入阻塞,这也是效率提升的因素之一
-
4.Unsafe.getUnsafe().compareAndSwapObject()——Unsafe类是一个单例模式
-
乐观锁与悲观锁
- 乐观锁:不怕别的线程来修改共享变量,修改了也没关系,自己重试——CAS无锁并发
- 悲观锁:最悲观的估计,防止其他线程来修改共享变量,只有解锁后,别的线程才有机会——sychronized
原子操作类
-
juc(java.util.concurrent)提供了原子操作类,可以提供线程安全的操作,例如
-
都采用CAS+volatile来实现
-
AtomicInteger
//创建原子整数对象 private static AtomicInteger i = AtomicInteger(0); //静态变量自增自减可能无需 i.getAndIncrement(); //i++ i.incrementAndGet(); //++i
-
AtomicBoolen
-
-
synchronized优化
概述
- Java Hotspot虚拟机中,每个对象都有对象头(class指针、Mark Word)
- 每个线程的栈帧中,都有一个锁记录结构,内部可以存储锁定对象的Mark Word
- Mark Word主要存储
- 对象hash码
- 对象分代年龄
- 当加锁的时候(成为锁对象?),这些信息就根据情况被替换为
- 标记位
- 线程锁记录指针
- 重量级锁指针
- 线程ID
- …
优化方案
轻量级锁
- 如果一个对象虽然有多线程访问,但多线程访问的时间时错开的(没有竞争),那么可以使用轻量级锁来优化
锁膨胀
- 如果在尝试调价轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
重量级锁
- 获取不到moniter的时候,先不阻塞(阻塞会保存状态,唤醒会重新加载——会有上下文切换),先自旋重试(会占用CPU),重试多次自旋失败进入阻塞
偏向级锁
轻量级铋在没有竞争时(就白己这个线程),每次重入仍然需要执行 CAS操作。Java 6中引入了偏向锁来做进一步优化:只有第一次使用 CAS将线程 ID 设置到对象的 Matk Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新CAS.
其他优化
- 减少上锁时间
- 减少锁的粒度
- 将一个锁才分成多个锁提高并发读
- CourrentHashMap
- LinkedBlockingQueue——入队出队使用不同的锁(相对于LinkedBlockingArray只有一个锁相率更高)
- 锁粗化
- 多次循环进入同步快不如同步块内多次循环
- 锁消除
- JVM会进行代码逃逸分析,例如某个加锁对象时方法内的局部变量,不会被其他线程访问到,这时候就会被即时编译器忽略所有同步操作
- 读写分离(写时复制技术)
- CopyOnWriteArrayList
- CopyOnWriteSet