运行时数据区域:
java虚拟机栈与方法区区别
虚拟机栈存放的是局部变量、方法出口、操作数栈、动态链接等信息是一个方法内部独有的
方法区存放的是常量、静态变量、类型信息这些可以供线程共享的数据
程序计数器:每条线程都有一个独立的程序计数器,各个线程之间互不影响独立存储。这类内存区域线程私有。目的是为了线程切换后能够恢复到正确的执行位置。
java虚拟机栈:也是线程私有的,描述的是java方法执行的线程内存模型:每个方法执行的时候,java虚拟机都会同步创建一个栈帧、用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从被调用到执行完毕的过程,都对应了一个栈帧在虚拟机栈中从入栈到出栈的过程。
该内存区域有两类异常情况,一种是线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机容量可以动态扩展,会抛出OutOfMemoryError异常
本地方法栈:与虚拟机栈发挥的作用相似,但是他针对的不是java方法,而是本地方法。
java堆:是jvm所管理的内存中最大的一块,是被所有线程共享的一块区域,在线程启动时创建。唯一目的是存放对象实例,几乎所有对象实例都在这里分配内存
方法区:线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓冲区等数据。永久代->元空间
运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存:并不是虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域,但是这部分内存也被频繁地使用,也可能导致oom异常。
对象探秘:
对象的创建:当java虚拟机遇到一条字节码new指令时,首先会去检查这个指令的参数能否在常量池中定义到一个类的符号引用,并且检查这个类是否已经被加载,如果没有被加载、解析、初始化过,那必须先执行相应的类加载过程。
当类加载检查通过后,虚拟机将为新生对象分配内存 分配方式有两种,指针碰撞和空闲列表
除了划分空间需要考虑 还需要考虑,对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B 又同时使用了原来的指针来分配内存的情况。
两种方案:一 对分配内存空间的动作进行同步处理,。一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,成为本地线程分配缓冲 TLAB
对象的内存布局:三部分:对象头、实例数据、对齐填充。
对象头部分包括两类信息:一类是用于存储对象自身的运行数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
另一类是类型指针,即对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是哪个类实例。
如果对象是一个java数组,那么在对象头中还必须有一块用于记录数组长度的数据。
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是父类继承下来的还是子类中定义的都必须记录。
对齐填充部分:并不是必然存在的,只是因为任何对象的大小都必须是8字节的整数倍。对象头部分已经被设计成正好是8字节的倍数,因此对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
对象的访问定位:java程序会通过栈上的reference数据来操作堆上的具体对象。
访问方式有二:一 使用句柄访问,java堆中可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
二使用直接指针访问,Java中堆对象的内存布局就必须考虑如何防止访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。直接指针速度更快。
“内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。”
outOfMemotyError异常
java堆溢出
虚拟机栈和本地方法栈溢出
方法区和运行时常量池溢出
程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作。这几个区域内不需要过多考虑回收问题,当方法结束或者线程结束时,内存自然就随之回收了。
但java堆和方法区两个地方具有不确定性:一个接口的多个实现类的内存可能会不一样,执行不同分支所需要的内存也可能不一样,只有运行的时候才知道程序到底会创建那些对象,创建多少个对象,这部分内存的分配和回收是动态的。
判断对象是否存活
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是当两个对象互相引用的情况下,导致他们的计数引用都不为零,引用计数算法也就无法回收他们。
java虚拟机并不是用计数引用算法来判断对象是否存活的
可达性分析算法:起始节点 root 和子节点,用引用链来连接。对象不可达时,说明该对象不可能会再被使用。
引用:强软弱虚
生存还是死亡:真正宣告一个对象死亡,要经历两次标记过程,没有与GC root相连的引用链,就会被第一次标记,随后进行一次筛选---关于finalize()方法的筛选 在此过程中 对象有机会拯救自己---只要重新与引用链上任何一个对象建立关联
回收方法区:回收的主要内容:废弃的常量和不再使用的类型。判定一个常量是否“废弃”相对简单,难的是判断一个类型是否属于“不再被使用的类”条件比较苛刻,要满足三个条件:
1、类的所有实例都已经被回收,2加载该类的类加载器已经被回收3该类对应的java.lang.class对象没有在任何地方被引用。
垃圾收集算法
分代收集理论:弱分代假说:朝生夕灭 强分代假说:熬过多次垃圾收集过程的对象就越难以消亡 跨代引用假说:跨代引用对于同代引用来说只是占极少数
部分收集、混合收集、整堆收集
标记-清除算法: 先标记 然后再清除 是最基础的回收算法 缺点:1、如果堆中对象太多,并且大多数是需要回收的,那么标记和清除的过程会随着对象数量的增长而降低 2、碎片化问题,标记-清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法:将内存分成两块,每次只使用其中一块,这一块内存用完了,就将存活着的对象复制到另外一块上面去,然后再把已经使用过的内存空间一次清理掉。如果多数对象都是存活的,那么复制将会带来大量开销。
标记-整理算法:先标记,然后让存活的对象移到内存空间的另一端,再直接清理掉边界以外的内存。由于移动对象的操作需要全程暂停用户应用程序才能进行,这种停顿被称为“stop the world”
是否移动对象?是和否都存在弊端,不移动对象那么内存分配的时候会更复杂,移动对象则内存回收时会更复杂。
Hotspot的算法细节实现:
1、根节点枚举
所有的收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此会面临相似的“Stop the world”的情况。当用户线程停顿下来之后,其实并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机是有办法直接得到那些地方存放着对象引用的。
Hotspot的解决方案是:使用一组称为OopMap的数据结构。一但类加载完成,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,也会在特定的位置记录下来栈里和寄存器里哪些位置是引用。
2、安全点
OopMap帮助HotSpot快速准确地完成了GC Roots枚举,但是如果为每一条指令都生成oppmap,那将会需要大量的额外存储空间。所以Hotspot并没有为每一条指令都生成oopmap,只是在 特定的位置记录了这些信息,这些位置称为安全点。有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能停顿下来进行垃圾收集,而是必须执行到达安全点后才能暂停。
如何在垃圾收集发生时让所有线程都跑到最近的安全点? 有两种选择:抢占式中断与主动式中断
抢占式中断几乎没有虚拟机采用。主动式中断是当垃圾收集需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的。
3、安全区域
安全点似乎完美解决了如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了。但是程序“不执行”的时候呢?所谓不执行就是没有分配处理器时间,比如用户线程处于sleep或者Blocked状态的时候,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也不可能持续去等待线程重新被激活分配处理器时间。因此,引入了安全区域来解决。
安全区域是指确保在某一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始垃圾收集都是安全的。可以看做是拉伸了的安全点。
4、记忆集和卡表 主要用于缩减GC Roots扫描范围
新生代中建立了一种叫记忆集的数据结构,用来避免把整个老年代加入到GC Roots扫描范围。是一中用于记录从非收集区指向收集区域的指针集合的抽象数据结构。比如 用非收集区域中所有的含跨代引用的对象数组来实现。
卡表是记忆集的一中具体实现,它定义了记忆集的记录精度。
卡表中有很多个卡页,一个卡页中包含不止一个对象,只要卡页内有一个或者更多个对象的字段存在跨代指针,那就将对应的数组元素的值标识为1,称这个元素变脏了,没有 则标识为0.。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易地得出那些卡页内存块中包含跨代指针,把它们加入GC Roots中一起扫描。
5、写屏障
在编译阶段的场景中需要一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作中去。在HotSpot中就是用的写屏障这种技术来实现的,写屏障可以看做是在虚拟机层面对“用类型字段赋值”这个动作的AOP切面。
6、并发的可达性分析
如果用户线程与收集器是并发工作的 收集器在对象图上标记颜色,同时用户线程在修改引用关系------修改对象图的结构。可能会发生两种后果。一种是把原本消亡的对象错误标记成存活,产生了一些逃过本次收集的浮动垃圾。第二种是把原本存活的对象错误标记为已消亡,程序会因此而发生错误。
解决方案:增量更新与原始快照。
经典垃圾收集器:
serial收集器:是最基础历史最悠久的收集器。是单线程收集器。事实上,serial收集器迄今为止仍然是Hotspot虚拟机运行在客户端模式下的默认新生代收集器,它简单而高效。
为什么需要stop the world? 你妈妈在给你打扫卫生的时候肯定是要求你老老实实呆在座位上别动,如果她扫地你同时,你一直丢垃圾,那这房间很难打扫完。
ParNew收集器:实质上是Serial收集器的多线程并行版本。
Parallel Scavenge收集器:也是一款新生代收集器
Serial Old收集器:是Serial收集器的老年代版本,也是一个单线程收集器,使用标记-整理算法。
Parallel Old收集器 Parallel Scavenge收集器的老年代版本,基于标记-整理算法。
CMS(Concurrent Low Pause Collector) 并发低迟钝 收集器:是一种以获取最短回收停顿时间为目标的收集器。非常符合很大一部分的集中在B/S架构的java应用。
仍然存在三个明显缺点:1、对处理器资源很敏感。并发阶段虽然不会导致用户线程停顿,但却会因为占用了一部分的线程而导致应用程序变慢,降低总吞吐量。2、无法处理“浮动垃圾” 3、因为其基于标记-清除算法实现,所以在进行完垃圾收集后,可能会有大量的空间碎片产生,会给将来为大对象分配内存带来麻烦。
G1收集器:是里程碑式的成果,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。将堆内存化整为零。分为四步:初始标记,并发标记,最终标记,筛选回收。
低延迟垃圾收集器:
Shenandoah收集器
ZGC收集器
选择合适的垃圾收集器
虚拟机及垃圾收集器日志
内存分配与回收策略:
关于垃圾收集:
Full GC 就是收集整个堆,包括新生代,老年代等收集所有部分的模式
针对 HotSpot VM 的实现,它里面的GC其实准确分类有两种:
Minor GC 是俗称,新生代(新生代分为一个 Eden区和两个Survivor区)的垃圾收集叫做 Minor GC:
当 Eden 区的空间耗尽了怎么办?这个时候 Java虚拟机便会触发一次 Minor GC来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor区。
简单说就是当新生代的Eden区满的时候触发 Minor GC
1、对象优先在新生代Eden区分配,当Eden区没有足够内存时,虚拟机将发起一次minor GC
2、大对象直接进入老年代:大对象就是指的需要大量内存空间的Java对象,比如元素数庞大的数组。Hotspot虚拟机提供了参数--------可以指定大于某个设置值的对象直接在老年代分配
3、长期存活的对象将进入老年代:jvm给每个对象定义了一个对象年龄计数器,存储在对象头中。
对象通常在eden区诞生,如果经过第一次MInor GC依然存活,并且可以被survivor容纳的话,该对象会被移动到Survivor空间中,并且将其年龄设置为1岁。对象在survivor区每熬过一次minor GC,年龄就会增长1岁,当年令增长到一定程度,就会被晋升到老年代中。
4、动态对象年龄判定:Hotspot并不是永远要求对象的年龄必须达到某个标准值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于survivor的一般,年龄大于或等于该年龄就可以直接进入老年代。
5、空间分配担保:在发生minorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果成立,则代表这一次的MinorGC可以确保是安全的的。
冒险进行minorGC是冒了什么险?如果内存回收后新生代中所有对象都存活,就需要老年代来进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。
给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运行知识处理数据的手段
jsp:虚拟机进程状况工具:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类。
jstat:虚拟机统计信息监视工具:
jinfo:java配置信息工具
jmap:java内存映射工具
jhat:虚拟机堆转储快照分析工具
jstack:java堆栈跟踪工具
可视化故障处理工具
JHSDB:基于服务型代理的调试工具
JConsole:java监视与管理控制台
VisualVM:多合一故障处理工具
案例分析
大内存硬件上的程序部署策略
服务器运行状态不理想,网站进场不定期出现长时间失去响应,有可能是由垃圾收集停顿所导致
控制Full GC频率的关键是老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应当太长,尤其是不能有成批量的,长生存时间的大对象产生,这样才能保证老年代空间的稳定。
在许多网站和B/S形式的应用中,多数对象的生存周期都应该是请求级或者页面级的,会话级和全局级的长生命对象相对较少。
集群间同步导致的内存溢出:
堆外内存导致的溢出错误:
直接内存太少,虚拟机虽然会对直接内存进行回收,但是直接内存却不能像新生代,老年代那样,发现空间不足了就主动通知收集器进行垃圾回收,只能等待老年代满后FullGc出现后“顺便”帮它清理掉内存的废弃对象。
除了java堆和方法区之外,下面这些区域也会占用较多的内存:
直接内存、线程堆栈、Socket缓存区、JNI代码、虚拟机和垃圾收集器。
外部命令导致系统缓慢:每个用户请求的处理都需要执行一个外部的shell脚本来获得系统的一些信息。这个shell脚本通过java的Runtime.getRuntime().exec()方法来调用。折中调用方式可以达到执行shell脚本的目的,但是他在java
虚拟机中是非常消耗资源的。最后去掉了该shell脚本执行语句,改为了使用java的API去获取这些信息后,系统很快恢复了正常。
服务器虚拟机进程崩溃:由于MIS系统的用户多,待办事项变化快,为了不被OA系统速度拖累,使用了异步的方式调用Web服务,但由于两边的服务速度完全不对等,时间越长就累积了越来越多的Web服务没有调用完成,导致在等待的线程和socket连接越来越多,最终超过虚拟机的承受能力后导致虚拟机进程崩溃。通知OA门户方修复无法使用的集成接口,并将异步调用改为了生产者消费者模式的消息队列实现后,系统恢复正常。
不恰当使用数据结构导致内存占用过大:HashMap
由Windows虚拟内存导致长时间停顿
由安全点导致长时间停顿
编译时间和类加载时间优化、调整内存设置控制垃圾收集频率、选择垃圾收集器降低延迟
无关性:
所有平台都统一支持的程序存储格式——字节码是构成平台无关性的基石。
java中各种语法、关键字、常量变量、和运算符号的语义最终都会由多条字节码指令组合来表达,这决定了字节码指令所能提供的语言描述能力必须比java语言本身更加强大。
Java程序(*.java)->javac编译器->字节码(*.class)->Java虚拟机
Java技术能够一直保持非常良好的向后兼容性,Class文件结构的稳定功不可没。
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,会按照高位在前的方式分割成若干个8个字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。无符号数属于基本的数据类型。而表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
魔数:每个class文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。文件格式的制定者可以自由选择膜树脂,class文件的魔数值是0xCAFEBABE 紧接着魔数的四个字节存储的是Class文件的版本号,第五个和第六个字节是次版本号,第七第八个是主版本号
常量池:紧接着主次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,是Class文件结构与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,比如文本字符串、被声明为final的常量值。而符号引用则属于编译原理方面的概念。
当虚拟机做类加载时,会从常量池获得对应的符号引用,再在类创建时或者运行时解析、翻译到具体的内存地址之中。常量池中每一项常量都是一个表。
访问标志:常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括,这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话,是否被生命为final 等等。
类索引、父类索引、接口索引集合:类索引(this_class)父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于java不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类的父类索引都不为0.接口索引集合就用来描述这个类实现了那些接口,这些被实现的接口将按implements关键字后的接口顺序从左到右排列在接口索引集合中。
字段表集合:字段表用于描述接口或者类中声明的变量。java语言中的"“字段”包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有 字段的作用域(public等)是实例变量还是类变量?(Static)修饰符、可变性(final)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
方法表集合:依次包括访问标志、名称索引、描述索引、属性表集合。访问标志与字段表中的访问标志有一些不同,比如方法表中没有volatile和transient,而多出了synchronized,native,strictfp和abstract关键字。
那么 方法的定义可以通过访问标志,名称索引、描述符索引来表达清楚,但是方法里面的代码去哪里了? 方法中的java代码,经过javac编译器编译成字节码指令后,存放在方法属性表集合中一个名为“code”的属性里面。
属性表集合:Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
1、Code属性: 出现在方法表的属性集合中,并非所有方法表都必须存在这个属性,,比如接口与抽象类的方法中就不存在Code属性,该属性表中包括max_stack(操作数栈深度最大值)虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度,比如max_locals代表局部变量表所需的存储空间。比如code属性,代表了方法体中的Java代码
2、Exceptions属性:该属性会列举出方法中可能抛出的受检查异常。也就是在throws关键字后面列举的异常。
3、lineNumberTable属性:用来描述java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
..........略
java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(操作码)以及跟随其后的零到多个代表此操作所需的参数(操作数)构成。
字节码与数据类型:jvm指令集中,大多数指令都包含其操作所对应的数据类型信息。比如Iload指令用于从局部变量表中加载int型的数据到操作数栈中。
加载和存储指令:用于将数据在詹振中的局部变量表和操作数栈之间来回传输。
运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。运算指令大概分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。
类型转换指令:可以将两种不同的数值类型相互转换,一般用于实现用户代码中的显示类型转换操作。
对象创建与访问指令:
操作数栈管理指令:
控制转移指令:可以让Java虚拟机有条件或者无条件地从指定位置指令的下一条指令继续执行程序。可以理解为控制指令就是有条件或者无条件地修改PC寄存器的值。
方法调用和返回指令:
异常处理指令:
同步指令:
总结:Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。
上一节讲了Class文件存储格式的具体细节,在Class文件中描述的各类信息,最终都需要加载到虚拟机中之后才能被运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机之后会发生什么变化呢?
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程就是虚拟机的类加载机制。在java语言中,类型的加载、连接、和初始化过程都是在程序运行期间就完成的。
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括:加载、验证、准备,解析,初始化、使用、卸载七个阶段,其中,验证,准备,解析统称为连接。
加、验、准、初始化、卸载这五个阶段顺序是确定的,但是解析并不一定。
加载:加载是整个“类加载”过程中的一个阶段
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个自己恶劣所代表的的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。 验证字节码是java虚拟机保护自身的一项必要措施。
验证阶段有:文件格式验证、元数据验证、字节码验证、符号引用验证。
1、文件格式验证:字节流是否符合Class文件格式的规范。
是否魔数0xCAFEBABE开头?
主次版本号是否在当前Java虚拟机接受范围之内?
常量池的常量是否有不被支持的常量类型?
.........
2、元数据验证:对字节码描述的信息做语义分析,以保证其描述的信息符合《java语言规范》的要求。验证点如下
这个类是否有父类?(除了Object外 其他的类都应该有父类)
这个类的父类是否继承了不允许被继承的类?(被final修饰的类)
............
3、字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对数据类型校验完毕后,这阶段就要对类的方法体(class文件中的code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
例如:不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给一个父类对象,这是安全的,但是把父类对象赋值给子类对象、甚至是赋值给一些不相关的对象,则是危险和不合法的
...........
符号引用验证:可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,就是,该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源。本阶段需要校验:
1、符号引用中通过字符串描述的全限定名是否能找到对应的类。
2、在指定类中是否存在符合方法的字段描述符及简单名称锁描述的方法和字段。
............
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
1、类或接口的解析
2、字段解析
3、方法解析
4、接口方法解析
初始化
运行准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作被放到java虚拟机外部趋势线,以便让应用程序自己决定如何去获取所需的类,实现这个工作的代码被称为“类加载器”
类与类加载器:对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。比较两个类是否“相等”只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个java虚拟机加载,只要加载他们的类加载器不同,他们就必定不相等。
双亲委派模型
站在java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(BootStrap),用C++语言实现。是虚拟机自身的一部分;另外一种是其他所有的类加载器,这些类加载器都由java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
启动类加载器:
扩展类加载器:
应用程序类加载器:
jdk9之前的java应用都是由这三种类加载器互相配合来完成加载的,用户还可以加入自定义的类加载器来进行拓展。
各种记载器之间的层次关系被称为类加载器的“双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应该有自己的父类加载器。
双亲委派模型的工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类反馈无法完成这个加载请求时,子加载器才会尝试自己取完成加载。使用双亲委派模型来组织类加载器之间的关系,让java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。比如object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器进行加载,因此object类在程序的各种类加载器环境中都能够保证是同一个类。
破坏双亲委派模型
双亲委派模型遭到过三次破坏
Java模块化系统
模块的兼容性:为了使可配置的封装隔离机制能够兼容传统的类路径查找机制,JDK9提出了与“类路径”相对应的“模块路径”
上一章说了如何将类加载到虚拟机之中,这一章将讲述虚拟机如何执行定义在Class文件里的字节码。虚拟机的方法调用和字节码执行。
java虚拟机以方法作为最基本的执行单元,“栈帧”则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,他也是虚拟机运行时数据区中的虚拟机栈的战元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
一个线程中的方法调用链可能会很长,以java程序的角度来看,同一时刻,同一条县城中,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧” 与其相关的方法即“当前方法”
局部变量表
是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
操作数栈
是一个后入先出的栈。跟局部变量表一样。最大深度在编译的时候会被写入到Code属性的max_stacks数据项中。
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法盗用过程中的动态链接。
方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法,一种是遇到任意一个方法返回的字节码指令,另一种是方法执行过程中遇到了异常,并且该异常没有得到妥善处理。
方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
方法调用阶段唯一的任务是确定被调用方法的版本,并未涉及方法内部的具体运行过程。一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是直接引用。)
解析
在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。
解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接饮用,不必延迟到运行期再去完成。而另一种主要的方法调用形式:分派调用则复杂得多。
分派
静态分派
动态分派
单分派和多分派
动态类型语言支持
关于动态类型:变量obj本身并没有类型,变量obj的值才具有类型,编译器在编译时最多只能确定方法名称、参数、返回值这些信息,并不需要确定方法所在的具体类型。“变量无类型而变量值才有类型”
基于栈的字节码解释执行