一、JVM内存结构
1. 程序计数器(PC寄存器)
记录指令执行位置。当前线程正在执行的那条字节码指令的地址。如果当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
线程私有
唯一不会出现OutOfMemoryError的内存区域
2. Java虚拟机栈
JVM会为每个即将运行的Java方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息。包括局部变量表、操作数栈、动态连接、方法出口等信息。
局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
-
JVM栈会出现两种异常:StackOverFlowError和OutOfMemoryError
- StackOverFlowError:若JVM栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前JVM栈的最大深度时,会抛出该异常。出现该异常时,内存空间可能还有很多
- OutOfMemoryError:若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,会抛出该异常。
- HotSpot虚拟机的栈容量是不可以动态扩展的
3. 本地方法栈
- 本地方法栈是为JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以通常又叫C栈。
- 它与Java虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。
HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,故-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有效果的,栈容量只能由-Xss参数来设定。
4. 堆
- 堆是用来存放对象的内存空间
- 堆是线程共享的,JVM中只有一个堆,在虚拟机启动时创建
- 是垃圾回收的主要场所,故也被称为“GC堆”
- 进一步可以分为:新生代(Eden区:From Survivor,To Survivor)、老年代
- 堆的大小可以固定也可以扩展。无法再扩展时抛出OutOfMemoryError异常
- -Xmx(堆最大值)、-Xms。两个参数设置为一样即可避免堆自动扩展。
5. 方法区
- 方法区时堆的一个逻辑部分。存放以下信息
- 已经被虚拟机加载的类型信息
- 常量
- 静态变量
- 即时编译器编译后的代码缓存
- 永久代。方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为“永久代”。(针对HotSpot JDK8之前版本而言) JDK8之后称为元空间。
- 可以固定大小也可以动态扩展,还允许不实现垃圾回收
- OutOfMemoryError
- -XX:MaxMetaspaceSize 设置元空间最大值,默认-1,即不限制,或者说只受限于本地内存大小。
- -XX:MetaspaceSize 指定元空间初始空间大小,以字节为单位。达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少空间,那么在不超过过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
- -XX:MinMetaspaceFreeRatio 在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
6. 运行时常量池
- 方法区中的常量就存放再运行时常量池中。
- JDK 7起,字符串常量池被移至Java堆中
- 当类被JVM加载后,.class文件中常量池表存放的常量就存放在方法区的运行时常量池中。
- 而且在运行期间,可以向常量池中添加新的常量。如String类的
intern()
方法(保存首次遇见的实例引用)可以向常量池中添加字符串常量。具有动态性。 - OutOfMemoryError
7. 直接内存(堆外内存)
- 除JVM之外的内存
- JDK1.4中新引入的NIO类,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
- OutOfMemoryError
- -XX:MaxDirectMemorySize 指定直接内存的容量大小。如果不指定, 则默认与Java堆最大一致
二、HotSpot虚拟机对象探秘
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:
1. 对象头
- 记录对象运行过程中需要使用的一些数据
- 运行时数据(Mark Word)
- 哈希码
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针
- 通过该指针能确定对象属于哪个类。
- 如果对象是一个数组,那么对象头还会包括数组长度。
- 并不是所有的虚拟机都有类型指针
- 运行时数据(Mark Word)
- 8字节的倍数(1倍或者2倍)
2. 实例数据
- 成员变量的值,包括父类成员变量和本类成员变量。
3. 对齐填充
- 用于确保对象的总长度为8字节的整数倍。
- HotSpot VM 的自动内存管理系统要求对象的大小必须是8字节的整数倍。
- 对象头部分正好是8字节的整数倍(1倍或者2倍),因此当对象实例部分没有对齐时,就需要通过对齐填充来补全。
- 对齐填充仅仅起着占位符的作用。并不是必然存在的。
4. 为新生对象分配内存
-
指针碰撞
如果Java堆中内存绝对规整(采用“复制算法”或者“标记整理法”),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为“指针碰撞”。
-
空闲列表
如果Java堆中内存并不规整,已使用的内存和空间内存交错(采用标记-清除法,有碎片),此时VM必须维护一个列表,记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例,这种分配方式称为“空闲列表”。
5. 对象访问方式
-
句柄访问方式
堆中创建一个叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。引用类型的变量存放的是该对象的句柄地址。
引用中存储的是稳定句柄地址,对象的移动只修改句柄中的实例数据指针
-
直接指针访问方式
HotSpot采用该方式。即直接指针方式访问对象。不需要多一次间接访问的开销。
6. 对象的创建(new过程)
- JVM遇到new指令
- 检查指令的参数是否能在常量池中定位到一个类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析和初始化过
- 没有的话执行相应的类加载过程
- 分配内存,初始化零值
- 执行Class文件中的<init>()方法初始化,即构造函数
三、垃圾收集策略与算法
1. 存活判断
-
引用计数算法
在对象头维护着一个counter计数器,对象被引用一次则计数器+1;若引用失效则计数器-1。当计数器为0时,就认为该对象无效了。
无法解决循环引用问题。
-
可达性分析算法(Java)
通过一系列称为"GC Roots"的根对象作为起始节点集, 从这些节点开始, 根据引用关系向下搜索, 搜索过程所走过的路径称为"引用链", 所有和GC Roots 直接或间接关联的对象都是有效对象,和GC Roots 没有关联的对象就是无效对象。
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
-
在Java中, 固定可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
- Java虚拟机内部的引用, 如基本类型对应的Class对象, 一些常驻的异常对象等, 还有系统类加载器
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
2. 引用种类
根据可达性状态`reachable`
-
强引用
永远不被回收
-
软引用
发生内存溢出异常前, 会把这些对象列进回收范围之中进行第二次回收
-
弱引用
下一次GC时被回收
-
虚引用(Phantom Reference)
为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被GC时收到一个系统通知
3. 回收无效对象
-
堆对象回收
- 判定finalize()是否有必要执行。如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么视为“没有必要执行”。那么对象基本上就真的被回收了。
- 如果对象被判定为有必要执行finalize()方法,那么对象会被放入一个F-Queue队列中,虚拟机会以较低的优先级执行这些finalize()方法,但不会确保所有的finalize()方法都会执行结束。如果finalize()方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
- 如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果没有,将会被GC清除。
- 任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
-
方法区内存回收
方法区中存放生命周期较长的类信息、常量、静态变量,每次GC只有少量的垃圾被清除。方法区中主要清除两种垃圾:废弃常量和无用的类。
当常量池中的常量不被任何变量或对象引用,那么这些常量将会被清除。
-
无用的类:
- 该类的所有对象都已经被回收。
- 加载该类的ClassLoader已经被回收。
- 该类的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。
4. 垃圾收集算法
-
标记-清除算法
- 标记:将GC Roots可达的对象标记为存活的对象
- 清除:清除没有标记的对象,清除对象标记
- 存在碎片
- 复制算法(新生代)
- 为解决空间利用率问题,可以将内存分为三块:Eden、From Survivor、To Survivor,比例是8:1:1,每次使用Eden和其中一块Survivor。
- 回收时,将Eden和Survivor中还存活的对象一次性复制导另外一块Survivor空间上,最后清除掉Eden和刚才使用的Survivor空间。
- 为对象分配内存空间时,如果Eden+Survivor中空闲区域无法装下该对象,会触发MinorGC 进行垃圾收集。但如果Minor GC 后依然有超过10%的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入Eden区。
-
标记-整理算法(老年代)
- 标记:将GC Roots可达的对象标记为存活的对象
- 整理:移动所有存活对象,按内存地址次序依次排序,然后将末端内存地址以后的内存全部回收。
-
分代收集算法
- 新生代:复制算法
- 老年代:标记-整理算法
-
分代收集理念
-
部分收集( Partial GC )
- Minor GC: 新生代收集
- Major GC: 老年代收集
- Mixed GC: 整个新生代和部分老年代。目前只有G1有这种行为
-
整堆收集( Full GC )
整个Java堆和方法区
-
记忆集(Remembered Set)
记忆集是一种用户记录从非收集区域指向收集区域的指针集合的抽象数据结构。
在新生代中创建,用于解决对象跨代引用所带来的问题,避免把整个老年代加进GC Roots扫描范围。
-
四、HotSpot垃圾收集器
1. 新生代垃圾收集器
-
Serial垃圾收集器(单线程)
HotSpot 客户端模式下的默认新生代收集器
使用复制算法,只开启一条GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)。
-
ParNew垃圾收集器(多线程版的Serial)
- ParNew是Serial的多线程版本。由多条GC 线程并行地进行垃圾清理。
- 清理过程需要Stop The World。
- 可与CMS搭配使用(JDK9后只能与CMS搭配使用),常用于服务端
-
Parallel Scavenge垃圾收集器(多线程)
基于复制算法
Parallel Scavenge 追求CPU吞吐量,通过减少GC运行时间达到。但是这样会导致更高的暂停时间。
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
- -XX:GCTimeRadio 设置垃圾回收时间占总CPU时间的百分比。
- -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。
- -XX:+UseAdaptiveSizePolicy 开启自适应策略。根据设置好的堆大小和MaxGCPauseMillis或GCTimeRadio,自动调整新生代的大小、Eden和Survivor的比例、对象进入老年代的年龄,以最大程度上接近设置的MaxGCPauseMillis或GCTimeRadio。
2. 老年代垃圾收集器
-
Serial Old 垃圾收集器
相当于Serial的老年代版本,使用标记-整理算法。
-
Parallel Old收集器
相当于Parallel Scavenge的老年代版本,使用标记-整理算法
CMS垃圾收集器(Concurrent Mark Sweep,并发标记清除)
使用标记-清除算法。追求低卡顿。在垃圾收集时用户线程和GC 线程并发执行,因此在垃圾收集过程中用户不会感到明显的卡顿。
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与GC Roots 直接关联的对象进行标记。
- 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清除:只是用一条GC线程,与用户线程并发执行,清除刚才标记的对象。非常耗时。
对于产生的碎片空间问题,可以用过开启-XX:+UseCMSCompactAtFullCollection(默认开启),在每次Full GC 完成后都会进行一次内存压缩整理。设置 -XX:CMSFullGCsBeforeCompaction 告诉GMS,经过N次Full GC之后再进行一次内存整理。
3. G1通用垃圾收集器(Garbage First)
G1时一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是**将堆划分为一块块独立的Region**。当要进行垃圾收集时,首先估计每个Region中垃圾的数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率(Mixed GC模式)。
- 从整体看,G1是基于“标记-整理”算法的收集器,从局部(两个Region之间)看是基于“复制”算法实现的,着意味着运行期间不会产生内存空间碎片。
- 每个Region都有一个Remembered Set集合,用于记录本区域中所有对象引用的对象所在的区域。避免整体扫描问题。
- 工作过程:
- 初始标记:Stop The World, 仅使用一条初始线程对所有与GC Roots 直接关联的对象进行标记。
- 并发标记:使用一条标记线程与用户线程并发执行。进行可达性分析,速度很慢。
- 最终标记:Stop The World,使用多条标记线程并发执行。
- 筛选回收:Stop The World,使用多条回收线程回收废弃对象。
迄今为止,所有收集器再根节点枚举这一步骤时都是必须暂停用户线程的
五、内存分配与回收策略
1. 内存分配
大多数情况下,对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一起Minor GC。
-
当新生代发生一次Minor GC 后,存活下来的对象年龄+1,当年龄超过一定值时,转移到老年代中。
使用-XXMaxTenuringThreshold设置新生代的最大年龄。
如果当前新生代的Survivor中,相同年龄所有对象大小的总和大于Survivor空间的一半,年龄>=该年龄的对象就可以直接进入老年代。无需等到MaxTenuringThreshold 中要求的年龄。
-
空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC, 否则将进行 Full GC。
通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保,这个过程就是分配担保。
2. 回收策略
Minor GC: 回收新生代(包括Eden和Survivor区域)
Major GC/ Full GC: 回收老年代。Major GC 的速度一般会比Minor GC 慢10倍以上。
-
Full GC 触发:
System.gc()调用。建议JVM进行Full GC,不一定执行。可以通过XX:+ DisableExplicitGC禁用System.gc()。
老年代空间不足。若Full GC后依然不足,则抛出
java.lang.OutOfMemoryError: Java heap space
永久代(方法区)空间不足。若Full GC后依然空间不租,则抛出
java.lang.OutOfMemoryError: PermGen space
-
CMS GC 时出现 promotion failed 和 concurrent mode failure
promotion failed:担保失败
concurrent mode failure:执行CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足。
统计得到的Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间。
六、JVM性能调优
1. JDK常用命令
-
jps(JVM Process Status Tool)-- 虚拟机进程状况工具
查看运行中的Java进程:
-
jstat(JVM Statistics Monitoring Tool) --- 虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息,它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
-
jinfo --- 查看配置信息
实时查看和调整虚拟机各项参数。
包括使用中的系统信息、JDK版本、JVM基本信息、JVM参数、运行时命令行参数等信息
java.vm.version = 25.161-b12 java.vm.name = Java HotSpot(TM) 64-Bit Server VM java.runtime.version = 1.8.0_161-b12 VM Flags: Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=60817408 -XX:MaxHeapSize=945815552 -XX:MaxNewSize=315097088 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=19922944 -XX:OldSize=40894464 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC Command line: -Dspring.profiles.active=dev -Dfile.encoding=UTF-8
-
jmap --- 内存映射工具
生成堆转储快照(一般称为heapdump或dump文件)
-
jstack --- 堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
步骤:
- top -p <pid>,显示该pid下java进程的内存情况
- 按H获取每个线程的内存情况
- 找到内存和cpu占用最高的线程tid,并使用 pringf "%x\n" 转为16进制
- 使用 jstack <pid> | grep '十六进制' 查看对应线程执行的堆栈信息
- 根据堆栈信息定位消耗cpu的原因
-
javap --- 字节码分析工具
javap -verbose 字节码文件
2. JDK常用工具
-
JConsole
远程连接方式,项目启动时添加以下参数:
java -Djava.rmi.server.hostname=10.160.13.111 #远程服务器ip,即本机ip -Dcom.sun.management.jmxremote #允许JMX远程调用 -Dcom.sun.management.jmxremote.port=3214 #自定义jmx 端口号 -Dcom.sun.management.jmxremote.ssl=false # 是否需要ssl 安全连接方式 -Dcom.sun.management.jmxremote.authenticate=false #是否需要秘钥,false表示只需要输入用户名 -jar test.jar
注意检查服务器hostname -i 是否指向公网ip
-
jvisualvm
可利用该工具分析程序性能
-
jmc
可持续在线的监控工具
3. 方案
-
使用若干Java虚拟机,建立逻辑集群来利用硬件资源
将jar包分多个端口运行,根据总内存大小设置-Xms和-Xmx,配置负载均衡
七、Class文件结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。
无符号数:基本数据类型,以u1、u2、u4、u8来代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的复合数据类型。为了便于区分,所有表的命名惯性地以“_info"结尾。每个表前都有一个u2的count代表容量计数器(常量池从1开始计数,其他从0开始计数)
-
魔数(magic) u4
确定这个文件是否为一个能被虚拟机接受的Class文件。
Class文件的魔数是用16进制表示的“ CAFE BABE ”
魔数相当于文件后缀名,只不过后缀名容易被修改,不安全。
-
版本信息(minor_version + major_version)u2+u2
5-6字节表示次版本号,7-8字节表示主版本号,它们表示当前Class文件中使用的是哪个版本的JDK。
高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
-
常量池(constant_pool_count + constant_pool )
常量池中的每一项常量都是一个表,表结构起始的第一位是个u1类型的标志位(tag)
常量池中存放两种类型的常量:
-
字面量(Literal)
字面值常量是程序中定义的字符串、被final修饰的值等
-
符号引用(Symbolic References)
符号引用是我们定义的各种名字:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符。
- 方法句柄和方法类型
- 动态调用点和动态常量
特点:
- 常量池中常量数量不固定,因此常量池开头放置一个u2类型的无符号数,用来存储当前常量池的容量。
- 常量池的每一项常量都是一个表,表开始的每一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
-
-
访问标志(access_flags u2)
用于识别一些类或者接口层次的访问信息。包括:这个Class是类还是接口;是否定义为public类型;是否被abstract/final修饰。
-
类索引、父类索引、接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces_count + interfaces)是一组u2类型的数据(interface_count * interface)集合。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的权限定名。
-
字段表集合(fields_count + fields)
字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。
access_flags + name_index + descriptor_index + attributes_count + attrubutes
- access_flags:放字段修饰符,u2
- name_index:字段的简单名称。如int m的简单名称为m
- descriptor_index:字段的描述符。用来描述字段的数据类型。
-
方法表集合(methods_count + methods)
access_flag + name_index + descriptor_index + attrubutes_count + attrubutes
- access_flag:方法修饰符
- name_index:方法的简单名称。如inc()的简单名称为inc
- descriptor_index:方法的描述符。用来描述方法的参数列表(包括数量、类型以及顺序)和返回值。
特征签名:指一个方法中各个参数在常量池中的字段符号引用的集合。
重载要求与原方法具有相同的简单名称和与原方法不同的特征签名。
因为特征签名不包含返回值,故无法仅仅依靠返回值的不同来对一个已有方法进行重载。
-
属性表(attributes_count + attributes)
Class文件、字段表、方法表都可以携带自己的属性表集合。
八、类加载机制
1. 类加载的时机
-
类的生命周期
其中加载、验证、准备、初始化和卸载这五个阶段的开始顺序是确定的。
2. 类加载过程
-
加载
- 通过类的全限定名获取该类的二进制字节流
- 将二进制字节流所代表的静态结构转化为方法区的运行时数据结构
- 在内存中创建一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,再由类加载器创建数组中的元素类。
-
验证
-
文件格式验证
- 是否以魔数 0XCAFEBABE 开头
- 主次版本号是否在当前虚拟机处理范围内
- 常量池是否有不被支持的常量类型
- 指向常量的索引值是否指向了不存在的常量
- CONSTANT_Utf8_info 型的常量是否不符合UTF8编码的数据
- ...
-
元数据验证
对字节码描述信息进行语义分析,确保其符合Java语法规范
- 该类是否由父类(除了java.lang.Object之外,所有的类都应当有父类)
- 该类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载)
- ...
-
字节码验证
对方法体进行语义分析,确定语义是合法的、符合逻辑的。
对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证任何跳转指令都不会跳转导方法体以外的字节码指令上
- 保证方法体中的类型转换总是有效的
- ...
-
符号引用验证
本阶段发生在解析阶段,确保解析正常执行
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问行是否可被当前类访问
- ...
-
-
准备
- 准备阶段是正式为类变量(静态成员变量)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配
- 没有final修饰的静态变量在该阶段初始值是数据类型的零值(0,null..);有final修饰的变量值为指定的值
-
解析
将常量池中的符号引用替换为直接引用
-
初始化
- 执行类构造器 <clinit>() 方法
- <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并中产生的,编译器收集的顺序是由语句在源代码中出现的顺序所决定的
3. 类加载器
-
判断类是否“相等”
- 任意一个类,都由加载它的类加载器和类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。
- 这里的“相等”,包括代表类的Class对象的equals()方法、isInstance() 方法的返回结果、也包括使用instanceof关键字做对象所属关系判定等情况。
-
加载器种类
-
启动类加载器(Bootstrap ClassLoader)
负责将存放在 **<JAVA_HOME>\lib **目录中的(或者被-Xbootclasspath参数所指定的路径中存放的),并且能被虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中。
-
扩展类加载器(Extension ClassLoader)
负责加载 **<JAVA_HOME>\lib\ext **目录中的所有类库,可直接使用扩展类加载器。
-
应用程序类加载器(Application ClassLoader)
负责加载用户类路径(classpath)上所指定的类库,可以直接使用该类加载器
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是这个程序中默认的类加载器。
-
-
双亲委派模型
描述类加载器之间的层次关系。要求除了顶层的启动类加载器外,其余的类加载器都应当由自己的父类加载器。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此;只有当父类加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。
像java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶层的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。确保了这些类的唯一性。