Java 虚拟机基础知识

基于 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》,第 3 版已经出来,后续更新!

类加载过程

在这里插入图片描述
 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

加载

查找字节流,导入 class 文件,创建类的过程

其中,加载阶段,涉及到双亲委派模型,如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,当父加载器无法完成这个请求时,子类才会尝试去加载。

  • 启动类加载器(Bootstrap ClassLoader) :负责加载 JAVA_HOME\lib 目录中的,或通过 -Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。

  • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
    在这里插入图片描述

  • 链接:

验证

验证文件内容是否满足虚拟机规范

  • 文件格式验证,验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。

  • 元数据验证,语法校验

  • 字节码验证,进行数据流和控制流分析,对类的方法体进行校验分析

  • 符号引用验证,符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

准备

静态字段分配内存,静态字段默认零值(如0、0L、null、false等)注意:public static final int v = 8080; 如果类变量加上 final,那么此时值将是赋值的值。final 表示不可变,所以在准备阶段就赋指定值。

解析

符号引用解析,将常量池内的符号引用替换为直接引用的过程

  • 符号引用,以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

  • 直接引用,直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

执行类构造器方法的过程,静态字段赋值

  • 类初始化方法,编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
  • 对象初始化方法(类的实例化),编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

JVM类生命周期概述:加载时机与加载过程

类初始化时机

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

不会执行类初始化场景(不是对象实例化过程)

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  • 定义对象数组,不会触发该类的初始化。

  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

  • 通过类名获取Class对象,不会触发类的初始化。

  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作

对象初始化顺序

创建一个对象常常需要经历如下几个过程:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。

深入理解Java对象的创建过程:类的初始化与实例化

JVM 运行时数据分区

在这里插入图片描述

存放对象的地方

  • Dog dog = new Dog(); new Dog() 这个对象就在堆上

  • 堆进一步可分为:年轻代、老年代。年轻代对象大多数存活时间短,很快会被垃圾回收。年轻代存活久的会进入老年代,当然有些对象比较大,会直接进入老年代。年轻代内存分区进一步可分为 Eden、survivor0、survivor1 三个部分,内存默认大小比例为 8:1:1。

  • 垃圾回收的时候, Eden 和其中一个 surivor 内的对象大部分会被清除,而没清除的放入另外一个 surivor 中(垃圾回收算法——复制算法)。
    在这里插入图片描述

  • JDK 1.8 之前,堆中有永久代(Perm GEN)这个概念,JDK 1.8 开始,去掉永久代,取而代之的是元空间(MetaSpace),元空间不属于堆,元空间的大小仅受限于机器的内存大小。当 Perm GEN 属于堆的时候,有时候由于堆内存大小不足,报 “java.lang.OutOfMemoryError: PermGen space” 错误,现在这个错误将不复存在;当然对应的 -XX:MaxPermSize 也将不起作用

方法区

存储元(Meta)数据,类结构信息,运行时常量池(Run-Time Constant Pool),字段,方法代码。

方法区是一个接口概念,是 Java 虚拟机定义的一个规范;而永久代、元空间则被认为是方法区这个规范的实现,并且永久代 HotSpot 虚拟机才有的概念,其它虚拟机没有。

JDK 1.7 时,永久代包含类的元信息、静态变量、常量池(Constant Pool Table);

JDK 1.8 开始元空间(元空间不属于堆,在机器的本地内存中)存储类的元信息。静态变量、常量池并入堆中。

常量池:用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

常量池

JVM常量池浅析

  • Class 文件常量池,class 文件中有定义,包括字面量和符号引用

    • 字面量:比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等

    • 符号引用:

      • 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)

      • 字段的名称和描述符(private、static 等描述符)

      • 方法的名称和描述符(private、static 等描述符)

  • 运行时常量池,JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时在 jdk 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域

  • 全局字符串常量池

  • 基本类型包装类对象常量池

Java 虚拟机栈

在这里插入图片描述
一个线程就包含一个虚拟机栈,与线程共存亡。

虚拟机栈有大小,如果栈的深度大于 JVM 所允许的范围,会抛出 StackOverflowError;如果申请不到额外空间,会抛出 OutOfMemoryError,这两种错误如果要捕获,需使用 Throwable 进行捕获。

