阅读 92
5000字JVM总结

5000字JVM总结

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

一、执行引擎

  1. 编译
  2. 执行
  3. 垃圾回收

二、类加载器子系统

  1. 类的生命周期
  2. 类加载器类型
  3. 双亲委派机制
  4. 类初始化的时机

三、运行时数据区


JVM结构思维导图

JVM,Java Virtual Machine(Java虚拟机),是Java语言的运行环境。 JVM的内部体系结构分为三部分:类加载器子系统、运行时数据区和执行引擎。

一、执行引擎

执行引擎包括:解释器、即时编译器、垃圾回收器。

1. 编译

大部分的程序代码,在转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图中的步骤: 在这里插入图片描述

众所周知,java代码可通过javac编译器生成字节码(这部分操作是在虚拟机之外进行的),如下图所示(对应上图中的橙色部分): 在这里插入图片描述

2. 执行

虚拟机中的执行引擎通过字节码解释器,将上述编译生成的字节码翻译成对应的机器指令,逐条读入,逐条解释翻译; 由于解释执行的速度比较慢,所以Java又引入了JIT技术,将源代码直接编译成和本地机器平台相关的机器语言,从而提高了执行速度。 这也是后来Java被称为“半编译半解释型语言”的原因。 在这里插入图片描述

JIT的工作原理如下图所示:

在这里插入图片描述

当一段代码在将来只会被执行一次,那么编译就是在浪费精力,因为将代码翻译成字节码,相对于编译这段代码并执行代码来说,要快得多。 但是,当一段代码频繁地调用方法,或是一个循环,也就是这段代码被多次执行时(“热区代码”),那么编译一次再多次执行,就比多次解释加执行快得多。

3. 垃圾回收

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。 在这里插入图片描述

垃圾对象标记算法主要有以下两种: 1)引用计数算法:每个对象保存一个整型引用计数器,用来记录对象被引用的次数,当该对象被另一个对象引用时,计数器加1,当失去一个引用时,计数器减1;引用计数算法就是通过判断对象的引用数量来决定对象是否可以被当做垃圾对象回收掉。虽然引用计数法效率高,但是当两个对象互相引用时会导致这两个对象一直不会被回收,这是一个致命缺陷。所以JVM并没有采用该标记算法。 2)可达性分析算法:运行程序把所有的引用关系链看作一张图,通过GC-Roots根对象集合作为起始点,从每个根节点向下不断搜索被根对象集合所连接的对象是否可达,搜索路径称为引用链(Reference-Chain),如果对象到GC-Roots没有任何引用链存在,则说明此对象是不可用的,如图所示: 在这里插入图片描述

相对于引用计数法算法,可达性分析算法则避免了循环引用导致的问题,同样具备执行高效的特点,也是JVM采用的标记算法。 垃圾回收算法主要有四种:标记清除算法、标记整理算法、复制算法、分代收集算法。 1)标记清除法:分为标记和清除两个阶段;标记阶段:从根对象集合进行扫描,对存活的对象对象标记;清除阶段:再次扫描发现未被标记的对象并进行回收; 在这里插入图片描述

该算法效率不高,进行垃圾回收需要暂停应用程序,同时会产生大量内存碎片,后续程序运行过程中分配内存占用较大的对象时,会有连续内存不够情况,容易触发再一次垃圾收集动作。

2)标记整理算法:分为三个阶段,可以理解为比标记清除算法多了一个整理阶段;第一阶段标记出垃圾对象;第二阶段让所有存活的对象都向内存区一端移动;第三阶段直接清理掉边界端以外的内存,类似于磁盘整理的过程。 在这里插入图片描述

该垃圾回收算法效率不高,对象移动过程需要暂停应用程序,适用于对象存活率高的场景,比如老年代。

3)复制算法:复制算法将内存按容量划分为大小相等的两块,每次只使用其中的一块,当使用的这块的内存用完,就将还存活着的对象复制到另外一块空闲内存上,然后使用过的内存空间一次清理。 在这里插入图片描述

该算法实现简单,运行效率高,但是内存空间严重浪费,适用于对象存活率低的场景,比如新生代。

4)分代收集算法:分代收集算法根据年轻代和老年代的各自特点采用不同的算法机制,不同内存区域中对象生命周期也不同,因此对堆内存不同区域采用不同的回收策略可以提高垃圾回收执行效率。通常情况新生代对象存活率低,回收频繁,就采用复制算法;老年代存对象生命周期长,存活率高,就用标记清除算法或者标记整理算法。老年代的垃圾回收称之为Major GC或Full GC,新生代的垃圾回收称之为MinorGC或Young GC。 在这里插入图片描述

新生代对象满足一定条件后会转移到老年代中:

  • 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
  • 如果对象的大小大于Eden的二分之一会直接分配在Old区;如果小于Eden的一半但是没有足够的空间,就进行新生代GC,新生代GC后仍然放不下,再放到Old区。如果Old区也分配不下,会做一次老年代GC。
  • 动态年龄判断 ,Eden区->Survivor 区后对象的初始年龄变为1,新生代GC后如果对象还存活则进入Survivor 区且年龄+1。当大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代。

