26 分钟
深入理解Java虚拟机(一)
目录
二、Java内存区域与内存溢出异常
2、Java运行时数据区域
(1)程序计数器
是一小块内存空间,可以看作当前线程所执行字节码的行号的行号指示器。且每个线程都需要一个程序计数器
(2)Java虚拟机栈
- 线程私有的,生命周期与线程相同。
- 栈由栈帧组成,每个方法执行时都会创建一个栈帧用于储存:
- 局部变量表:
- 存放编译器可知的8种基本数据类型和引用类型
- 其中64位长度的long和double类型会占用2个局部变量空间(槽slot)、其余的占1个
- 操作数栈、
- 动态链接、
- 方法出口等信息
- 局部变量表:
- 栈由栈帧组成,每个方法执行时都会创建一个栈帧用于储存:
- 栈可能抛出两种异常
- StackOverflowError:栈深度超过最大深度
- OutOfMemoryError:栈扩展无法申请更多内存
(3)本地方法栈
- 与Java虚拟机栈功能相似,是为的是Native方法服务。有些虚拟机将两种栈合二为一
- 同样抛出StackOverflowError和OutOfMemoryError异常
(4)Java堆
- Java内存中最大的一块,被所有的线程共享
- 唯一目的是存放对象实例
- 是垃圾收集器工作的主要区域,所以又叫GC堆
- 粗粒度的可划分为:
- 新生代
- 老年代
- 细致的可划分为:
- Eden空间
- From Survivor空间
- To Survivor空间等
- 从多线程角度看,可能会划分出多个线程私有的缓冲区
- 指令:
-Xmx
-Xms
(5)方法区
- 多线程共享
- 储存已被虚拟机加载的
- 类信息、
- 常量、
- 静态变量、
- 即时编译后的代码等
- 又叫非堆
- 永久代不能代表方法区,永久代是一种方法区的实现,但是可能出现内存溢出
- 无法分配内存产生OutOfMemoryError
(6)运行时常量池
- 是方法区的一部分
- 用于存放编译期间生成的各种字面量和符号引用,这些内容在类加载后放入该区域
- 同时允许在运行期间放入该区域,比较常见的例子就是
String.intern()
- 无法分配内存产生OutOfMemoryError
(7)直接内存
在Java 1.4加入NIO后,引入了一种基于Channel和Buffer的IO方式。其中的Buffer使用的内存就是放在该区域的。通过Native函数库直接分配堆外内存。避免传统IO在Java堆和Native堆中的来回复制
3、hotspot虚拟机对象探秘
(1)对象创建过程
- 检查到new
- 检查运行时常量池是否存在这个类的符号引用,并检查该类是否已经加载、解析和初始化过,若没有先进行类加载过程
- 为新生对象在堆分配内存,分配的方法:
- 若堆内存是规整的,则直接移动指针(指向已使用区域的最后一个字节或者下一个)即可
- 若堆内存不是规整的,则虚拟机将维护一个空闲内存表,这时需要查找出一块足够大的区分配
- 多线程分配内存保证不会出错:
- 使用同步或者CAS操作
- 按照线程将堆空间根据线程预先划分小的区域(本地线程分配缓冲TLAB),只有分配满了之后才需要同步锁定
- 分配完成后,将内存区域初始化为0
- 写入数据:
- 对象头(类元信息、类哈希码、对象GC分代年龄等)
- 执行
<init>
函数进行初始化
(2)对象的内存布局
- 对象头
- 运行时信息:(类哈希码、对象GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等)
- 该部分的数据长度为32位或64位
- 类型指针,指向类元数据的指针
- 如果是数组还保存数组的长度
- 运行时信息:(类哈希码、对象GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等)
- 实例数据
- 对齐填充(8字节对齐)
(3)对象的访问定位
两种方式: 使用句柄池: reference -> 句柄池中的句柄(包括指向对象实例数据的指针、指向对象类型指针)
优点:当对象数据发生移动(GC工作),仅需要改变句柄
直接指针访问(Hotspot采用): reference(就是指向对象实例数据的指针) -> 对象实例数据(当然包括指向对象类型指针)
优点:访问更快
4、OutOfMemoryError异常
(1)Java堆溢出
- 产生的太多的对象,但是垃圾收集器无法回收
(2)虚拟机栈和本地方法栈溢出
- 递归调用层次太深
- 线程创建太多
解决方法 * 见效其他区域大小
(3)方法区和运行时常量池溢出
- 大量使用反射或动态代理(Java实现或CGlib)运行时创建类
(4)本地直接内存溢出
- NIO 使用问题
三、垃圾收集器和内存分配策略
2、对象已死了吗?
(1)引用计数法
算法描述:有一个地方引用了一个对象,那么这个对象的引用数+1,当引用失效后,引用-1。当引用数位0的对象即为不可用。 问题:无法解决循环引用的问题
Java虚拟机没有采用
(2)可达性分析
算法描述:通过一系列称之为GC Roots的对象作为起点,向下搜索,搜索所经过的路径称为引用链,当一个对象到GCroots没有任何引用链相链(不可达),这说明这个对象不可用。
可以作为GC Roots的对象: * 虚拟机栈中引用的对象 * 方法区中的静态属性引用的对象 * 方法区中常量引用的对象 * 本地方法栈中JNI引用的对象
(3)再谈引用
引用的分类:
* 强引用:指在代码中普遍存在的类似于Object obj = new Object();
这类的引用,只要强引用存在,垃圾收集器就永远不会回收该引用指向的对象
* 软引用:用来描述一些还有用但并非必须的对象。在系统将要发生溢出时回收,通过SoftReference实现
* 弱引用:用来表述非必须的对象,轻度更弱,仅能生存到下一次垃圾回收前,通过WeakReference实现
* 虚引用:最弱的引用关系,为一个对象设置虚引用,为了在对象在被垃圾收集时收到一个通知,通过PhantomReference
软引用,弱引用,虚引用的作用: * JDK实现动态代理的时候使用WeakReference作为缓存 * 使用这些做内存缓存可以避免OOM
(4)生存还是死亡
- 判断某个对象不可达,判断对象是否覆盖finalize()方法、或者是否被虚拟机执行过
- finalize没有覆盖或已经被执行过——finalize不需执行:
- 反之——finalize需要执行:将对象放入F-queue,使用低优先级线程调用其finalize方法,但是不承诺等待他运行结束(防止finalize方法死循环阻塞F-queue)
- 如果finalize方法成功自救,在GC进行第二次小规模标记将他移出即将回收集合。
- 否则将会被回收
注意 * finalize仅能执行1次,且最好不用使用他,因为他没有保证 * 自救的方式,重新将this赋给类变量或者其他可达成员的引用上
(5)回收方法区
回收内容 * 废弃常量和无用的类
废弃常量回收方法:同样使用引用可达分析
判断类是否无用,必须满足以下三个条件:
* 该类的所有实例都被回收
* 该类的ClassLoader被回收
* java.lang.Class
没有被引用
3、垃圾收集算法
(1)标记-清除算法
不足: * 标记和清除效率不高 * 产生内存碎片
(2)复制算法
描述 将内存划分为大小相等的两部分,将一块用完之后,将还存活的对象赋值到另一块
优点 没有内存碎片 内存分配简单 运行高效
代价 可用内存缩小一半
用途 用于回收新生代
实际使用 * 将内存划分为一块较大的Eden空间和两块较小的Survior空间 * 每次使用一个Eden和Survior * 当回收时,将还活着的Eden和Survior复制到另一块Survior * 默认Eden和Survior比为8:1 * 当Survior不够使使用其他内存(老年代)进行担保 * 能使用这个方法的原因:大部分对象都是朝生夕死
(3)标记整理算法
主要用在老年代,类似于标记清除,添加一个整理过程
(4)分代收集算法
虚拟机在使用的算法,就是对以上算法进行综合,根据对象的生命时长选择不同的收集算法
将虚拟机分为新生代和老年代,新生代使用复制算法、老年代使用标记清理或者标记整理
4、HotSpot算法实现
阐述HotSpot如何查找不可达对象(收集算法)
(1)枚举根节点
可以作为GC Roots的:全局性引用和执行上下文。若要逐个检查引用会很耗时。另外还会造成系统完全停止。
目前主流的虚拟机都使用准确式GC,虚拟机应当可以直接知道那些地方存放。使用称为OopMap的数据结构可以实现。
(2)安全点
OopMap可能是某一连续的空间,记录了执行到某条指令所有的GC Roots的引用。这条指令的位置称为安全点。
虚拟机不会生成过多的安全点,安全点的选定以“是否具有让程序长时间执行的特征”
另一个问题是要让所有的线程都跑在最近安全点上载定下来。有两种方案 * 抢占式中断:在GC发生时,首先让所有线程是否停在安全点上,不是则让其执行跑到安全点停止 * 主动式中断:当GC需要线程中断时,不直接进行操作,而是简单设置一个标志,各个线程主动轮训这个中断发现中断为真时自己就挂起
(3)安全区域
安全区域是指在一个代码段内,引用关系不在发生变化,那么在这个区域内任意地方进行GC都是安全的,是扩展的安全点
在线程执行到安全区后,首先标示自己进入了安全区,在安全区的这段时间,JVM发生GC,就不会管这个线程了。当线程离开安全区,检查是在进行GC,如果没有则正常执行,否则等待。
5、垃圾收集器
Java对垃圾收集器的实现没有任何规定。所以Hotspot提供了7中针对不同分代对象的垃圾收集器
(1)Serial收集器
最基本历史最悠久的收集器,一种新生代收集器。当他工作时整个虚拟机仅允许存在他者一个线程。他的运行会造成“世界停顿”。但是他简单高效,是Clinet模式下默认的垃圾收集器。(使用复制算法)
(2)ParNew收集器
是Serial的多线程版本。同样会造成停顿。是Server模式下首选的新生代收集器(在多CPU下更好)(使用复制算法)
(3)Parallel Scavenge收集器
该收集器是新生代收集器,使用赋值算法,他的目的是达到一个可控制的吞吐量(用户CPU时间占比)。高吞吐量的适用于后台任务。
提供了两个控制参数和一个开关参数:
* 控制最大停顿时间(-XX:MaxGCPauseMilis)
* 控制吞吐量的大小(-XX:GCTimeRatio GC时间占比)表示
* 垃圾收集占比为1/(1+GCTimeRatio)
* 自适应新生代分配(-XX:+UseAdaptiveSizePolicy)
降低停顿时间的代价是: * 更小的新生代空间 * 更频繁的垃圾回收
无法和CMS配合工作
(4)Serial Old收集器
是Serial的老年代版本,同样是单线程版,使用标记-整理算法,主要在Client下使用
在Server模式有两大用途: * 在Java5及之前与Parallel Scavenge搭配使用 * 作为CMS的后备方案
(5)ParNew Old收集器
是Parallel Scavenge的老年代版本,使用多线程和标记-整理算法,从Java1.6中使用。
在注重吞吐量的情况,使用Parallel Scavenge和ParNew Old组合
(6)CMS收集器
Concurrent Mark Sweep 一种获取最短回收停顿时间的收集器。很大一部分用于BS系统,这类系统重视系统响应速度
基于标记清除实现,分为4个过程: * 初始标记(标记GCRoots直接关联的对象) * 并发标记(用户线程可执行) * 重新标记(标记上一步发生变化对象) * 并发清除(用户线程可执行)
特点:针对老年代、并发收集、低停顿
有三个明显缺点 * 对CPU资源敏感 * 无法处理浮动垃圾(在并发清除阶段产生的垃圾),如果浮动垃圾占满了堆,将会发生收集失败,从而启用后备方案,Serial Old * 产生大量的碎片,如果没有空间放下大对象,将触发Full GC。可以通过配置成在碎片严重时,进行合并整理(只能单线程执行,造成停顿过长)
(7)G1收集器
面向服务端应用,特点如下 * 并行与并发:能够充分利用多CPU多核,使用多CPU缩短停顿时间 * 分代收集,自己集成了各个代的收集,不需要其他收集器配合 * 空间整合:从整体上看,基于标记整理算法,从局部上看基于复制算法是实现 * 可预测的停顿:能让使用者明确指明在长度为M毫秒的时间片内,消耗在垃圾收集的时间不得超过N毫秒
使用G1后的堆内为不在简单的划分新生代和老年代。而是将整个堆划分为多个大小相等的独立区域(Region),新生代和老年代都是一部分Region的集合,而且不需要连续
G1避免整个堆的垃圾收集,而是跟踪每个Region回收收益比,使用优先队列维护,在回收过程中优先回收价值最大的Region
(8)理解GC日志
(7)垃圾收集器参数总结
参见书籍
6、垃圾分配与回收策略
- Minor GC新生代GC
- Full GC 老年代GC
(1)对象优先分配在Eden分配
(2)大对象直接进入老年代
(3)长期存活对象进入老年代
对象为每个对象添加一个Age(年龄计数器),每经过一次Minor GC将计数器+1,当年龄增加到一定程度(默认15),将进入老年代
(4)动态年龄判断
虚拟机并不是一定要求年龄达到上线才进入老年代,如果在Survivor相同年龄的所有对象大小总和大于Survivor的一半,则年龄大于等于此年龄的对象直接进入老年区
(5)空间分配担保
若发生MinorGC,首先检查老年代最大可用连续空间是否大于新生代所有对象总空间。若否将虚拟机检查是否配置HandlePromotionFailure是否允许担保生效。若允许,则检查老年代的最大连续空间是否大于历次晋升到老年代的对象的平均大小。如果大于,则进行Minor GC。这样做虽然存在风险,但是可以避免FullGC过于频繁
(6)什么情况下会触发Minor GC
- Eden区满时,触发Minor GC。
(7)什么情况下会触发Full GC
- 老年代空间不足
- 老年代碎片比较严重没有足够空间分配给大对象
- 永久代空间不足
- 显示调用
System.gc()
- 发生新生代提升到老年代失败的情况,会触发Full GC
- 统计得到的每次Minor GC晋升到旧生代的平均大小,大于老年代的剩余空间时
四、虚拟机性能监控与故障处理工具
2、JDK的命令行工具
(1)jps
显示系统内所有的运行在虚拟机上的进程,该工具显示的pid(称为LVMID)为其他工具所使用
参数列表
* -q
只输出LVMID
* -m
输出虚拟机启动时传递给主类main的参数
* -l
输出主类的全名,如果是jar,输出jar路径
* -v
输出虚拟机启动时JVM参数
(2)jstat:虚拟机统计信息监视工具
他可以显示本地或者远程调用虚拟机进程中的类装载内存垃圾收集JIT编译等运行信息
格式jstat [opt vmid [间隔时间[s|ms] 次数]
例子jstat -gc 2756 250 30
对进程2756每隔250ms查询一次垃圾收集情况,共检查20次
参数列表
* -class
查询类装载、卸载数量、总空间以及类加载所需时间
* -gc
监视Java堆的状况
* -gccapacity
类似于-gc但是更关注Java各个堆各个区域使用到最大、最小空间
* ...
请查书
(3)jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数
(4)jmap:Java内存镜像工具
生成内存快照dump
(5)jhat:虚拟机堆转快照分析工具
(6)jstack:Java堆栈跟踪工具
用于生成当前时刻的线程快照
(7)HSDIS:JIT生成代码反汇编
3、JDK可视化工具
(1)JConsole:Java监视与管理台
启动JConsole
JAVA_HOME/bin/jconsole
功能 * 内存监控:相当于jstat * 线程监控:相当于jstack
(2)VisualVM:多合一故障处理工具
略
五、调优案例分析与实战
1、高性能硬件上的程序部署策略
(1)例子
在多CPU大内存的服务器上,给堆分配了很大的内存。会不定期的出现长时间无法响应的问题。
(2)原因
由于对堆进行Full GC造成了停顿。
(3)大内存服务器的配置方案
- 通过64位JDK分配超大的堆来使用大内存
- 使用若干个虚拟机建立集群来利用资源
(4)使用64位JDK使用大内存
对于用户交互性强、对时间停顿敏感的系统,要降低Full GC的频率。比如:几十个小时执行一次Full GC,这样可以通过定时任务触发GC或者直接重启服务器
控制GC的关键是看大多数对象是否符合朝生夕灭,通过合理编程,对于网站来说是可能的。
使用此方案需要面临的问题: * 内存回收导致长时间停顿 * 需要程序足够稳定,不能发生堆溢出。因为无法产生如此大的内存转储文件
(5)使用若干个虚拟机建立集群来利用资源
实现方式: * 在机器上创建多个服务器进程分别占用不同的端口 * 在前端搭建一个负载均衡服务器,通过反向代理方式来分配访问请求 * 前端代理服务器要通过负载分配一个机器,然后记录sessionid,在session生命周期内都会在同一节点进行
使用此方案需要面临的问题: * 避免节点竞争全局资源比如说磁盘文件 * 很难有效利用资源池 * 若是32位仍存在内存限制 * 使用本地缓存后会出现大量的空间浪费,可以考虑使用集中式缓存
亲和式集群:节点间不存在session同步。在整个session生命周期内,给用户提供服务的是同一个节点
2、集群中同步导致的内存溢出
在使用JBossCache,可能导致内存溢出,原因是: JBossCache使用自定义协议栈来实现通信,必须保存协议栈的消息以便于重传(类似TCP协议栈),当网络状态差的情况下可能产生堆栈溢出
3、堆外内存导致的溢出
- 使用nio时会使用direct memory,很可能造成内存溢出,可通过-XX:MaxDirectMemorySize调整
- 线程堆栈,可通过-Xss调整
- Socket缓冲区:读写分别约为37KB和25KB
- JNI代码
- 虚拟机和GC
4、外部命令
通过Runtime.getRuntime().exec()
来执行外部命令会
* 克隆虚拟机进程
* 使用新的进程执行命令
* 退出这个进程
这非常耗费CPU和内存资源
5、JVM崩溃
两个服务发生调用关系,如果两端的服务速度不对等若使用socket连接等待的情况可能出现过多的等待socket导致崩溃
可以通过生产者-消费者消息队列,解决(也有可能产生饥饿和背压)
6、不恰当数据结构导致内存占用过大
使用容器和包装器对象导致空间利用率极低比如说Long的利用率为8B/44B = 18%
,从而导致GC效率的降低
7、由Windows虚拟内存导致的长时间停顿
Java GUI 程序 在最小化后,其工作内存被自动交换到磁盘页面文件中了。可以通过 -Dsun.awt.keepWorkingSetOnMinimize=true
8、eclipse运行速度调优
注意:此为较老的版本jdk1.6的调优
* 使用较新版本的JDK
* 禁用字节码校验:-Xverify:none
* 使用-server
模式(可能降低启动速度)
* 将-Xms
和-XX:PermSize
设置的与-Xmx
和-XX:MaxPermSize
一样大
* 禁用显示垃圾收集-XX:+DisableExplicitGC
* 使用CMS+ParNew
9、虚拟机启动参数
(1)启动参数分类
- 标准参数(
-
),所有的JVM实现都必须实现这些参数的功能,而且向后兼容; - 非标准参数(
-X
),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容; - 非Stable参数(
-XX
),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
(1)标准参数
查看方式:java
常用设置:
* -client
启动快,运行慢
* -server
启动慢,运行块
* -classpath
或-cp
:目录和 zip/jar 文件的类搜索路径,使用:分割
* -verbose:class
输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。
* -verbose:gc
输出每次GC的相关情况。
* -verbose:jni
输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
(2)非标准参数
查看方式:java -X
常用设置:(Java9)
* -Xms<size>
设置初始 Java 堆大小 一般设置与-Xmx
一致
* -Xmx<size>
设置最大 Java 堆大小
* -Xmn<size>
为年轻代 (新生代) 设置初始和最大堆大小。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
* -Xss<size>
设置 Java 线程堆栈大小
* -Xint
禁用JIT
* -Xloggc:<file>
将 GC 状态记录在文件中 (带时间戳)
* -Xverify
设置字节码验证器的模式
(3)非Stable参数
查看方式:未知
常用设置
* -XX:-DisableExplicitGC
禁止调用System.gc();但jvm的gc仍然有效
* -XX:+MaxFDLimit
最大化文件描述符的数量限制
* -XX:+ScavengeBeforeFullGC
新生代GC优先于Full GC执行
* -XX:+UseGCOverheadLimit
在抛出OOM之前限制jvm耗费在GC上的时间比例
* -XX:-UseConcMarkSweepGC
对老生代采用并发标记交换算法进行GC(CMS)
* -XX:-UseParallelGC
启用并行GC
* -XX:-UseParallelOldGC
对Full GC启用并行,当-XX:-UseParallelGC启用时该项自动启用
* -XX:-UseSerialGC
启用串行GC
* -XX:+UseThreadPriorities
启用本地线程优先级
* -XX:PermSize
方法区大小(jdk1.6之前)间接控制常量池
* -XX:MaxPermSize
方法区最大大小(jdk1.6之前)
* -XX:MaxDirectMemorySize
本机直接内存,nio使用,默认等于-Xms
在Java8及以后永久代的完全移除、新增了元空间metaspace
* 字符串常量由永久代转移到堆
* 静态变量移动到堆
* 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
* -XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
* -XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。
* 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
* -XX:MinMetaspaceFreeRatio
,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
* -XX:MaxMetaspaceFreeRatio
,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
改变的原因 原因: * 字符串存在永久代中,容易出现性能问题和内存溢出。 * 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 * 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。 * Oracle 可能会将HotSpot 与 JRockit 合二为一。
六、类文件结构
2、无关性基石
JVM无关性表现在两个方面: * 平台无关性 * 语言无关性
也就是说JVM是连接编程语言和操作系统的平台,任何语言都可以编译成JVM文件并在任何机器上使用
3、Class文件结构
(1)基本结构
- Class文件是一个二进制字节流文件
- 使用大端方式储存(也就是说,人类可以按照从左到有的合理顺序观察文件)
- 定义了无符号数和表类型:
- 无符号数有:u1, u2, u4, u8类型
- 表类型:类似于结构体,是无符号类型的复合体,在命名上以
_info
结尾
整个class文件的内容如下:
类型 | 名称 | 数量 |
---|---|---|
U4 | magic | 1 |
U2 | minor_version | 1 |
U2 | major_version | 1 |
U2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
U2 | access_flags | 1 |
U2 | this_class | 1 |
U2 | super_class | 1 |
U2 | interfaces_count | 1 |
U2 | interfaces | interfaces_count |
U2 | fields_count | 1 |
field_info | fields | fields_count |
U2 | methods_count | 1 |
method_info | methods | methods_count |
U2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
(2)magic
在文件开头,magic用于识别文件是否是class类型文件4字节长,值为0xcafebabe
(3)minor_version子版本号
(4)major_version主版本号
(5)constant_pool_count常量池大小
表示接下来有constant_pool_count-1
个常量,下标从1
到constant_pool_count-1
(6)constant_pool常量池内容
常量池是不固定长度的,在虚拟机加载进来后,解析完成后,在后续的位置使用常量时,是通过下标(符号引用)来访问的
常量池内部存放的内容都是表类型
允许的类型如下:
类型 | 标志 | 含义 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整形字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethod_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
例子:CONSTANT_Class_info结构
类型 | 名称 | 数目 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
含义: * tag表示常量类型表中的标志 * name_index表示类名的引用,类名字符串在常量表中的位置下标
例子:CONSTANT_Utf8_info结构
类型 | 名称 | 数目 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
- tag表示类型标志
- length表示字符串的的字节数(而不是长度)
- 字符串的utf8编码
查看常量池
javap -verbose TestClass
常见类型描述:见《深入理解Java虚拟机》
(7)access_flags
类访问控制符2字节16位,具体详见《深入理解Java虚拟机》
(8)类索引、父类索引、接口索引
- this_class 当前类名索引
- super_class 当前父类的索引,只有Object类为0
- interfaces_count 实现的数量接口
- interfaces 每个接口的索引共 interfaces_count 个
(9)字段/方法表集合
- fields_count 表示接下来有多少个字段
- fields 有fields_count个类型为field_info,表述着每个字段的信息
- methods_count 表示接下来有多少个方法
- methods 有methods_count个类型为method_info,表述着每个方法的信息
field_info/method_info部分结构
* u2 access_flags 访问标志主要记该字段的访问权限、是否static、是否final、是否volatile等
* u2 name_index 字段名索引,如int a;
则字段名为a
;void foo()
则字段名为foo
* u2 descriptor_index 字段方法描述符
* int a
的描述符为:I
* String [][] arr
的描述符为[[Ljava/lang/String
* int indexOf(char []source, int sourceCount)
的描述符为([CI)I
* u2 attribute_count 属性数量
* attribute_info attributes 属性内容
其他参加《深入理解Java虚拟机》
(10)属性表集合
Class文件最后的内容。包含一下内容
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明过时的字段和方法 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 方法局部或匿名类属性,表示这个类所在外围的方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源代码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | Java 1.6 新增 |
Signature | 类、方法、字段表 | Java 1.5 新增,用于记录泛型相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | Java 1.6 新增 用于保存非Java语言编写的编译成Class文件的相关调试信息。例如JSP调试 |
Synthetic | 类、方法表、字段表 | 表示方法或者字段为编译器自动生成 |
LocalVariableTypeTable | 类 | Java1.5新增,使用特征签名代替描述符,为了泛型签名 |
RuntimeVisibleAnnotation | 类、方法表、字段表 | Java1.5 新增 指明那些哪些注解是运行时注解 |
RuntimeInvisibleAnnotation | 类、方法表、字段表 | Java1.5 新增 指明那些哪些注解是运行时不可见注解 |
RuntimeVisibleParameterAnnotation | 方法表 | 类似以上 |
RuntimeInvisibleParameterAnnotation | 方法表 | 类似以上 |
AnnotationDefault | 方法表 | 记录注解类元素的默认值 |
BootstrapMethods | 类文件 | Java 1.7 用于保存invokedynamic指令引用的引导方法限定符 |
其他参见《深入理解Java虚拟机》
4、字节码指令简介
(1)特点
- 操作码长度为1字节(叫做字节码的原因)
- 放弃了操作数对齐,操作数是按字节对齐的
- 面向操作数栈的体系而非面向寄存器的体系
执行过程伪代码
do {
自动计算PC寄存器值加1;
根据PC值取出操作码;
if(操作码存在操作数) 从字节流中取出操作数;
执行操作码定义的操作;
} while(字节码流长度 > 0)
(2)字节码与数据类型
- 由于操作码长度有限,所以不可能对每种数据类型提供操作码
- 一般提供int类型的整数操作码(小于int的数据类型采用类型提升来计算),long、float和Double
(3)加载存储指令
- 将一个局部变量加载到操作数栈:
iload
、iload_<n>
等 - 将一个数值从操作数栈放入局部变量表:
istore
、istore_<n>
等 - 将一个常量加载到操作数栈:
bipush
、sipush
等 - 扩充局部变量表的访问索引的指令:wide
(4)运算指令
- 加
- 减
- …
(5)类型转换指令
- 小范围转向大范围不需要显示转换
- 窄化转换指令有:
i2b
、i2c
等
(6)对象创建与访问指令
- 实例创建:new
- 数组创建:newarray、anewarray、multianewarray
- 访问类变量和实例变量:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将操作数栈的值存到数组中:xastore(x可选八种基本数据类型简写字母)
- 取数组长度指令:arraylength
- 查找实例类型:instanceof、checkcast
(7)操作数管理指令
- 出栈:pop、pop2
- 栈顶元素复制双份入栈dup、dup2、dup_x1等
- 栈顶元素交换
(8)控制转移指令
- 分支
- 复合条件分支
- 无条件分支
(9)方法调用和返回
- invokevirtual 根据实际类型调用实例方法
- invokeinterface 调用接口方法
- invokespecial 调用特殊方法如构造方法、私有方法、父类方法
- invokestatic 调用static方法
- invokedynamic
(10)异常处理
(11)同步指令
5、公有设计和私有实现
Java Class文件规范作为通用的设计,但是各个虚拟机的实现可以根据情况选择何时数据结构,或者内部的Class文件表示方式
七、虚拟机类加载机制
2、类加载的时机
- 遇到new、getstatic、putstatic或invokestatic字节码时
- 使用“反射”时
3、类加载的过程
加载-验证-准备-解析-初始化
(1)加载
- 获取到类的二进制流
- 将二进制流转换为JVM中方法区中的数据结构
- 生成一个java.lang.Class对象
(2)验证
尽量检测字节码是否正确,主要验证如下内容 * 文件格式验证(是否符合Class文件格式标准) * 元数据验证 * 是否存在父类 * 是否继承了final类 * … * 字节码验证 * 符号引用验证
(3)准备
- 所有类字段分配的空间内存清零
(4)解析
- 符号引用
直接引用
类或接口的解析
字段解析
类方法解析
接口方法解析
(5)初始化
- 执行static语句块和static字段赋值
4、类加载器
(1)功能
根据类的权限定名获取到Class文件的二进制流
(2)类与类加载器
使用不同的类加载器加载同一个类,他们是不等的
(3)双亲委托模型
Java内置3个类加载器
* Bootstrap ClassLoader 本地语言实现 启动类加载器 - 用于加载运行时环境如rt.jar (<JAVA_HOME>/lib
)
* Externsion ClassLoader 扩展类加载器 - 用于加载(<JAVA_HOME>/lib/ext
)
* Application ClassLoader 应用程序类加载器 - 加载用户ClassPath中的类
下层的的类加载器先调用其父加载器,如果已加载则自己不加载,否则重新加载
(4)破坏双亲委托模型
- 双亲委托不是强制要求,不建议覆盖loadClass,建议覆盖findClass方法
- 上层基础类要调用下层的用户类,通过线程上下文类加载器
- OSGI通过类加载器实现模块化