JVM高级特性
内存管理
jvm是自动内存管理的
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域 有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是 依赖用户线程的启动和结束而建立和销毁
线程独享
- 虚拟机栈
- 生命周期 与线程相同
- 每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。
- 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和 double类型的数据会占用两个变量槽,其余的数据类型只占用一个。当进入一个方法时,该方法在栈帧中的内存分配已经确定,且运行期间不会改变,内存分配是以变量槽为基本单位分配的
- 两种异常:
- StackOverflowError:栈深度不够
- OutOfMemoryError(OOM):栈内存不够
- 本地方法栈
- 调用本地方法所存的具体数据(Native)
- 程序计数器(PC)
- 当前线程所执行的字节码的行号指示器
- 是一块较小的内存空间,存储着下一条将要执行的指令的地址,是程序控制流的治时期
- 存"行号"------指示要执行的下一条指令的值
- 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一 个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因 此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程 之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭
线程共享
- 方法区
- 存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 堆
- JVM中内存最大的一种,是被所有线程共享的一块内存
- 唯一目的就是存放对象实例,Java里几乎所有的对象实例都在此分配内存
- Java堆是垃圾收集器管理的内存区域,由于现代垃圾收集器大部分都是基于分 代收集理论设计,所以会分为"新生代""老年代""永久代""Eden空间""From Survivor空 间""To Survivor空间"等
- 分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率(先分配一部分内存栈使某一线程独占)
- 将Java堆细分的目的只是为了更好的回收内存或更快的分配内存
对象的创建
在虚拟机为新生对象分配内存时,主要有两种方法:
- 指针碰撞
- 假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象大小相等的距离
- 空闲列表
- Java堆中的内存并不是规整的,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分 配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
- 选择哪种分配方式由Java堆是否规整 决定,而Java堆是否规整又由所采用 的垃圾收集器是否带有空间压缩整理(Compact)的能力决定 。因此,当使用Serial、ParNew 等带压缩 整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效 ;而当使用CMS这种基于清除 (Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存
- 每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定。
对象内存布局(对象在堆中是如何构成的)
-
对象头
*mark word
- 类型指针
- 即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例
- 数组长度(如果对象为数组的话才会考虑到)
- 类型指针
-
示例数据
- 是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
-
对齐填充
- 并不是必然存在的,也没有特别含义,仅仅是占位符的作用。
- 要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
- 并不是必然存在的,也没有特别含义,仅仅是占位符的作用。
对象的访问定位
创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具 体对象。但是reference类型只规定了一个指向对象的引用,并未定义具体方法,所以主要的实现还是由虚拟机
- 句柄
- 使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就 是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
- 直接指针
- 使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关 信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问 的开销。
- 两个比较
- 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地 址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改。
- 使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访 问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本
垃圾收集与内存分配
垃圾收集(Garbage Collection),垃圾 收集需要完成的三件事情:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
判断对象已死
- 引用计数法
- 脑门刻字法
- 原理:在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。
- 优缺点
- 优点:原理简单,判定效率高,在大多数情况下都是一个不错的算法,经典案例比如:微软COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer
- 缺点:需要考虑很多例外情况,必须要配合大量额外处理才能保证正确工作,譬如单纯的引用计数 就很难解决对象之间相互循环引用的问题。
- 脑门刻字法
- 可达性分析
- 平地长树法
- 原理:通过 一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的
- 图示如下:
- 在java里,固定可以作为GC Roots的对象包括以下:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- 所有被同步锁(synchronized关键字)持有的对象等
- 在java里,固定可以作为GC Roots的对象包括以下:
- 平地长树法
引用
java讲引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,引用强度依次降低
- 强引用:
- 最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj=new Object()"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
- 软引用:
- 用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常
- SoftReference类来实现软引用
- 弱引用:
- 用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。
- WeakReference类来实现弱引用。
- 虚引用:
- 也称为"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
- PhantomReference类来实现虚引用。
回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
- 回收废弃常量
- 与回收Java堆中的对象非常类似,当发生内存回收时,垃圾收集器判断确有必要(常量没有任何地方引用),那么该常量就将会被系统清理出常量池
- 不再使用的类型
- 判断一个类是否属于"不再被使用的类"要同时满足三个条件
- 该类的所有实例都已经被回收,即Java堆中不存在该类及其任何派生自类的实例
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方 法
- Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是"被允许",而并不是 和对象一样,没有引用了就必然会回收。
- 判断一个类是否属于"不再被使用的类"要同时满足三个条件
垃圾收集算法
-
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为"引用计数式垃圾收集"(Reference Counting GC)和"追踪式垃圾收集"(Tracing GC)两大类,这两类也常被称作"直接垃圾收集"和"间接垃圾收集" (已下所述皆为追踪式垃圾收集)
-
分代收集理论
建立在两个分代假说之上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡(熬过多次垃圾收集即被引用的次数很多或者一直在被引用,所以此类很难被回收)。
-
垃圾收集器的一致的设计原则(由上面的两个假说奠定):
- 收集器应该将Java堆划分 出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储。
- 具体过程:
- 将那些朝生夕灭的的对象放到一起,该区域每次垃圾收集的时候只需要考虑那些存活下来的即可,不需要耗费大量资源去标记被回收的对象,以较低代价回收到大量的空间;
- 相对的,如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有 效利用。
-
关于新生代和老年代
- 在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
-
跨代引用
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数
- 存在互相引用关系的两个对象,是应该倾 向于同时生存或者同时消亡的
- 根据这条假说,在碰到跨代引用时,只需在新生代上建立一个全局的数据结构(该结构被称 为"记忆集",Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会 存在跨代引用。
-
主要代:
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意"Major GC"这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为
-
-
标记-清除算法
- 最早出现也是最基础的垃圾收集算法
- 算法分为"标记"和"清除"两部分,过程如下:
- 首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象(标记过程就是判定对象是否属于垃圾)。
- 缺点:
- ①执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低
- ②内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 图示如下:
-
标记-复制算法
- 常被简称为复制算法
- 基本思路:
- 将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 基本思路:
- 优缺点:
- 优点:不会产生内存碎片(因为每次都是对整个半区进行操作),只要移动堆定指针,按顺序即可,实现简单,运行搞笑
- 缺点:特别浪费空间,因为每次回收都将可用内存缩小为原来的一半
- 图示:
- 大部分都采用该方法去回收新生代(新生代中由98%的对象熬不过第一轮收集)
- 半区复制分代算法的优化:Appel式回收
- 具体做法
- 把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空 间。
- 具体做法
- 常被简称为复制算法
-
标记-整理算法
- 主要是针对老年代(因为老年代的存活率基本接近100%,此时使用标记复制的话,就需要全部复制,效率会降低)
- 过程:
- 主要分为两个过程,先标记,在整理,标记的过程与标记清楚的过程相同,整理的意思则是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内 存
- 标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动 式的
- 图示:
- 移动回收后对象的风险决策:
- 若移动存活对象,就与要面临"Stop The World",即更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用 程序才能进行
- 同时,若不考虑移动和整理存活对象,那么也会跟标记清除一样产生内存碎片的问题,此时就只能依赖更为复杂的内存分配器和内存访问器来解决,譬如通过"分区空闲分配链 表"来解决内存分配问题(但这必定会影响吞吐量,同样影响效率)
- 基于以上两点的解决方法:
- 让虚 拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间(基于标 记-清除算法的CMS收集器就是如此)
经典的垃圾收集器
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,CMS非常符合B/S的服务端
-
主要步骤:
CMS是基于标记-清除算法实现的
- 初始标记(CMS initial mark)
- 需要"Stop The World",仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark)
- 需要"Stop The World",就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
- 重新标记(CMS remark)
- 该阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短
- 并发清除(CMS concurrent sweep)
- 清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
- 初始标记(CMS initial mark)
-
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
- 图示如下:
- 图示如下:
-
优点:并发收集、低停顿(故一些文档也称之为"并发低停顿收集器")
-
缺点:
- ①CMS收集器对处理器资源非常敏感
- ②由于CMS收集器无法处理"浮动垃圾"(Floating Garbage),有可能出现"Con-current Mode Failure"失败进而导致另一次完全"Stop The World"的Full GC的产生
- CMS是一款基于"标记-清除"算法实现的收集器,会产生内存碎片
Garbage First收集器(G1收集器)
Garbage First(简称G1)收集器开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式
- G1是一款主要面向服务端应用的垃圾收集器
- G1开创的基于Region的堆内存布局是它能够实现这个目标的关键
- G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region)
- 主要步骤:
- 初始标记(Initial Marking)
- 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿
- 并发标记(Concurrent Marking)
- 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking)
- 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录
- 筛选回收(Live Data Counting and Evacuation)
- 负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的
- 初始标记(Initial Marking)
- G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保 证吞吐量所以才选择了完全暂停用户线程的实现方案
- 图示:
- 图示:
内存分配与回收策略
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存 以 及自动回收分配给对象的内存。
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC(新生代垃圾收集)
虚拟机的内存分配,会将内存分为老年代与新生代,其中新生代又分为Eden和Survivor,一般Eden区与一 个Survivor区的空间比例是8∶1
大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组
- 避免大对象的原因:
- 在分配空间时,它容易 导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,
- 当复 制对象时,大对象就意味着高额的内存复制开销
- HotSpot虚拟机提供了**-XX:PretenureSizeThreshold** 参数,指定大于该设置值的对象直接在老年代分配
- 目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。
长期存活的对象将进入老年代
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存 活对象应当放在新生代,哪些存活对象放在老年代中。
- 解决
- 虚拟机会给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中
- 过程:
- 对象在Eden中诞生,若经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁
- 对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中
- 对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置
动态对象年龄判断
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到 XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。
空间分配担保
- 在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若该条件成立,那这一次Minor GC可以确保是安全的
- 若不成立,虚拟机会先查看 XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure)
- 若允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的
- 若小于,或者-XX: HandlePromotionFailure不允许冒险,那这时就要改为进行一次Full GC。
虚拟机性能监控、故障处理工具
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映射工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
虚拟机执行子系统
类文件结构
虚拟机类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化 ,最 终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
- 一个类的生命周期
- 加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称 为连接(Linking)
- 在上图中,加载、验证、准备、初始化和卸载这几个阶段的顺序是确定的,但解析就不一定了,它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
- 同时注意,按部就班地"开始",而不是按部就班地"进行"或按部就班地"完成",强调这点是因为这些阶段通常都 是互相交叉地混合进行的
- 类初始化阶段的六个有且只有的情况:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
- 使用java.lang.reflect包的方法对类型进行反射调用的时候
在上图中,加载、验证、准备、初始化和卸载这几个阶段的顺序是确定的,但解析就不一定了,它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)
- 同时注意,按部就班地"开始",而不是按部就班地"进行"或按部就班地"完成",强调这点是因为这些阶段通常都 是互相交叉地混合进行的
- 类初始化阶段的六个有且只有的情况:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
- 使用java.lang.reflect包的方法对类型进行反射调用的时候