描述的是 Java 方法执行的内存模型,线程执行一个方法时,虚拟机栈就会创建一个栈帧,栈帧内包含局部变量表,操作数栈。方法执行完退出,该栈帧就会清除。

栈帧内容:

  • 局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和 returnAddress 类型(它指向了一条字节码指令的地址)。

    对象引用:强引用、软引用、弱引用、虚引用。(在垃圾回收里面详细说)

  • 操作数栈

    操作数栈的最大深度也是在编译的时候就确定了,当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。

    Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

    基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。

  • 动态链接

    每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。

    这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

  • 方法出口

    一般来说,方法正常退出时,调用者的 PC 计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

程序计数器

PC,Program Counter Register

当前线程所执行的字节码的行号指示器。唯一一个无 OOM 的区域

如果这个方法不是 native 方法,那么 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令地址。如果是 native 方法,那么 PC 寄存器保存的值是 undefined。

任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,而这个被线程执行的方法称为该线程的当前方法,其地址被存在 PC 寄存器中。

本地方法栈

「当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也会使用到本地方法栈。如果 Java 虚拟机不支持 natvie 方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈。」

垃圾回收

确定垃圾

  • 引用计数:相互引用问题

  • 可达性分析:JVM会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为GC Roots。如果在 “GC roots” 和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

引用类型

强引用:关键字new

软引用:SoftReference ReferenceQueue,JVM 认为内存不足时,会去试图回收软引用指向的对象。

弱引用:WeakReference ReferenceQueue,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

虚引用:PhantomReference,无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 fnalize 以后,做某些事情的机制。

垃圾回收算法

  • 复制: 类似 eden、survivor 将存活的对象复制到另外一块区。

  • 标记-清除:标记需要回收的对象然后统一回收。效率低、碎片多

  • 标记-整理:结合标记-清除、复制。标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

  • 分代收集:主流垃圾回收算法,根据对象存活周期不同分配到不同区域。新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

  • 分区收集:G1

垃圾回收器

在这里插入图片描述

  • Serial GC,单线程,Stop-The-World,是 Java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器,新生代采用复制算法,老年代采用标记-整理算法。

  • ParNew GC,Serial GC 的多线程版本,多个线程并发进行垃圾回收,其它与 Serial 一样。它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

    • 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。

  • Parrallel Scavenge GC,新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  • Serial Old:Serial 垃圾收集器年老代版本、标记-整理,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

  • Parallel Old:使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

  • CMS(Concurrent Mark Sweep) GC,标记-清除(Mark-Sweep)算法,一种以获取最短回收停顿时间为目标的收集器,它而非常符合在注重用户体验的应用上使用。

    运行过程:

    • 初始标记:标记一下 GC Roots 能直接关联的对象,暂停其他线程、速度很快

    • 并发标记:GC tracing 过程,与用户线程一起工作

    • 重新标记:修改并发标记期间,程序运行改变标记的部分,暂停其他线程。

    • 并发清除:与用户线程一起工作,清除标记的区域。

  • G1 GC,Region之间是复制算法,但整体上实际可看作是标记-整理(Mark-Compact)算法

    • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU或者CPU核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

    • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

    • 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

    • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

命令

-Xms / -Xmx — 堆的初始大小 / 堆的最大大小

-Xmn — 堆中年轻代的大小

-XX:-DisableExplicitGC — 让System.gc()不产生任何作用

-XX:+PrintGCDetails — 打印GC的细节

-XX:+PrintGCDateStamps — 打印GC操作的时间戳

-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小

-XX:NewRatio — 可以设置老生代和新生代的比例

-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布

-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值

-XX:TargetSurvivorRatio:设置幸存区的目标使用率

调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
• jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

• jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

• jmap,JVM Memory Map命令用于生成heap dump文件

• jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看

• jstack,用于生成java虚拟机当前时刻的线程快照。

• jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

对象

对象在内存中存储的布局可以分为3块区域:

对象头(Header):HotSpot虚拟机的对象头包括两部分信息,

第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部
分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为"Mark Word"。这部分数据长度在32位机器和64位机器虚拟机中分别为4字节和8字节

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据(Instance Data):实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

对齐填充(Padding):对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值