JVM笔记
为什么Java可以跨平台
Java源码首先会被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
为什么JVM不直接将源码解析成机器码去执行
- 准备工作:每次执行都要进行各种语法、语义检查,不能保存下来,性能会受到影响
- 兼容性:也可以将别的语言解析成字节码
JVM如何加载.class文件
-
Class Loader:依据特定格式,加载class文件到内存
-
Execution Engine:对命令进行解析
-
Native Interface:融合不同开发语言的原生库为Java所用
-
Runtime Data Area:运行时数据区,JVM内存空间构造模型
反射
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
类从编译到执行的过程
- 编译器将xxx.java源文件编译为xxx.class字节码文件
- ClassLoader将字节码转换为JVM中的Class对象
- JVM利用Class对象实例化为Robot对象
ClassLoader
它主要工作在类装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
- 自定义ClassLoader:Java编写,定制化加载
双亲委派机制
为什么要用双亲委派机制呢?
jvm中,确定一个类的唯一性是依赖于加载这个类的类加载器和这个类本身的。只有加载类的类加载器和类本身两者都一致,jvm才会认为这个类是唯一的。假设现在不用双亲委派机制,那么我们用户在自己编写的时候创建了个java.lang.Object类,放在classpath下,然后启动,那么由于Java本身就有个java.lang.Object类并且该类是由启动类加载器加载的,而我们自定义的Object类是由应用程序类加载器加载,那么一个类的唯一性就被打破了,造成类的混乱,而如果使用了双亲委派机制,会进行限制,假如还是自定义了java.lang.Object,那么在运行时会只加载java顶层类库里的Object类。而且,这种机制避免了在内存中进行多份字节码的加载。
类的装载过程
- 加载
- 通过ClassLoader加载class文件字节码到内存中,并将这些静态数据转换成运行时数据区中的方法区中的类型数据,在运行时在堆中生成代表这个类的java.lang.Class对象作为方法区类的访问入口
- 链接
- 校验:检查加载的class的正确性和安全性
- 准备:为类常量分配存储空间并设置类变量初始值,类变量随类信息存放在方法区中,生命周期很长,使用不当可能引起内存泄漏
- 解析:JVM将常量池内的符号引用转换为直接引用
- 初始化:
- 执行变量赋值和静态代码块
loadClass和forName的区别
- Class.forName得到的class是已经初始化完成的
- Classloader.loadClass得到的class是还没有进行链接的
GC类型
- MinorGC / YoungGC:发生在新生代的收集动作
- MajorGC / OldGC:发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为
- MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为
- FullGC:收集整个Java堆和方法区的GC
Stop-The-Word
STW是Java中一种全局暂停的现象,多半由于GC引起。所谓全局停顿,就是所有Java代码停止运行,native代码可以执行,但不能和JVM交互。
其危害是长时间服务停止,没有响应。对于HA系统,可能引起主备切换,严重危害生产环境
垃圾收集类型
- 串行收集:GC单线程内存回收、暂停所有的用户线程,如:Serial
- 并行收集:多个GC线程并发工作,此时用户线程是暂停的,如:Parallel
- 并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,如:CMS
判断是否垃圾的步骤
- 根搜索算法判断不可用
- 看是否有必要执行finalize方法,这个方法是在对象第一次回收的时候,会调用这个finalize方法,如果这个对象没有覆盖finalize方法或者这个方法已经被虚拟机调用过的话,这就没必要执行finalize方法了。
- 两个步骤走完后对象仍没有人使用,那就属于垃圾
判断类无用的条件
- JVM中该类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 没有任何地方引用该类的Class对象
- 无法在任何地方通过反射访问这个类
经过如上判断若该类已经无用,则可进行类的卸载
垃圾收集算法
标记-清除法
标记清除法(Mark-sweep)算法分成标记和清除两个阶段,先标记出要回收的对象,让后统一回收这些对象
- 优点是简单
- 缺点:
- 效率不高,标记和清除的效率都不高
- 标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC
复制算法
把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉
-
优点是:实现简单,运行高效,不用考虑内存碎片的问题
-
缺点是:内存有些浪费
-
JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor,回收时,把存活的对象复制到另一块Survivor
-
HotSpot默认的Eden和Survivor比是8:1,也就是每次都能用90%的新生代空间
-
如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代
-
分配担保
当新生代进行垃圾回收后,新生代的存活区放置不下,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保,步骤如下:
1、在发生MinorGC前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的。
2、如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
3、如果老年代最大的连续可用空间比历次晋升到老年代对象的平均大小要大,则尝试进行一次MinorGC,反之,则改做一次Full GC
-
标记整理法
由于复制算法在存活对象比较多的时候,效率很低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理法,标记整理法标记过程和标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存
垃圾收集器
HotSpot中的收集器
在JDK13中默认垃圾收集器为G1
串行收集器
Serial(串行)收集器 / Serial Old收集器,是一个单线程的收集器,在垃圾收集时,会Stop-The-World
可以看到,在串行收集器中,不管是新生代还是老年代,在垃圾回收的时候都只起一个垃圾回收线程,并且要暂停所有的用户线程。
-
优点
简单,对于单CPU,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器
-
使用-XX:+UseSerialGC来开启,会使用:Serial + Serial Old的收集器组合
-
新生代使用复制算法,老年代使用标记-整理算法
并行收集器
- ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World
注意,在老年代处配合CMS的示意图中并不是代表只开了一个GC线程,只是后面的部分交由CMS去做了。
- 在并发能力好的CPU环境里,它停顿的时间要比串行收集器短;但对于单CPU或并发能力较弱的CPU,由于多线程的交互开销,可能比串行回收器更差
- 是Server模式下首选的新生代收集器,且能和CMS收集器配合使用
- 不再使用-XX:+UseParNewGC来单独开启
- -XX:ParallelGCThreads:指定线程数,最好与CPU数量一致
- 只在新生代使用,用复制算法
新生代Parallel Scanvenge收集器
-
新生代Parallel Scanvenge收集器 / Parallel Old收集器:是一个应用于新生代的、使用复制算法的、并行的收集器
-
跟ParNew很类似,但更关注于吞吐量,能最高效率的利用CPU,适合运行后台应用
-
使用-XX:+UseParallelGC来开启
-
使用-XX:+UseParallelOldGC来开启老年代使用Parallel Old收集器,使用Parallel Scanvenge + Parallel Old的收集器组合
-
-XX:MaxGCPauseMillis:设置GC的最大停顿时间
-
新生代使用复制算法,老年代使用标记-整理算法
CMS收集器
CMS(Concurrent Mark and Sweep 并发标记清除) 收集器分为:
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 并发清除:并发回收垃圾对象
-
在初始标记和重新标记两个阶段还是会发生Stop-the-World
-
使用标记清除算法,多线程并发收集的垃圾收集器
-
最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备
-
优点:
低停顿、并发执行,因为有两个阶段都是与用户线程并行的,所以用户线程可以不用停止,而且它停的地方基本都是标记阶段,只是打个标记,所以停顿的时间也相对较短
-
缺点:
- 并发执行,对CPU资源压力大;
- 无法处理在处理过程中产生的垃圾,可能导致FullGC
- 采用的标记清除算法会导致大量碎片,从而在分配大对象时可能触发FullGC
-
开启:-XX:UseConcMarkSweepGC:使用ParNew + CMS + Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
-
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发回收,默认80%
G1收集器
G1(Garbage-First)收集器:是一款面向服务端应用的收集器,与其他收集器相比,具有如下特点:
- G1把内存划分成多个独立的区域(Region)
- G1仍采用分代思想,保留了新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合,且不需要Region是连续的
- 一块Region既可以是新生代也可以是老年代,也可以是Survivor区,也可以是Eden区,也就是说在G1中,新生代和老年代只是逻辑概念上的东西,并非物理上的。
- G1能够充分利用多CPU、多核环境硬件优势,尽量缩短STW
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片,而且在G1中的复制算法不止应用于新生代中,老年代也会使用,所以一般情况下,G1是不会触发FullGC的
- G1的停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间
- G1跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效集合。也就是说,G1每次在回收的时候都不会去做FullGC,通常情况下它每次会做新生代的内存回收,然后再加上部分价值最高的老年代的区域的回收,这个模式也称作MixedGC模式
运行阶段
跟CMS类似,也分为四个阶段:
-
初始标记:
只标记GCRoots能直接关联到的对象
-
并发标记:
进行GC Roots Tracing的过程
-
最终标记:
类似于CMS的重新标记阶段,修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
-
筛选回收:
根据时间来进行价值最大化的回收
- 使用和配置G1:-XX:+UseG1GC:开启G1,JDK13默认就是G1
- -XX:MaxGCPauseMillis=n:最大GC停顿时间,这个是软目标,JVM将尽可能(但不保证)停顿小于这个时间
- -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
- -XX:NewRatio=n:默认为2
- -XX:SurvivorRatio=n:默认为8
- -XX:MaxTernuringThreshold=n:新生代到老年代的岁数,默认是15
- -XX:ParallelGCThreads=n:并行GC的线程数,默认值会根据平台不同而不同
- -XX:ConcGCThreads=n:并发GC使用的线程数
- -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
- -XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域
ZGC收集器
JDK11加入的具有实验性质的低延迟收集器
ZGC设计目标是:支持TB级的内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%
ZGC里面的新技术:着色指针和读屏障
GC性能指标
- 吞吐量:应用代码执行的时间 / 运行的总时间
- GC负荷:与吞吐量相反,是GC时间 / 运行的总时间
- 暂停时间:就是发生STW的总时间
- GC频率:就是GC在一个时间段内发生的次数
- 反应速度:就是从对象成为垃圾到被回收的时间,对于该指标而言,反应速度越快,说名对象从成为垃圾到被回收的时间越短,内存的利用效率越高,但是也可能伴随频繁的GC,所以并不是说反应速度越快越好
- 交互式应用通常希望暂停时间越少越好
JVM内存配置原则
- 新生代尽可能设置大点,如果太小会导致:
- YGC次数更加频繁
- 可能导致YGC后的对象进入老年代,如果此时老年代满了,会触发FGC
- 对老年代,针对响应时间优先的应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数
- 如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停
- 如果设置大了,会需要较长的回收时间
- 对老年代,针对吞吐量优先的应用:通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象
- 依据对象的存活周期进行分类,对象优先在新生代中分配,长时间存活的对象进入老年代
- 根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法;大量对象存活,适合标记清除或者标记整理