Java采用分代收集算法。

二、类加载子系统

1. 类的生命周期

在这里插入图片描述

顾名思义,类加载就是把Java类加载到JVM中。类的整个生命周期分为五个阶段:加载->连接->初始化->使用->卸载,其中连接又分为三步:验证->准备->解析,如下图所示。

在这里插入图片描述 1)加载 1.通过全限定类名来获取定义此类的二进制字节流。 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3.在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2)连接

  • 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

1.文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
2.元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
4.符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
复制代码

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  • 准备

为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

  • 解析

虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。 3)初始化 到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。 4)使用 Object类是所有类的父类,常用的类有String类、Math类、Date类、System类等等。每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。 5)卸载

  • 由Java虚拟机自带的类加载器(根类加载器、扩展类加载器和系统类加载器)所加载的类,在虚拟机的生命周期中,始终不会被卸载。
  • 由用户自定义的类加载器加载的类是可以被卸载的。Java中没有提供显式进行类卸载的API,但是如果加载类的ClassLoader对象被垃圾回收器回收的话,这个类就会被卸载。所以我们可以自己实现ClassLoader,自己加载类,然后对ClassLoader对象的引用赋值为null,等ClassLoader对象剩下的引用数量为0时会被回收,这样就达到卸载类的目的了。

2. 类加载器类型

Java的类加载器可分为四种:

  • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分,用来加载 Java 的核心类;

这个类加载器负责将存放在 <JRE_HOME>\lib 目录中的、或者被 -Xbootclasspath 参数所指定的路径中并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载;出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)类库加载到虚拟机内存中; 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。

  • 扩展类加载器(Extension ClassLoader),使用Java实现,是由ExtClassLoader实现的;

负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader),负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
  • 用户自定义类加载器:开发者可通过继承Java. lang. ClassLoader来自定义自己的类加载器。

在这里插入图片描述

3. 双亲委派机制

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 主要分为两个步骤: 1)首先自底向上的检查类是否已经加载过,如果加载过就直接返回该类; 2)如果都没有加载过的话,那么就自顶向下的尝试加载该类。

在这里插入图片描述

4. 类初始化的时机

只有当对类的主动使用时才会导致类的初始化,类的主动使用包括以下5种: 1)遇到new、get static、put static或invoke static这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4)当虚拟机启动时,用户需要指定一个要执行的主类( 包含main()方法的那个类),虚拟机会先初始化这个主类。 5)当使用JDK 1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例正好是对 REF_getStatic, REF_putStatic, REF_invokeStatic 进行方法句柄解析的结果时,并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。

三、运行时数据区

在这里插入图片描述

  1. 程序计数器:内存空间小,线程私有。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
  2. 虚拟机栈:线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
  3. 本地方法栈:区别于虚拟机栈的是, 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
  4. 堆:分为新生代和老年代两部分。对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出OutOfMemoryError异常。
  5. 方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

在这里插入图片描述

当new一个对象时:

  1. 判断是否第一次使用该对象所对应的类,如果是第一次使用,则使用类加载器通过双亲委派机制将该类加载到JVM中;
  2. 类加载完成后,可确定对象所需内存大小,于是在堆中为该对象分配一块内存;
  3. 内存分配完成后,将分配到的内存空间中的数据类型初始化为零值;
  4. 对对象头进行必要的设置(例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中);
  5. 调用对象的init()方法,根据传入的属性值给对象属性赋值;
  6. 在线程栈中新建对象引用,并指向堆中的对象。

至此,对象创建完成,之后便是对对象的访问,主要有两种访问方式:句柄访问(间接访问)和直接指针访问。这两者的主要区别在于,采用句柄访问时,堆中存储的是句柄,句柄中不包含对象实例数据,而是一个指向对象实例数据的指针。 在这里插入图片描述


参考文章与扩展阅读:

  1. 《深入理解Java类加载》www.cnblogs.com/czwbig/p/11…
  2. 《Java的类加载器种类》www.cnblogs.com/fengbs/p/75…
  3. 《Java类初始化时机详解》www.jianshu.com/p/3afa5d24b…
  4. 《Java类的卸载机制》blog.csdn.net/xorxos/arti…
  5. 《 与 方法》www.jianshu.com/p/8a14ed0ed…
  6. 《JVM执行引擎理解》www.cnblogs.com/junlinsky/p…
  7. 《JIT简介》developer.ibm.com/zh/articles…
  8. 《虚拟机系列 | 执行引擎和垃圾回收》blog.csdn.net/cicada_smil…
  9. 《垃圾收集器》blog.csdn.net/zq602316498…
  10. 《Java垃圾回收》www.cnblogs.com/czwbig/p/11…
  11. 《Java虚拟机(JVM)你只要看这一篇就够了!》blog.csdn.net/qq_41701956…
  12. 《Java中new一个对象是一个怎样的过程?JVM中发生了什么?》www.cnblogs.com/gjmhome/p/1…
  13. 《Java程序员必备基础结构图》www.cnblogs.com/jay-huaxiao…
  14. 《学习Java有必要学习JVM吗?》www.zhihu.com/question/36…
文章分类
后端
文章标签