JVM详解
JVM和JRE和JDK的区别
JVM的体系结构
java编译器输入的指令流是一种基于栈的指令集架构,特点是跨平台性、指令多、指令集小,但是执行性能比基于寄存器的指令集架构要低
虚拟机分类
- classic VM:只有解释器,没有即时编译器
- Exact VM:jdk1.2提出,虚拟机可以知道某个位置的数据是什么类型,并且和即时编译器可以混合使用
- Hotspot VM:沿用至今,主要特点是热点代码探测技术
- JRocket VM:全靠即时编译器,专注于服务器端的应用,也是最快的JVM
- J9:定位和hotspot相似
一. 程序计数器
用来记住下一条JVM指令执行的地址,执行完某一条指令后,程序计数器中存储了下一条该执行的指令的位置。物理上程序计数器通过寄存器来实现,寄存器是读取速度最快的单元。
特点:线程私有、不会存在内存溢出(内存区唯一不会溢出的区)
二. 虚拟机栈
栈中只保存基础数据类型的对象和自定义对象的引用。即线程运行时所需要的内存空间。当某个方法执行时,会向当前线程所在的栈压入栈帧,栈帧是每个方法运行时所需的内存,当方法执行完毕后,栈帧出栈。因此不需要垃圾回收。
栈帧中包含:
- 局部变量表:是一个数字数组,一个存储单元成为slot,32位以内的类型占一个slot,64位的类型占两个slot,主要存储方法的参数和定义在方法体内的局部变量,大小是在编译期确定下来的。
- 操作数栈(或表达式栈):数组实现,主要用来保存计算结果的中间过程
- 动态链接(或指向运行时常量池中方法的引用):即指明调用的方法
- 方法返回地址:调用者PC寄存器作为返回地址
- 一些附加信息
活动栈帧:在栈顶部正在执行的方法叫活动栈帧
特点:
- 不需要垃圾回收
- 栈内存的划分不是越大越好,因为总内存是一定的,单个线程所占的内存越大,总线程数越少。栈的大小分配可以通过
-Xss1024k
的形式设置。 - 如果方法内的局部变量没有逃离方法的作用范围,比如方法内定义的变量,则是线程安全的;如果方法内的局部变量逃离了方法的作用范围,比如将变量返回、将变量以参数传入,则是线程不安全的。
栈内存溢出:
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
线程运行诊断:
案例一:CPU占用过高
- 使用命令:
top
定位哪个进行对CPU占用高 - 使用命令:
ps H -eo pid,tid,%cpu |grep 进程id
来定位由哪个线程引起的CPU占用过高 - 使用命令:
jstack pid
查找对应的进程中的线程,显示出的线程编号为16进制,因此需要将pid转换为对应的16进制,然后去找
案例二:程序运行很长时间没有结果,一般是由死锁
三. 本地方法栈
本地方法所使用的内存就叫本地方法栈,本地方法指的是直接与硬件打交道的方法,可能是由C语言或者C++编写的。本地方法栈也是线程私有。HtoSpot虚拟机将虚拟机栈和本地方法栈合并。
四. 堆
分为新生代和老年代。新生代包括伊甸园区、幸存区0(from区)幸存区1(to区)。老年代比新生代的大小分配默认为2:1,伊甸园区、from区、to区默认大小比例为8:1:1
存储的是通过new关键字创建的对象,这些对象都会使用堆内存。
特点:
- 一个JVM实例只有一个堆区,线程共享的,需要考虑线程安全问题
- 有垃圾回收机制
- 堆空间的分配可以通过
-Xmx1g
来分配堆空间的最大内存,-Xms1g
来分配堆空间的起始内存
堆内存问题诊断:
jps
命令查看当前系统中的java进程jmap -heap 进程id
命令查看堆内存占用情况jconsole
命令,图形界面的多功能检测工具,可以连续监测
案例一:垃圾回收后内存占用还是很高
堆空间的参数设置
- -XX:+PrintFlagsInitial:查看所有参数的默认初始值
- -XX:+PrintFlagsFinal:查看所有参数的默认值
- -Xms:初始堆空间内存
- -Xmx:最大堆空间内存
- -Xmn:设置新生代的大小
- -XX:NewRatio:配置新生代与老年代在堆结构占比
- -XX:SurvivorRatio:设置新生代中伊甸园区和幸存区的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails:输出详细的GC处理日志
- -XX:HandlePromotionFalure:是否设置空间分配担保
对象的内存布局
- 对象头
- 运行时元数据:包含哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
- 类型指针:指向元空间中对象所属的类型
- 如果是数据对象还有数组的长度
- 实例数据:包括程序代码中定义的各种类型的字段,也包括父类的
代码优化:
- 标量替换(分离对象)
有的对象不需要作为一个连续的内存结构被访问,那么对象的部分可以不存储在堆,而是存在栈帧中。(但对象仍然被看作是放在堆中的,只是部分在栈中) - 栈上分配
没有发生逃逸的对象可以分配到栈上。选项-XX:+DoEscapeAnalysis
显式开启逃逸分析,选项-XX:+PrintEscapeAnalysis
查看逃逸分析的筛选结果,HotSpot虚拟机并没有实现 - 同步省略
也叫锁消除,如果一个对象只会被一个线程访问,那么可以不对其进行同步
五. 方法区
又叫静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,如类信息,运行时常量池、static变量、即时编译器编译后产生的代码缓存。在虚拟机启动时被创建。jdk1.8中方法区的永久代被元空间替代。方法区的垃圾收集主要包括常量池中废弃的常量和不再使用的类型。
参数设置
- jdk7以前
- -XX:PermSize=20.75m来设置永久代初始分配空间,默认为20.75M
- -XX:MaxPermSize=82m来设置永久代最大可分配空间。32位机器默认位64M,64位机器默认为82M
- jdk8以后
- -XX:MetaspaceSize=21M来设置永久代初始分配空间,默认为21M,建议设置大一点减少full GC
- -XX:MaxMetaspaceSize来设置永久代最大可分配空间。默认为-1,即没有限制
运行时常量池
属于方法区一部分,常量池就是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
常量池是*.class文件中的,当该类被加载,他的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。
类的垃圾回收
如何判断一个类可以回收
- 该类的所有实例都被回收
- 加载该类的类加载器被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
六. 字符串常量池StringTable
StringTable是用哈希表实现的,jdk6的默认的长度为1009,jdk7默认大小为60013,jdk1.8开始,最小长度可设置为1009.
jdk1.6中StringTable的位置
jdk1.8中StringTable的位置
为什么放入堆中?
- 1.7放到堆中,因为永久代(方法区)默认很小,如果大量的字符串常量在里面,那么会有内存溢出。
- 方法区的垃圾回收频率低
静态创建的常量被放入字符串常量池,动态创建的常量被放入堆中。
特性:
- 常量池中的字符串仅是符号,第一次用到才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中没有的字符串放入串池,如果串池中已存在该字符,则不会放入,若不存在jdk1.7会将该字符对象的引用复制一份放入串池,并返回地址;jdk1.6会拷贝一份字符对象然后放入串池,放入后都会将串池中的字符地址返回。
- StringTable中的字符达到一定的容量后会自动触发GC,释放一些没有变量引用的字符
StringTable调优:StringTable的本质是哈希表,每次放入字符时都会去查找哈希表中是否已存在该字符。因此调优的思路就是如何让查找的速度更快,那么就是让哈希表的长度越大越好(让桶的个数越多越好),可以通过JVM参数:XX:StringTableSize=桶个数
来配置。
七. 直接内存
操作系统划出的内存,java代码可以直接访问,系统也可以直接访问。直接内存也会有内存溢出的问题
特点:
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本高,但读写性能高
- 不受JVM内存回收管理,由Unsafe类实现直接内存的释放,会调用其中的freeMemory方法。ByteBuffer的实现类内部使用Cleanner来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemery来释放内存
八. 执行引擎
解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的机器指令
JIT即时编译器:虚拟机将源代码直接编译成本地机器平台相关的机器语言。分类如下:
- client:指定java虚拟机运行在客户端模式下,并使用C1编译器(特点是对字节码进行简单优化,耗时短,以达更快的编译速度)
- server:64位电脑默认模式,指定java虚拟机运行在server模式下,并使用C2编译器(C2进行耗时较长的优化,以及激进优化,效率更高)
当方法计数器和回边计数器都到达一定的次数后触发即时编译。
相关参数设置:
- Xint:完全采用解释器模式运行程序
- Xcomp:完全采用即时编译器模式执行程序。如果即时编译器出现问题解释器会介入执行
- Xmixed:采用解释器+即时编译器的混合模式共同执行程序
垃圾回收
收集分类
- minorGC(Yonug GC):针对新生代的回收
- majorGC(Old GC):针对老年代的回收,目前只有CMS收集器会有这种行为
- mixedGC:回收新生代全部和老年代部分,G1收集器会有这种行为
- fullGC:整个java堆和方法去的垃圾收集
如何判断对象可以回收
-
引用计数法:当引用变为0时对该对象进行回收
-
可达性分析算法:JAVA虚拟机默认采取可达性分析算法来探索存活的对象。扫描堆中的对象,看是否能沿着GC root为起点的引用链找到该对象,若找不到,则可以回收。若找到则不能回收。GC root通常包含以下几类元素:
- 虚拟机栈、本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区字符串常量池中的引用
- 所有被同步锁synchronized持有的对象
- JAVA虚拟机内部的引用,比如Class对象、常驻的异常对象、系统类加载器
gc()的理解
System.gc()和Runtime.getRuntime().gc();的调用会显式的触发FullGC。然而System.gc()附带免责声明,无法保障调用垃圾收集器。
安全点与安全区域
应用程序并不是在任何时刻都能进行垃圾回收,必须当所有用户线程走到安全的时间点才能进行垃圾回收。但是有的用户线程在执行过程中可能进入了Sleep或Blocked状态,这时候线程无法响应JVM走到安全点挂起的中断请求,这就需要安全区域。
安全区域是指一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置GC都是安全的。
内存溢出和内存泄漏
内存溢出即OOM,表示没有空闲内存,并且垃圾收集器也无法提供更多内存。
内存泄漏指的是对象不再被程序使用了,但GC又不能回收它们,就叫内存泄漏。数据库链接、socket链接、io用完必须关闭,否则会产生内存泄漏。
不主动调用finalize方法的原因
finailize只会执行一次,并且最好由垃圾收集器自动调,有以下原因:
- 可能会导致对象复活
- 该方法的执行时间没有保障,完全由GC线程决定
- 一个糟糕的finalize方法会严重影响GC性能
由于finalize方法的存在,虚拟机中对象一般处于三种可能的状态
- 可触及的:从根节点开始可以达到该对象
- 可复活的:对象所有引用都被释放,但对象可能在finalize中复活
- 不可触及的:对象的finalize被调用并且没有复活
垃圾回收算法
- 标记清除,将被引用对象进行标记,然后在对象头中记录为可达对象。然后在清除阶段将未标记的对象进行清除,所谓清楚是将其地址放到空闲列表,下次新对象直接覆盖。缺点是效率不算高,会STW,并且造成空间不连续
- 标记整理,将垃圾对象进行标记,在清理的过程中,会对内存地址进行整理,让内存地址更紧凑。优点是没有内存碎片,缺点是整理时间耗时长。老年代采用该算法。
- 复制算法,将内存分为两块,每次使用一块。对正在使用区域的存活对象进行标记并复制到另一个区域,然后将正在使用内存区域的对象全部清除。该算法用于幸存者0区到幸存者1区的复制。优点是运行高效、没有内存碎片。缺点是需要两块内存
分代垃圾回收
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
- 新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
- 老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用标记清除或者标记整理算法回收。
当创建一个新的对象时,默认会被放到新生代的伊甸园区,当伊甸园区内存满了时会触发Minor GC,此时其他用户的线程都会停止。具体的过程是将伊甸园区和幸存区FROM中存活的对象进行标记,并将其放入到幸存区TO中,年龄+1,然后清除伊甸园区和幸存区FROM中无效的对象并交换FROM和TO区域(如果minor GC后伊甸园区仍然放不下,那么会将对象直接放到老年代;或者幸存者区年龄相同的对象大于幸存者区的一半,那么当大于等于该年龄时会直接进入老年代)。当幸存区FROM某个对象的寿命达到15(默认最大为15)之后就一定会被放入老年代中。当老年代空间不足,会对新生代垃圾进行Minor GC,回收过后内存仍然不足时,会触发一次full GC,即先对新生对回收,然后再对老年代进行垃圾回收。如果老年代仍然空间不足,那么会触发out of memory(OOM),但是一个线程的OOM不会导致主线程也停止。
大对象回收:当对象的所占用的内存大于新生代的内存时,那么不会触发GC,该对象会直接进入老年代
以下是一些相关的VM参数
线程局部缓存TLAB
堆区是线程共享的区域,但是TLAB是JVM为每个线程在伊甸园区分配的私有缓存区域,该位置默认占伊甸园区的1%。在为对象分配内存时会首先考虑该位置,该位置不够时才会用加锁机制再分配到伊甸园区的其他位置。
增量回收算法
垃圾收集线程不再长时间STW,每次只收集一小块内存,然后和用户线程交替执行。缺点是因为经常性的切换线程,因此总的垃圾收集时间会很长
分区算法
分区算法主要针对G1收集器,它将整个堆分为一个个小的区间(region)。每次合理的回收若干个小区间,从而减少GC的停顿。
五种引用
- 强引用:当某个对象只被强引用指向时,当所有的强引用都不存在时,该对象才会被回收
- 软引用:当与对象的连接只剩软引用时,若执行了垃圾回收并且还是内存不足,那么会回收该对象。高速缓存使用软引用。
- 弱引用:当与对象的连接只剩弱引用时,若执行了垃圾回收则会直接回收该对象
无论是软引用还是弱引用,在回收了对象之后,这俩对象本身可以选择进入引用队列等待被释放
- 虚引用:当没有强引用对象时,那么该对象会直接被回收,但是还有直接内存未被释放,那么此时虚引用对象会被放入引用队列,从而间接的用一个线程调用虚引用的方法,释放直接内存。虚引用目的是被收集器回收时收到一个系统通知。
- 终结器引用:当没有强引用对象引用时,那么终结期引用也会进入引用队列,会由一个优先级很低的线程发现该终结期引用,然后找到连接的对象,并调用该对象的finallize方法实现垃圾回收
评估GC的性能指标
- 吞吐量:运行用户代码的时间比总时间(用户代码+垃圾收集)
- 暂停时间:执行垃圾收集时,程序的工作线程被占用的时间
- 内存占用:Java堆区所占内存的大小
吞吐量和暂停时间相互矛盾,一方变优,另一方必然变得不好。追求的是在可控的暂停时间内,尽量提高吞吐量。
垃圾回收器
串行
涉及到的垃圾回收器:
- Serial 收集器:这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。用来回收新生代,采用复制算法。
- Serial Old 收集器:串行收集器的老年代版本,单线程,使用标记整理算法。
意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
开启串行回收器的JVM参数:-XX:+UseSerialGC=Serial+OldSerial
,其中新生代的垃圾回收器采用复制算法,老年代采用标记整理算法.
吞吐量优先
涉及到的垃圾回收器:
- Parallel Scavenge 收集器:并行回收器,指多条垃圾收集线程并行工作,此时用户线程处于等待状态。用来回收新生代
- Parallel Old 收集器:并行回收器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用标记整理算法回收老年代
1.8默认使用的回收器,多线程垃圾回收器,适合堆内存较大的场景,适合多核CPU。该回收器尽可能让STW最短。
开启回收器的JVM参数:-XX:+UseParallelGC~-XX:+UseParallelOldGC
,回收算法和串行垃圾回收器是一样的。几核CPU就会开启几个回收线程(可以调整)
响应时间优先
涉及到的垃圾回收器:
- ParNew 收集器:并行回收器,可以认为是 Serial 收集器的多线程版本。回收新生代,采用复制算法。
- CMS 收集器:并发回收器,CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记清除算法实现。回收老年代
多线程垃圾回收器,适合堆内存较大的场景,适合多核CPU。尽可能让单次暂停时间(STW)最短。
运作步骤:
- 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
- 并发标记(CMS concurrent mark):进行 GC Roots Tracing,继续沿着直接关联到的对象查找能延申到的对象
- 重新标记(CMS remark):修正并发标记期间的变动部分
- 并发清除(CMS concurrent sweep)
开启回收器的JVM参数:-XX:+UseConcMarkSweepGC~ -XX:+UseParNewGC ~ SerialOld
,表示当老年代碎片过多导致内存不足时,老年代的回收器会由CMS变为SerialOld,导致响应时间变多。
CMS弊端:
- 会产生内存碎片,并发清除后,用户线程可用空间不足,在无法分配大对象的前提下不得不触发Full GC
- CMS收集器对CPU资源非常敏感,并发阶段虽然不会导致,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。
- CMS无法处理浮动垃圾,在并发清理环节如果产生新的垃圾则无法进行回收
G1 收集器
-
面向服务端的垃圾回收器,同时注重吞吐量和低延迟,在能容忍的响应时间下追求最大的吞吐量。默认的暂停目标是30秒,超大的堆内存,会将堆划分为多个Region,会优先回收那些垃圾比较大的Region。整体上是标记整理算法,Region和Region之间是复制算法。
-
优点:并行与并发、分代收集、空间整合、可预测停顿。
-
G1垃圾回收三个阶段
-
新生代的垃圾回收(E:伊甸园区,S:幸存区,O:老年代),年轻代的收集阶段是一个并行的独占式收集器,回收期间会暂停所有应用程序线程。
-
新生代回收加并发标记
新生代回收时会进行初始标记,当老年区的内存达到总堆空间的一定的阈值时(默认45%),会触发并发标记
-
混合回收
该阶段会进行全面的垃圾回收,最终标记和拷贝存活(这俩都会发生STW),老年代进行拷贝存活时会将垃圾最多的老年代进行拷贝
当老年代回收速度大于对象产生的速度时,会进行并发标记加混合回收的阶段,如果低于产生对象的速度时,会进行串行收集,即此时用户线程不能执行。会发生full GC
- 新生代的跨代引用
因为新生代有的根对象是来自于老年代的,那么将老年代做成卡表,当某个区域引用了新生代的对象时,就将其标为脏卡。那么新生代中的伊甸园区就会将所有脏卡的位置保存起来,下次新生代在进行回收操作时,找到根对象的速度就会大大加快。 - 字符串去重:字符串的实现是基于char数组的,新生代回收时,G1并发检查是否有字符串重复,有的话会让他们指向同一个char数组。优点是节省内存,缺点是会略微占用新生代的垃圾回收时间
- 类卸载:所以对象都经过并发标记后就知道哪些类(一般是自定义类加载器)不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
- 回收巨型对象:一个对象的大小大于Region的一半时就叫巨型对象,G1不会对巨型对象进行拷贝,并且优先回收巨型对象。当某个巨型对象在老年代的引用为0时,那么在新生代回收时就将其回收。
Full GC介绍
Serial GC和Parallel GC在老年代内存不足时发生的GC都叫full GC,但是CMS和G1只有在并发失败即回收速度比产生垃圾的速度慢时才会叫full GC
垃圾回收调优
调优目标
科学运算:追求高吞吐浪,延长一点响应时间并不影响,比如ParalellGC
互联网项目:追求响应时间,给用户造成好的体验,比如CMS,G1,ZGC
尽量不发生GC
- 查看数据是否太多
- 查看数据的表示是否太臃肿
- 查看是否存在内存泄露,尽量考虑第三方缓存
新生代的调优
新生代的特点:
- 所有new操作的内存分配非常低廉
- 死亡对象的回收代价是0
- 大部分对象用过即死
- Minor GC 时间远低于Full GC
理论上说新生代的内存越大,吞吐量会越高,但是新生代的变大会使得老年代的空间变小,那么更容易触发Full GC,新生代的内存官方建议是在25%到50%,但是如果仔细分析,将其保持在新生代所能容纳【并发量*(请求-响应)】
的数据
让幸存区长时间存活的对象尽快的晋升到老年代(可以调整晋升阈值),让短暂存在的对象留在暂存区,这样可以减少新生代对象复制的时间
老年代的调优
CMS的老年代内存越大越好,减少full GC。一般老年的空间达到75%以上时进行full GC
案例分析
- MInor GC和full GC发生频繁
答:新生代的内存设置太小,导致新生代频繁,使得一些对象更快的晋升到老年代,因此老年代的内存也快速变大,使得full GC更加频繁 - 请求高峰期发生full GC,单次暂停时间特长(CMS)
答:因为CMS回收器在重新标记的时间比较长,不但要扫描老年代的对象,还要扫描新生代的对象,因此调优就是在重新标记之前先对新生代做一次垃圾回收 - 老年代内存充裕的情况下发生了full GC(CMS,jdk1.7)
答:永久代的空间太小导致的full GC,将其设置的大一点
类加载和字节码技术
java类文件结构如下:
- 魔数
- 版本
- 常量池
- 访问标识与继承信息
- Field信息
- Method信息
- 附加属性
语法糖
- 默认构造器:若本类没写构造方法,那么会自动生成无参的默认构造器,并调用父类的构造方法
- 自动拆装箱:比如会将
Integer i=0
自动转化为Integer i=Integer.valueOf(i)
,int j=i
自动转化为intj=i.intValue()
- 泛型擦除:编译泛型代码后会执行泛型擦除,集合中的对象实际都被当做了Object对象,那么在获取集合中对象的时候自动为我们加了强制类型转换
- 可变参数:会将可变参数变为一个数组对象
- foreach循环:默认转化为Iterator方式的迭代
- switch中的String:会将String转化为hashcode,然后进行比对,内部会进行两次switch对比,第一次对比hashcode时也会调用equals方法比较字符串,并且赋值给变量,第二次switch变量,选择要执行的语句
- switch中的enum:会在类中创建一个内部类,然后在该内部类中创建一个map,将枚举类中的元素的ordinary作为键,数字作为值,然后在switch中则选择元素的ordinary来判断要执行的语句
- try-with-resource:即在try后面加括号,里面放着要关闭的资源(该资源必须要实现AutoCloseable接口),这样就不用在finally中再进行释放
类加载阶段
分为三个阶段:加载、链接、初始化
- 加载:将类的字节码载入方法区中,如果这个类的父类还没有被加载,那么就先加载父类,加载和链接会交替运行
- 链接:分为验证、准备、解析三个阶段
- 验证:验证类是否符合JVM规范,安全性检查,主要对文件格式、元数据、字节码、符号引用验证
- 准备:为static变量分配空间设置默认值(jdk1.7之后静态变量存储于堆中的mirrorClass中),在准备阶段为static变量分配内存空间,并且为其赋默认值(整型为0,boolean为false,引用为null,但是final修饰的变量是在编译期间就已经赋值了),后面的初始化阶段为static变量赋真实值。
- 解析:将常量池中的符号引用解析为直接引用
- 初始化:实际就是去执行类构造器方法clinit的过程,该方法不同于构造方法,
- 初始化的情况:main方法所在的类,总会被优先初始化;首次访问该类的静态变量时,会发生初始化;子类初始化,如果父类还没初始化会引发;子类访问父类的静态变量会引发父类的初始化;Class.forName会引起初始化;new会导致初始化
- 不会初始化的情况:访问类的static final类型不会导致初始化,因为该变量在编译时就初始化了;类对象.class不会触发初始化;创建该类的数组不会触发初始化;类加载器的loadClass不会触发;Class.forName的第二个参数为false是不触发
类加载器
- Bootstrap类加载器:加载JAVA_HOME/jre/lib下面的类,C++编写,无法直接访问
- Extension类加载器:加载JAVA_HOME/jre/lib/ext下面的类,上级为Bootstrap类加载器
- Application 类加载器:加载classpath下面的类,上级为Extension加载器
- 自定义类加载器:加载自定义
双亲委派模式:
调用类的加载器时,他有一个规则,当加载某个类时,会先询问其上级加载器是否已经加载,若上级未加载则会继续访问上级的上级,知道其所有上级加载器都没加载时该加载器才可以加载该类。比如加载String类时,Application加载器会委派Extension类加载器,看其是否已经加载,若未加载,则会继续委派Bootstrap类加载器看起是否已经加载,String是在lib包下的,因此确定已经加载成功。双亲委派机制在一定情况下也会被打破,比如加载MySQL时,那么他会在启动类加载器中使用应用程序加载器加载驱动。
自定义类加载器的使用时机
- 想要加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦(框架常用)
- 这些类希望予以隔离(比如类的老版本也想接着用),那么可以同时加载,常见于tomcat
步骤:
- 继承ClassLoader父类
- 重写findClass方法(不是loadClass,否则不走双亲委派),读取类文件字节码,调用父类的define方法来加载类
- 使用者调用该类加载器的loadClass方法加载类
运行期优化
JVM将执行状态分为5个层次:
- 0层,解释执行,即字节码被加载到虚拟机后,由解释器将字节码解释为机器码,这样机器才可以识别
- 1层,使用C1即时编译器编译执行(不带profiling),当字节码被反复调用,那么会启用C1即时编译期,将反复执行的代码编译成机器码存储起来,那么下次再执行相同的代码时,就不会再将字节码解释,会直接拿存储的机器码使用。
- 2层,使用C1即时编译器编译执行(带基本的profiling),profiling是指运行过程中收集一些程序执行状态的数据,比如方法的调用次数、循环的回编次数等。
- 3层,使用C1即时编译器编译执行(带完全的profiling)
- 4层,使用C2及时编译器编译执行,相较于C1做了更彻底的优化
即时编译器(JIT)和解释器的区别:解释器会将字节码解释为所有平台都通用的机器码,JIT会生成平台特定的机器码