Java性能优化JVM相关
Java性能优化JVM相关
Calyee雨雀照片引用实现参考Github hexo-yuque-picture,直接通过MarkDown语法链接引用即可(也可以通过标签)
TCP🥢
三次握手
- 第一次握手:客户端向服务端发送一个 SYN
- 第二次握手:服务端接收到然后发送一个 ACK+SYN 给客户端
- 第三次握手:客户端接收到其信息,那么则确认接受发生都没问题,然后发送一个 ACK 给服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
- 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
- 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
- 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常
四次挥手
- 第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
- 第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
- 第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
- 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
- 客户端 FIN: 我话说完了,宝宝,你还有话要说吗?
- 服务端 ACK: 宝宝,我还有几句话没说完,我说完在挂。
- 服务端 FIN: 宝,我说完了。你还有话吗?没有的话那可以挂了。
- 客户端 ACK: 宝,我也说完了。(此时电话就可以挂断了)
只要四次挥手没有结束,客户端和服务端就可以继续传输数据!
为什么 TCP 的四次挥手的等待时间为 2sml?
MSL(Maximum Segment Lifetime) : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。
Java内存结构
在 jdk1.7 的时候
jdk8
区别:在 1.8 的时候把线程共享的方法区变成在本地内存的元空间了。
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
JVM🥢
内存布局(5个)🥢
线程共享(两个):堆、方法区
线程私有(三个):程序计数器、虚拟机栈、本地方法区栈
什么是GC?
介绍JVM-GC
GC:就是 garbage collection 垃圾回收
Stop The World
需要了解 GC,那么得先知道一个词 Stop The World,这个可以理解为程序的停顿时间。当 Stop The World 发生的时候,出来 GC 所需的线程外,所有的线程都进入等待状态直到 GC 完成。
它回收什么区域呢
堆和方法区的基本数据和对象(那不就是线程共享的数据吗)
如何判断对象可以被回收了
可以通过引用记数法,在引用的对象加一个计数器,有引用则+1
可达性分析法(根搜索算法):通过一条引用链,对调用的根节点一个一个的遍历,假如遇到不可达的节点,那么它的子节点也都不可达,就是通过链
什么时候触发 GC
当内存不足时
当创建一个新的对象或者基本数据类型的时候
分代
其中分为新生代和老年代,针对不同的年代使用不同的垃圾回收策略
分代回收机制🥢
对于新生代,他绝大多数对象都是朝生夕死的,每次触发GC,这个区域里大部分对象都会被回收,非常适合使用可达性分析法,因为从根节点顺着引用链遍历下去,只有在这个引用链上的才是存活的,假设本次触发GC,这个区域里90%的对象都要被回收,但实际上只需要关注引用链上10%的对象就可以了,使用复制算法把这10%移动到一个幸存者区域,剩下的直接释放即可。
大部分情况下,对象都首先会在Eden 区域进行创建,然后在触发 Minor GC 存活下来的对象,此时如果能被皮Survivor容纳的话,它会进入Survivor 区,此时它会被赋予初始年龄 1 岁(在新生代最大年龄阈值为 15)
对于熬过很多次依然存活的对象,这种对象一般很难被回收了,这样的情况下,每次GC都对他们进行搜索标记,太浪费资源。把它们放到老年代区,这样JVM就能以较少的频率来回收这个区域,假如老年代的空间占比是60%,在不触发老年代回收的情况下,只需要对占比40%内存空间的新生代进行搜索和释放,效率提升还是很明显的
Minor GC:只回收新生代区域。
Major GC:只回收老年代区域。只有CMS实现了Major GC,所以在老年代里,触发GC,除了CMS和G1之外的其他收集器,大多数触发的其实是 Full GC
Full GC:回收整个堆区和方法区
Mixed GC:回收整个新生代和部分老年代。G1收集器实现了这个类型。
新生代
对于新生代,它大多数对象都是朝生夕死的,那么就非常适合使用标记-复制算法
新生代GC收集的执行顺序如下:
1、绝大多数新创建的对象会存放在伊甸园空间(Eden)。
2、在伊甸园空间执行第 1 次GC(Minor GC)之后,存活的对象被移动到其中一个幸存者空间(Survivor)。
3、此后每次 Minor GC,都会将 Eden 和 使用中的Survivor 区域中存活的对象,一次性复制到另一块空闲中的Survivor区,然后直接清理 Eden 和 使用过的那块Survivor 空间。
4、从以上空间分配我们知道,Survivor区内存占比很小,当空闲中的Survivor空间不够存放活下来的对象时,这些对象会通过分配担保机制直接进入老年代。
5、在以上步骤中重复N次(N = MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。
我们需要重点记住的是,新创建的对象,是保存在伊甸园空间的(Eden)。那些经历多次GC依然存活的对象会经由幸存者空间(Survivor)转存到老年代空间(Old generation)。
也有例外出现,对于一些大的对象(指需要占用大量连续内存空间的对象)则直接进入到老年代。
Java提供了 -XX:PretenureSizeThreshold 来指定对象大于这个值,直接分配到老年代。
老年代(Old generation)
对象在新生代周期中存活了下来的,会被拷贝到这里。通常情况下这个区域分配的空间要比新生代多。正是由于对象经历的GC次数越多越难回收,加上相对大的空间,发生在老年代的GC次数要比新生代少得多。这个区域触发的垃圾回收称之为:Major GC 或者 Full GC
老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间里绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor 空间中熬过来的,它们绝不会轻易狗带。因此,Major GC 或 Full GC 发生的次数不会有 Minor GC 那么频繁。
为什么老年代的回收耗时,比新生代更长呢?
有两点原因:
1、老年代内存占比更大,所以理论上回收的时间也更长
2、老年代使用的是标记-整理算法,清理完成内存后,还得把存活的对象重新排序整理成连续的空间,成本更高
方法区(Method area)
这个区域主要回收废弃的常量和类型,例如常量池里不会再被使用的各种符号引用等等。类型信息的回收相对来说就比较严苛了,必须符合以下3个条件才会被回收:
1、所有实例被回收
2、加载该类的ClassLoader 被回收
3、Class 对象无法通过任何途径访问(包括反射)
可以使用 -Xnoclassgc 禁用方法区的回收。
回收算法
标记 - 清除算法
这个算法和它的名字一样,分两个步骤:标记 和 清除。首先标记出所有存活的对象,再扫描整个空间中未被标记的对象直接回收。
并没有规定标记阶段一定要标记“存活”的对象,也可以标记“可回收”的对象
标记“存活”的,还是标记“可回收”的,网上各种说法都有,我个人理解,是标记存活的。这样效率高些。
首先,Java使用的是可达性分析算法来判断对象是否存活,上面有详细说这个算法,这里就不重复了。
我们假设要标记“可回收”的对象,再进行清除,那么需要三个步骤:
1、先通过可达性分析法,通过根对象(GC Roots)顺着引用链先把这些存活对象都标出来
2、遍历这个区域所有对象,把没标记存活的对象,打上一个“可回收”的标记
3、遍历这个区域所有对象,把标记了“可回收”的对象,释放掉。
但标记的是“存活”的对象,再进行清除,只需要两个步骤即可:
1、先通过可达性分析法,通过根对象(GC Roots)顺着引用链先把这些存活对象都标出来
2、遍历这个区域所有对象,把没标记存活的对象,直接清理掉即可。
所以,标记“可回收”的对象,会多了一次完全没有必要的遍历。这也是我不认同标记“可回收”的原因。
标记 - 清除算法由于回收后没有进行整理的操作,所以会存在内存空间碎片化的问题,这个确实是缺点,但也是这个算法的特点,正因为它不进行整理,所以效率才高。
标记 - 复制算法
常规的复制算法,是把内存分成两块大小相同的空间(1 : 1),每次只使用其中一块,当使用中的这块内存用完了,就把存活的对象移动到另一块内存中,再把使用过的这块内存空间一次性清理掉。这个做法虽然效率极高,但也浪费了一半的内存空间。
标记-复制算法,在这个基础之上对其进行了优化,IBM曾有过一项针对新生代的研究,结论是绝大多数情况下,新生代区域里的对象有98%都熬不过第一次回收。
所以不需要按照 1 : 1 的比例来实现复制算法,而是可以按照 8 : 1 : 1 的比例来分配内存空间,也就是一个80%的Eden空间和两个10%的Survivor空间。
为什么要两块Survivor空间?
因为复制算法,必须要有一块空间是空闲的。想象一下,如果只有一块Eden空间 + 一块Survivor空间
当GC回收完成后,Eden中存活的对象会移动到Survivor空间。程序继续运行,新的对象又会进入Eden空间,此时就会出现 Eden 和 Survivor 空间里都有对象,复制算法也就进行不下去了。
每次分配内存,只使用Eden和其中一块Survivor空间,发生GC回收时,把Eden和其中一块Survivor空间中存活的对象,复制到另一块空闲的Survivor空间,然后直接把Eden和使用过的那块Survivor空间清理掉。
目前主流的使用分代回收机制的Java虚拟机,都是使用标记-复制算法来作为新生代的回收算法。它非常适合用在新生代这种回收率极高的场景,这样的场景下,复制算法浪费的空间几乎可以忽略不计。效率高,且内存不会有碎片化的问题。但对于老年代这种存活率很高的场景,就不适合了。
标记-复制算法还有一个非常重要的知识点,就是分配担保机制,虽然根据IBM的研究,每次GC新生代里98%的对象都会被回收,但这不是百分之百的几率,极端情况下可能会出现超过10%的对象存活。分配担保机制就是为了保证当出现这种情况时,有其他内存空间来进行兜底。通常这个“担保人”是老年代,当存活的对象超过Survivor空间大小时,这些存活的对象会忽略年龄,直接进入老年代里。
标记 - 整理算法
标记-清除算法会产生内存碎片,不适合哪些需要大量连续内存空间的场景,而标记-整理算法,就是在其基础之上,增加了整理这个操作,去解决这些内存空间碎片化的问题,如下图所示:
和标记-清除算法一样,先标记,但清除之前,会先进行整理,把所有存活的对象往内存空间的左边移动,然后清理掉存活对象边界以外的内存,即完成了清除的操作。标记-整理 算法是在 标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
老年代里的对象存活率很高,不适合使用标记-复制的算法。而且老年代存储大对象的概率要比新生代大很多,这些大对象需要连续的内存空间来存储,标记-清除这个算法也不适合。所以大多数的老年代都采用标记-整理来作为这个区域的回收算法。
Q & A
为什么老年代不使用和新生代一样的标记-复制算法呢?
新生代里绝大多数对象都是朝生夕死的,使用的标记-复制算法,空间占比可以8 : 1 : 1,但是老年代里对象存活率很高,这个占比明显不合适。如果占比得设置得大 (例如50%),又会浪费很多内存空间,而且由于对象很多都是存活的,复制移动也是一笔开销。所以标记-复制这个算法,不适合老年代这种对象存活率很高的区域。
垃圾收集器🥢
就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC,下面我们来看看这两大类的垃圾收集器。
分代收集器
CMS
以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除,详情可见 JEP 363open in new window。
CMS(Concurrent Mark Sweep)垃圾收集器是第一个关注 GC 停顿时间(STW 的时间)的垃圾收集器。之前的垃圾收集器,要么是串行的垃圾回收方式,要么只关注系统吞吐量。
CMS 垃圾收集器之所以能够实现对 GC 停顿时间的控制,其本质来源于对「可达性分析算法」的改进,即三色标记算法。在 CMS 出现之前,无论是 Serious 垃圾收集器,还是 ParNew 垃圾收集器,以及 Parallel Scavenge 垃圾收集器,它们在进行垃圾回收的时候都需要 Stop the World,无法实现垃圾回收线程与用户线程的并发执行。
CMS 垃圾收集器通过三色标记算法,实现了垃圾回收线程与用户线程的并发执行,从而极大地降低了系统响应时间,提高了强交互应用程序的体验。它的运行过程分为 4 个步骤,包括:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记,指的是寻找所有被 GCRoots 引用的对象,该阶段需要「Stop the World」。这个步骤仅仅只是标记一下 GC Roots 能直接关联到的对象,并不需要做整个引用的扫描,因此速度很快。
并发标记,指的是对「初始标记阶段」标记的对象进行整个引用链的扫描,该阶段不需要「Stop the World」。 对整个引用链做扫描需要花费非常多的时间,因此通过垃圾回收线程与用户线程并发执行,可以降低垃圾回收的时间。
这也是 CMS 能极大降低 GC 停顿时间的核心原因,但这也带来了一些问题,即:并发标记的时候,引用可能发生变化,因此可能发生漏标(本应该回收的垃圾没有被回收)和多标(本不应该回收的垃圾被回收)了。
重新标记,指的是对「并发标记」阶段出现的问题进行校正,该阶段需要「Stop the World」。正如并发标记阶段说到的,由于垃圾回收算法和用户线程并发执行,虽然能降低响应时间,但是会发生漏标和多标的问题。所以对于 CMS 来说,它需要在这个阶段做一些校验,解决并发标记阶段发生的问题。
并发清除,指的是将标记为垃圾的对象进行清除,该阶段不需要「Stop the World」。 在这个阶段,垃圾回收线程与用户线程可以并发执行,因此并不影响用户的响应时间。
CMS 的优点是:并发收集、低停顿。但缺点也很明显:
①、对 CPU 资源非常敏感,因此在 CPU 资源紧张的情况下,CMS 的性能会大打折扣。
默认情况下,CMS 启用的垃圾回收线程数是(CPU数量 + 3)/4,当 CPU 数量很大时,启用的垃圾回收线程数占比就越小。但如果 CPU 数量很小,例如只有 2 个 CPU,垃圾回收线程占用就达到了 50%,这极大地降低系统的吞吐量,无法接受。
②、CMS 采用的是「标记-清除」算法,会产生大量的内存碎片,导致空间不连续,当出现大对象无法找到连续的内存空间时,就会触发一次 Full GC,这会导致系统的停顿时间变长。
③、CMS 无法处理浮动垃圾,当 CMS 在进行垃圾回收的时候,应用程序还在不断地产生垃圾,这些垃圾会在 CMS 垃圾回收结束之后产生,这些垃圾就是浮动垃圾,CMS 无法处理这些浮动垃圾,只能在下一次 GC 时清理掉。
分区收集器
G1
G1(Garbage-First Garbage Collector)在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性:分代、增量、并行、标记整理、STW。
①、分代:相信大家还记得我们上一讲中的年轻代和老年代open in new window,G1 也是基于这个思想进行设计的。它将堆内存分为多个大小相等的区域(Region),每个区域都可以是 Eden 区、Survivor 区或者 Old 区。
可以通过 -XX:G1HeapRegionSize=n 来设置 Region 的大小,可以设定为 1M、2M、4M、8M、16M、32M(不能超过)。
G1 有专门分配大对象的 Region 叫 Humongous 区,而不是让大对象直接进入老年代的 Region 中。在 G1 中,大对象的判定规则就是一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中,而且一个大对象如果太大,可能会横跨多个 Region 来存放。
G1 会根据各个区域的垃圾回收情况来决定下一次垃圾回收的区域,这样就避免了对整个堆内存进行垃圾回收,从而降低了垃圾回收的时间。
②、增量:G1 可以以增量方式执行垃圾回收,这意味着它不需要一次性回收整个堆空间,而是可以逐步、增量地清理。有助于控制停顿时间,尤其是在处理大型堆时。
③、并行:G1 垃圾回收器可以并行回收垃圾,这意味着它可以利用多个 CPU 来加速垃圾回收的速度,这一特性在年轻代的垃圾回收(Minor GC)中特别明显,因为年轻代的回收通常涉及较多的对象和较高的回收速率。
④、标记整理:在进行老年代的垃圾回收时,G1 使用标记-整理算法。这个过程分为两个阶段:标记存活的对象和整理(压缩)堆空间。通过整理,G1 能够避免内存碎片化,提高内存利用率。
年轻代的垃圾回收(Minor GC)使用复制算法,因为年轻代的对象通常是朝生夕死的。
⑤、STW:G1 也是基于「标记-清除」算法,因此在进行垃圾回收的时候,仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
G1 中存在三种 GC 模式,分别是 Young GC、Mixed GC 和 Full GC。
当 Eden 区的内存空间无法支持新对象的内存分配时,G1 会触发 Young GC。
当需要分配对象到 Humongous 区域或者堆内存的空间占比超过 -XX:G1HeapWastePercent 设置的 InitiatingHeapOccupancyPercent 值时,G1 会触发一次 concurrent marking,它的作用就是计算老年代中有多少空间需要被回收,当发现垃圾的占比达到 -XX:G1HeapWastePercent 中所设置的 G1HeapWastePercent 比例时,在下次 Young GC 后会触发一次 Mixed GC。
Mixed GC 是指回收年轻代的 Region 以及一部分老年代中的 Region。Mixed GC 和 Young GC 一样,采用的也是复制算法。
在 Mixed GC 过程中,如果发现老年代空间还是不足,此时如果 G1HeapWastePercent 设定过低,可能引发 Full GC。-XX:G1HeapWastePercent 默认是 5,意味着只有 5% 的堆是“浪费”的。如果浪费的堆的百分比大于 G1HeapWastePercent,则运行 Full GC。
在以 Region 为最小管理单元以及所采用的 GC 模式的基础上,G1 建立了停顿预测模型,即 Pause Prediction Model 。这也是 G1 非常被人所称道的特性。
我们可以借助 -XX:MaxGCPauseMillis 来设置期望的停顿时间(默认 200ms),G1 会根据这个值来计算出一个合理的 Young GC 的回收时间,然后根据这个时间来制定 Young GC 的回收计划。
类加载流程🥢
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
其中使用流程画出来可以像下面这样:
加载 -> 连接/链接 -> 初始化
连接/链接:其中当前步骤包含(验证->准备->解析)
加载
类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
类加载规则
对于 Java 来说,每一个 Java 类都有一个引用指向加载它的 ClassLoader
。
(数组类不是通过 ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的)
然后在但是并不是在启动的时候就把所有的类全部加载进来,它有一个机制就是如果加载过了,就不会再加载。对于一个类加载器来说,相同二进制名称的类只会被加载一次。
JVM 的类加载器
JVM 中内置了三个重要的 ClassLoader
:
**BootstrapClassLoader**
**(启动类加载器)**:最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib
目录下的rt.jar
、resources.jar
、charsets.jar
等 jar 包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。**ExtensionClassLoader**
**(扩展类加载器)**:主要负责加载%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。**AppClassLoader**
**(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
双亲委派模型以及如何打破🥢
流程
在前面我们说道:对于一个类加载器来说,相同的二进制名称只会被加载一次。
其实这个就是双亲委派模型带来的结果,对于我们的类来说,我们要被加载,那么它会先寻找类的父类加载器,然后父类加载还会往上层找,直到找到顶层父加载器。
- 如果在顶层的加载器已经加载过了,那么则会直接返回该对象
- 如果没有找到则原路一层一层的返回,然后一层一层的询问该层的类加载器是否加载过该类,有则返回
- 一直重复上述操作,最后如果走到自己这个类了没有找到,那么它会抛出一个
ClassNotFoundException
异常。
好处:可以避免重复加载类。
打破双亲委派模型
我们可以继承 ClassLoader
类,然后如果是要打破的话,我们可以重写 loadClass()
。
如果我们是只需要自定义类加载器的话,仅仅需要重写 findClass()
即可。
连接/链接
在该类已经被类加载后,此时就是执行校验的操作时候
- 验证
文件格式验证(Class 文件格式检查)
元数据验证(字节码语义检查)
字节码验证(程序语义检查)
符号引用验证(类的正确性检查)
- 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
- 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
初始化
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
- 当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条字节码指令时,比如new
一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 - 当 jvm 执行
new
指令时会初始化类。即当程序创建一个类的实例对象。 - 当 jvm 执行
getstatic
指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 - 当 jvm 执行
putstatic
指令时会初始化类。即程序给类的静态变量赋值。 - 当 jvm 执行
invokestatic
指令时会初始化类。即程序调用类的静态方法。 - 使用
java.lang.reflect
包的方法对类进行反射调用时如Class.forName("...")
,newInstance()
等等。如果类没初始化,需要触发其初始化。 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main
方法的那个类),虚拟机会先初始化这个类。 MethodHandle
和VarHandle
可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
就必须先使用findStaticVarHandle
来初始化要调用的类。- 「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化