JVM体系总结
2023-04-04 23:44:15 1 举报
AI智能生成
适用于面试快速复习
作者其他创作
大纲/内容
JVM作用
Java实现跨平台的原理就是: 一次编译,到处运行 将源文件编译成class文件。 只要提供并且安装了相对应的虚拟机就可以跨该平台
Java文件 --> javac编译器 --> Class文件 --> JVM --> 机器码
JVM 模型由以下三个主要部分组成
Class Loader 子系统:负责加载各种类文件到 JVM 中,并将这些类文件转化为运行时数据结构,使得这些类可在 JVM 中被调用。
Class Loader 具有三个层次 :Bootstrap Class Loader、Extension Class Loader 和 Application Class Loader。
运行时数据区
执行引擎
将字节码指令翻译成底层操作系统可以理解的机器指令,并执行这些指令。执行引擎有两种类型:解释执行和编译执行。
Java虚拟机的运行时数据区域
程序计数器
当前线程所执行的字节码的行号指示器。
⼯作时通过改变这个计数器的值来选取下一条需要执行的字节码指令实现代码流程控制,分⽀、循环、跳转、异常处理、
保证每条线程切换后可以恢复到正确的位置,每条线程在方法执行时都需要一个私有的计数器,相互独立,互不干扰。
虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型
每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧
局部变量表
用来存放:局部变量 、方法参数、8种基本数据类型、对象引用地址、ReturnAddress 返回 、字节码地址
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,
在方法运行期间不会改变局部变量表的大小。
在方法运行期间不会改变局部变量表的大小。
操作数栈
在一步细化后的栈:就是方法中的 计算操作需要的临时空间,也是栈结构(先进后出)列如:加减乘除
存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。
⽅法出⼝信息,记录方法被调用返回的位置
动态链接
主要服务一个方法需要调用其他方法的场景,Class 文件的常量池里保存有大量的符号引用,比如方法引用的符号引用。
当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
程序运行中栈可能会出现两种错误
StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误
OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
本地方法栈
与虚拟机栈类似,为虚拟机使用到的native方法服务(不是Java实现的方法)
本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数 栈、动态链接、出⼝信息
堆 Heap
存放对象实例
描述
Java 虚拟机所管理的内存中最⼤的⼀块,此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
分代结构
新生代内存(Young Generation)
Eden
大部分的对象在Eden区分配内存,且大部分对象也在Eden区被回收:Eden 区的角色是为了促进短期存活对象的快速分配和回收
Survivor from
Survivor To
老生代(Old Generation)
存放了长时间存活 或者占内存较大的对象实例,这些对象不会被频繁的Minor GC选中(复制算法),对程序的内存使用和垃圾回收效率有着重要的影响
永久代(Permanent Generation)
JDK8后永久代被 元空间取代,元空间使用的是本地内存
新生代采用标记-复制算法,老年代采用标记-整理算法。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本质上就是一个HashSet<String> ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
为什么分代?将对象根据存活概率进行分类,对存活时间长的对象,放到固定区从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。
会出现的异常
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误
java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误
java堆参数配置
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时dump当前内存堆快照
最大堆内存可通过-Xmx参数配置
方法区(永久代)
主要存放 类信息、常量、静态变量、即时编译器编译后的代码等数据
在JDK1.7之前存在 其实方法去属于 堆的逻辑部分 但 是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来
方法区和永久代的区别
⽅法区和永久代的关系很像Java中接⼝和类的关系,类实现了接⼝,⽽永久代就是HotSpot虚拟机对虚拟机规范中⽅法区的⼀种实现⽅式。 也就是说,永久代是HotSpot的概念,⽅法区是Java虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久带这⼀说法。
-XX:PermSize=N //⽅法区(永久代)初始⼤⼩
-XX:MaxPermSize=N //⽅法区(永久代)最⼤⼤⼩,超过这个值将会抛出
OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
-XX:MaxPermSize=N //⽅法区(永久代)最⼤⼤⼩,超过这个值将会抛出
OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
元空间
JDK 1.8 的时候,⽅法区(HotSpot的永久代)被彻底移除了(JDK1.7就已经开始了),取⽽代之是元空间,元空间使⽤的是直接内存。
-XX:MetaspaceSize=N //设置Metaspace的初始(和最⼩⼤⼩)
-XX:MaxMetaspaceSize=N //设置Metaspace的最⼤⼤⼩
-XX:MaxMetaspaceSize=N //设置Metaspace的最⼤⼤⼩
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。
运⾏时常量池
运⾏时常量池是⽅法区的⼀部分,JDK1.7及之后版本的 JVM 已经将运⾏时常量池从⽅法区中移了出来,在 Java 堆(Heap)中开辟了⼀块区域存放运⾏时常量池。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
常量池包含的内容
字面量
文本字符串
被声明为final的常量值
基本数据类型的值
符号引用
类和结构的完全限定名
字段名称和描述符
方法名称和描述符
接口方法符号
JVM 垃圾回收
JVM 内存分配与回收策略?
对象优先在Eden区分配
Eden区内存足够
直接Eden分配对象内存
Eden区内存不足
虚拟机将发起一次Minor GC(采用复制算法收集)
内存足够
直接Eden分配对象内存
内存不足
新生代对象提前转移到老年代(分配担保机制)
新生代内存足够
直接Eden分配对象内存
新生代内存不够
大对象直接在老年代分配对象内存
老年代内存不足
触发 Full GC
大对象直接进入老年代
如果对象超过设置大小 或者 Eden区minor GC后都不能存放的对象会直接进入老年代
长期存活的对象将进入老年代
多次Miner GC后超过年龄阈值的存活下来的对象
对象动态年龄判断
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值
Minor gc后存活的对象Survivor区放不下
这种情况会把存活的对象部分挪到老年代,部分可能还会放在Survivor区
老年代空间分配担保机制
在执行任何一次Minor GC之前,JVM都会检查一下老年代的可用连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
Full GC: 清理整个堆空间,包括年轻代和永久代。GC期间会停止所有线程等待GC完成STW (Stop-The-World)
如何判断对象可以被回收?
引用计数算法
算法逻辑是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的
可达性分析算法
可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
可以作为gc roots的对象
虚拟机栈中引用对象
方法区静态属性引用对象
方法区常量引用对象
本地方法栈JNI(native方法)引用对象
引用分类
强引用
指在程序代码之中普遍存在的,类似 “Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
软引用:用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用
弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK1.2 之后,提供了 WeakReference 类来实现弱引用。
虚引用
虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后,提供了 PhantomReference 类来实现虚引用。
生存还是死亡?
即使在可达性分析算法中不可达的对象,也并非是 “非死不可”的,这时候它们暂时处于 “缓刑” 阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动创建的、低优先级的 Finalizer 线程去执行它。这里的 “执行” 是指虚拟机会触发这个方法,但不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,比如在自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出 “即将回收” 的集合;如果对象这个时候还没有逃脱,那基本上它就真的被回收了。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动创建的、低优先级的 Finalizer 线程去执行它。这里的 “执行” 是指虚拟机会触发这个方法,但不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,比如在自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出 “即将回收” 的集合;如果对象这个时候还没有逃脱,那基本上它就真的被回收了。
垃圾回收算法
标记-清除(Mark-Sweep)
过程:标记 和 清除
先标记出所有要回收的对象
在标记完成后统一回收所有被标记的对象
缺点
标记、清除效率都不高
产生大量不连续碎片
碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。
复制算法(Copy)
过程
内存按容量划分为相等的两块
当一块用完,就将活着的对象复制到另一块
然后把使用过的内存一次清理
优点
实现简单,运行高效,没有碎片产生
商业虚拟机用来回收新生代
缺点
缺点需要两倍的内存空间
应用
年轻代 中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。
标记-整理算法(Mark-Compact)
过程
首先标记可回收的对象
将存活的对象都向一端移动
然后清理掉边界以外的内存
优点
此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题
应用
老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。
分代收集算法
一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
新生代
复制算法
在新生代中,每次收集都有大量对象死去,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃圾收集。
年老代
标记清除
标记整理
垃圾收集器
串行收集器 Serial
串行收集器(Serial)
虚拟机client模式下默认新生代收集器
优点
简单高效
没有线程交互开销
停顿时间短
缺点
必须暂停所有工作线程
单线程
串行收集器 Serial Old
serial 老年代收集器,单线程,client模式下虚拟机使用
标记整理算法
并行收集器(Parallel) 这是 JDK1.8 默认收集器
Parallel Scavenge
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样
新生代收集器,多线程
复制算法
目标控制吞吐量
吞吐量:即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
调优
-XX:MaxGCPauseMills 最大垃圾收集停顿时间
GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的
-XX:GCTimeRatio 设置吞吐量大小
Parallel Old
Parallel Scavenge 老年代版本,多线程
标记整理算法
ParNew
serial多线程版本,虚拟机server模式下首选新生代收集器
优点
除了serial只有他能与CMS配合
默认线程数与cpu数相同
缺点
单cpu不会比serial效果好
调优
-XX:+UserParNewGC 指定使用
-XX:ParallelGC-Threads指定线程数
CMS收集器(Concurrent Mark Sweep)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
过程
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。
优点
并发收集 低停顿
缺点
对cpu敏感,占用cpu,导致应用程序变慢,中吞吐量降低
无法处理浮动垃圾(floating garbage)
浮动垃圾:出现在标记过程后的垃圾,只能下次gc处理
产生碎片
不能等年老代几乎满了在回收,需要预留一部分空间给应用程序使用
默认占用92%后触发
产生Concurrent mode failure :CMS运行期预留内存无法满足程序需要
启动SerialOld重新收集年老代,停顿时间拉长
标记清除算法
G1收集器(Garbage First)
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
G1收集器将堆内存划分多个大小相等的独立区域(Region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了再有限时间内获得更高的收集效率。
过程
初始标记
标记与gc roots直接关联的对象
并发标记
从gc roots 开始对堆中对象做可达性分析,这个阶段耗时比较长,但也可以与应用线程并发执行
最终标记
修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
gc停顿
筛选回收
对各个region的回收价值和成本排序
根据用户期望gc停顿时间制定回收计划
优点
并行并发,缩短stop the world的时间
分代收集,不需要其他收集器配合,独立管理gc堆
空间整合:不产生空间碎片
可预测停顿:可以指定长度为M毫秒的时间片内,消耗在gc上的时间不超过N毫秒
维护优先列表,每次根据允许手机的时间,回收价值最大的region
避免全堆回收
堆布局
堆划分为大小相等的独立区域(region),保留新生代年老代概念
标记整理算法
如何判断一个类是无用的类?
方法区主要回收的是无用的类,判断类无用同时满足三个条件
该类所有的实例都已经被回收,也就是 Java堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
如何判断⼀个常量是废弃常量?
假如在常量池中存在字符串 "abc",如果当前没有任何String对象引⽤该字符串常量的话,就说明常量
"abc" 就是废弃常量,如果这时发⽣内存回收的话⽽且有必要的话,"abc" 就会被系统清理出常量池。
"abc" 就是废弃常量,如果这时发⽣内存回收的话⽽且有必要的话,"abc" 就会被系统清理出常量池。
finalize()
finalize不执行的情况
当前对象没有覆盖finalize
finalize已经被调用过
若finalize有必要执行
该对象加入F-Queue队列,后续由低优先级的Finalizer线程去执行
finalize是对象逃脱回收的最后一次机会,只要将this与引用链上任何一个对象建立关联即可
类加载机制
加载过程
加载
通过全类名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成该Class对象,作为方法区该类的数据访问入口
验证
确保Class文件字节流包含的信息符合当前虚机要求不会危害虚机自身安全
过程
文件格式验证
魔数
主次版本号
常量池是否有不支持的常量
元数据验证
是否有父类
是否继承了final修饰的类
是否实现了父类要求实现的所有方法(interface)
类中字段、方法是否与父类产生矛盾,是否覆盖final字段,方法重载是否合法
字节码验证
符号引用验证
符号引用中的全限定名能否找到对应的类
NoSuchFieldError
NoSuchMethodError
符号引用的类、字段、方法是否有访问权限
IllegalAccessError
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
这时候进行内存分配的仅包括类变量
public static int value = 123
准备阶段过后,value的值为0,因为此时未执行任何java方法
public static final int value = 123
存在字段属性表constantValue中,准备阶段就会赋值为123
解析
将常量池的符号引用替换为直接引用
符号引用
使用时能无歧义的定位到目标的一组符号
与虚拟机的内存布局无关
引用目标不一定已经加载到内存
直接引用
直接指向目标的指针
与虚拟机内存布局相关
引用目标一定已经加载到内存
初始化
初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)
clinit()(class init)
由编译器自动收集所有类变量的赋值动作和静态语句块合并产生
编译器收集的顺序是有语句在源文件中出现的顺序决定的
静态语句块只能访问定义在静态语句块之前的变量,定义在之后的变量,可以赋值,不可访问
父类clinit先执行
接口例外
接口或者实现类的clinit执行时不用执行父接口的clinit
只有当父接口中定义的变量使用时,父接口clinit会执行
若类中没有静态语句块,也没有类变量赋值,编译器可以不生成clinit()
多线程
多线程同时初始化同一个类,只有一个线程会执行clinit方法,其他线程等待,直到该线程退出
如果clinit耗时很长会导致多个线程阻塞
对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类)
类加载器
判断两个类是否“相等”,必须在同一个类加载器加载的情况下
equals
isAssignableFrom
isInstance
instanceof
类型
启动类加载器(bootstrap classloader)
加载<java_home>/lib下的,按照文件名shibie
-Xbootclasspath指定加载路径
扩展类加载器(extesion classloader)
加载<java_home>/lib/ext目录
应用程序类加载器(application classloader)
ClassLoader.getSystemClassLoader()返回
加载classpath指定类库
双亲委派
过程
检查是否加载过
调用父类加载器loadClass()
若父加载器抛出classNotFoundException,再调用自己的findClass()
非强制性约束
解决基础类调用用户代码
JNDI(Java Naming and Directory Interface)
JDBC
线程上下文类加载器(ThreadContext Classloader)
Thread.setContextClassLoader
完成父类加载器请求子类加载器完成类加载动作
代码热替换(hotSwap)模块热部署(hot deployment)
OSGI(Open Service Gateway Initiative)
对象的创建过程
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
内存分配
指针碰撞
适用场合 :堆内存规整(即没有内存碎片)的情况下。原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表
适用场合 : 堆内存不规整的情况下。原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。使用该分配方式的 GC 收集器:CMS
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
实例化对象执行init方法
jdk 工具
jps(jvm process status)
列出正运行的虚拟机进程 jps [options] [hostid]
-l:输出完整的进程名和主类的包路径
-m:输出虚拟机启动时传递给主类main()方法的参数
-v:输出虚拟机进程启动时的JVM参数
-q:只输出进程ID,省略主类的名称信息
-Joption:将选项传递给Java虚拟机,如-Xms、-Xmx等
-help:显示帮助信息
-m:输出虚拟机启动时传递给主类main()方法的参数
-v:输出虚拟机进程启动时的JVM参数
-q:只输出进程ID,省略主类的名称信息
-Joption:将选项传递给Java虚拟机,如-Xms、-Xmx等
-help:显示帮助信息
jstat(jvm statistics monitoring tool)
用于收集 HotSpot 虚拟机各方面的运行数据
-class: 监视类装载、卸载数量、总空间、耗时等信息
-compiler: 监视JIT编译器编译的数量、时间、失败的数量等信息
-gc: 监视堆内存使用情况、GC情况等信息
-gccapacity: 监视堆内存容量、使用情况等信息
-gcnew: 监视新生代GC情况
-gcnewcapacity: 监视新生代内存容量、使用情况等信息
-gcold: 监视老年代GC情况
-gcoldcapacity: 监视老年代内存容量、使用情况等信息
-gcutil: 监视GC总体情况,包括堆内存使用率、GC时间占比等信息
-printcompilation: 输出已经被JIT编译的方法列表和编译状态
-uptime: 显示JVM启动以来的时间和加载类数
jstat [option vmid [interval[s|ms] [count]]]
interval:查询间隔
count:查询总次数
jstat -class -t 18112 2s 2
加上 -t参数可以在输出信息上加一个 Timestamp 列,显示程序的运行时间
jinfo(configuration info for java)
实时查看和调整虚拟机参数
-sysprops 打印System.getProperties()
-flag name=value 设置虚拟机参数值
jmap(memory map for java)
生成heapdump(堆转储快照)
jmap[option] vmid
jhat(jvm heap analysis tool)
与jmap搭配使用,内置http/html服务器,可以将dump分析结果在浏览器查看
实际工作中不会在生产服务器分析dump
jstack(stack trace for java)
生产虚拟机当前线程快照(theaddump/javacore)
用来定位线程出现长时间停顿原因,线程死锁、死循环、请求外部资源长时间等待等
jstack[option] vmid
Class文件结构
简介
任何一个Class文件都对应唯一一个类或接口定义
8字节为基础单位的二进制流
组成
魔数(magic number)
前4字节
唯一作用:确定能否被虚拟机接受(图片格式也有魔数,入gif、jpeg)
不用扩展名来识别,因为扩展名可以随意改动
版本号
魔数后4个字节
5、6字节为次版本号(Minor Version)
7、8字节为主版本号(Major Version)
虚拟机拒绝执行超过其版本号的Class
常量池
紧接着版本号后,长度不固定
class文件中资源仓库
常量池入口:constant_pool_count,代表常量数量
类型
字面常量(literal)
文本字符串
final 常量值
符号引用(symbolic references)
类和接口的全限定名(fully qualified name)
字段的名称和描述符(descroptor)
方法名和描述符
访问标志
紧接着常量池两个字节,access_flags
类还是接口、是否public、是否abstract、是否final
类索引、父类索引、接口索引集合
按顺序排列在访问标志后
类索引:确定类的全限定名
父索引:确定父类全限定名
接口索引集合:实现接口列表
字段表集合
描述接口或者类中声明的变量
类型
类级变量
static修饰
实例级变量
组成
access_flag
字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)
name_index(名称索引)
对常量池引用
descriptor_index(描述符索引)
对常量池引用
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
方法表集合
描述接口或类声明的方法
组成
access_flag
name_index
descriptor_index
attributes
Class文件中方法的特征签名包括返回值,即两个方法出了返回值其他都一样也可以共存于Class文件中
属性表(attribute_info)集合
属性类型
code
储存字节码指令
max_stack
操作数栈深度最大值
LineNumberTable
描述java源码行号与字节码行号对应关系
LocalVariableTable
描述栈帧局部变量表变量与java源码变量的关系
SourceFIle
记录生成该Class的源码文件名
ConstantValue
通知虚拟机自动为静态变量赋值
只有static修饰的才使用这里个属性
InnerClasses
记录内部类与宿主类之间的关联
Signature
保证运行期反射获取泛型信息
BootstrapMethods
用于保存invockedynamic指令引用的引导方法限定符
invockedynamic运行时动态解析调用点限定符所引用的方法
用于实现Lambda表达式
JVM原理调优
了解JVM的好处
- 遇到内存溢出可以知道什么原因
- 知道代码运行原理,提高代码执行效率
- JVM调优
JVM的4中引用类型
强引用
Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
软引用
如果内存够用就不回收,如果不够用了就回收
弱引用
只要遇到垃圾回收,不管内存够不够用都会回收
虚引用
形同虚设,不管什么时候都有可能被回收
JVM 调优
jvisualvm java 虚拟机诊断工具 Visual VM插件
JVM调优的目的是?
减少 full GC过程中的 STW(stop work)时间
减少 full GC次数
能否对JVM调优,让其几乎不发生full gc?
描述一下 JVM 加载 class 文件的原理机制?
说⼀下Java对象的创建过程
类加载检查
分配内存
初始话零值
设置对象头
执⾏ init ⽅法
0 条评论
下一页