自己整理了些关于JVM(Java Virtual Machine)的相关概念。
主要围绕着以下几个问题来写:
- jvm是什么
- 类的加载过程
- jvm的内存结构
- 双亲委派模型
- GC算法,如何调优
- FULL GC的条件
jvm是什么?
JVM是虚拟机的英文简称。它是java运行环境的一部分,是一个虚构出来的计算机,它是通过在实际的计算机上仿真模拟各种计算机功能来实现的。主要用来运行Java的类文件。
经常听说hotspot,她和jvm的区别在于 hotspot是jvm的一种实现,是sun jdk和open jdk中自带的虚拟机,同时也是目前使用范围最广的虚拟机。
命令行中输入 java -version 看到了hotspot
类的加载过程
1.加载
加载时jvm做了这三件事:
1)通过一个类的全限定名来获取该类的二进制字节流
2)将这个字节流的静态存储结构转化为方法区运行时数据结构
3)在内存堆中生成一个代表该类的java.lang.Class对象,作为该类数据的访问入口
2.验证
验证、准备、解析这三步可以看做是一个连接的过程,将类的字节码连接到JVM的运行状态之中
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到jvm的安全
验证主要包括以下几个方面的验证:
1)文件格式的验证,验证字节流是否符合Class文件的规范,是否能被当前版本的虚拟机处理
2)元数据验证,对字节码描述的信息进行语义分析,确保符合java语言规范,元数据验证验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的 所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深 入到方法体内。
3)字节码验证 既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验,通过数据流和控制流分析,确 定语义是合法的,符合逻辑的。
4)符号引用验证 这个校验在解析阶段发生,符号引用验证做的工作主要是验证字段、类方法以及接口方法的访问权限、根据类的全限定名是否能定位到该类等。具体过程会在接下来的解析阶段进行 分析。
3.准备 将类的静态变量放进方法区分配内存,初始化为系统的初始值。对于final static修饰的变量,
直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为0,而不是7
public static int a=7
4.解析
解析是将常量池内的符号引用转为直接引用(如物理内存地址指针)
5.初始化
到了初始化阶段,jvm才真正开始执行类中定义的java代码
初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。 虚拟机会保证一个类的
<clinit>() 方法在多线程环境中被正确加锁和同步。
何时初始化:
1>在类没有进行过初始化的前提下,当执行new
、getStatic
、setStatic
、invokeStatic
字节码指令时,类会立即初始化。对应的java操作就是new
一个对象、读取/写入一个类变量(非final
类型)或者执行静态方法。
2>在类没有进行过初始化的前提下,当一个类的子类被初始化之前,该父类会立即初始化。
3>在类没有进行过初始化的前提下,当包含main
方法时,该类会第一个初始化。
4>在类没有进行过初始化的前提下,当使用java.lang.reflect
包的方法对类进行反射调用时,该类会立即初始化。
5>在类没有进行过初始化的前提下,当使用JDK1.5
支持时,如果一个java.langl.incoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。第五个没看懂 囧
注:
通过子类引用父类的类变量不会触发子类的初始化操作
通过定义对象数组的方式是不能触发对象初始化的
引用类的final类型的类变量无法触发类的初始化操作
并发初始化情况下的运行机制又如何?
JVM虚拟机规定了几条标准:
- 先父类后子类,(源码中)先出现先执行
- 向前引用:一个类变量在定义前可以赋值,但是不能访问。
- 非必须:如果一个类或接口没有类变量的赋值动作和
static
代码块,那就不生成<clinit>
方法. - 执行接口的
<clinit>
方法不需要先执行父接口的<clinit>
方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>
方法。 - 同步性:
<clinit>
方法的执行具有同步性,并且只执行一次。但当一个线程执行该类的<clinit>
方法时,其他的初始化线程需阻塞等待。
jvm的内存结构
更详细的解释于https://blog.csdn.net/rongtaoup/article/details/89142396
程序计数器:指向当前线程正在执行的字节码指令的地址、行号
虚拟机栈: 存储当前线程运行方法所需要的数据、指令、返回地址。first in last out
本地方法栈: Java代码中用native修饰的,和操作系统交互的,底层用c/c++写的
方法区: 被虚拟机加载的类信息、常量、静态常量等。
堆: 主要存放new出来的对象
双亲委派模型
GC算法,如何调优
说到GC算法,首先要了解堆的概念。堆是线程共享的一块内存区域,也是垃圾回收器进行立即回收的最重要的区域。堆从 GC的角度可以分为
新生代:用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC(MinorGC采用复制算法)进行垃圾回收。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。
Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行 一次垃圾回收
MinorGC的过程:
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区);
其次清空eden 、servicorFrom 中的对象;
最后ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom 区。
老年代:主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出OOM(Out of Memory)异常。
永久代:指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被 放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这 也导致了永久代的区域会随着加载的Class的增多而胀满,终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize控制, 而由系统的实际可用空间来控制。
引用计数法:
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关 联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收 对象。
可达性分析:
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots” 对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收。
标记-清除算法:
基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清 除阶段回收被标记的对象所占用的空间。如图
从图中我们就可以发现,该算法大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
复制算法:
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用 的内存清掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是大的问题是可用内存被压缩到了原 本的一半。且存活对象增多的话,Copying算法的效率会大大降低。
标记-整理算法
结合了以上两个算法,为了避免缺陷而提出。标记阶段和标记-清除算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃 圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC 对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代 划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用 Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块Survivor空间中。
老年代因为每次只回收少量对象,因而采用标记-整理算法
1. JAVA虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储class类, 常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
2. 对象的内存分配主要在新生代的Eden Space和Survivor Space的From Space(Survivor目 前存放对象的那一块),少数情况会直接分配到老生代。
3. 当新生代的Eden Space和From Space空间不足时就会发生一次GC,进行GC后,Eden Space和From Space区的存活对象会被挪到To Space,然后将Eden Space和From Space进行清理。
4. 如果To Space无法足够存储某个对象,则将这个对象存储到老生代。
5. 在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。
6. 当对象在Survivor区躲过一次GC 后,其年龄就会+1。默认情况下年龄到达15 的对象会被 移到老生代中。
FULL GC的条件
1.System.gc()。
调用System.gc()
只是建议JVM进行FULL GC,并不一定保证,但是大多情况下都会进行一次FULL GC。不建议使用,可使用-XX: + DisableExplicitGC
来禁止调用System.gc()
。
2.方法区空间不足
3.老年代空间不足
4.堆中分派大对象。
大对象是指需要大量连续空间的java对象,例如数组。此类对象会直接进入老年代,但是老年代虽然有空闲空间,但是无法找到足够大的连续空间分配给该对象,这样就会触发一次FULL GC。
5.通过MinorGC进入老年代的平均大小大于老年代的可用内存
6.Eden区,从From Space区向To Space区复制的时候,对象大小大于To Space可用内存,则把该对象放到老年代,且老年代的可用内存大小小于该对象大小
部分引用于:https://www.cnblogs.com/coder-lichao/p/10919908.html
https://segmentfault.com/a/1190000012527652
https://www.dazhuanlan.com/2019/12/05/5de88e1a53a6d/
侵删