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变量不是——不是存在栈中?
-
-
栈内存溢出*
- 栈帧过多导致栈溢出——递归调用没有正确结束
- 栈帧多大,大于栈内存——不容易出现
线程运行诊断
- 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
类文件结构
-
查看二进制
.class
文件:od -t xC Demo1.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)
-
-