HotSpot

image.png

Java代码执行流程

  • 源程序通过编译器生成字节码文件
  • 字节码文件通过类加载器和解释器加载对生成的字节码文件进行解释运行

JVM生命周期

  • 启动
    • 启动是通过引导类加载器 (bootstrap class loader) 创建一个初始类 (initial class) 来完成的, 这个类是由虚拟机的具体实现指定的
  • 执行
    • Java虚拟机的使命就是执行Java程序
    • 程序开始执行它才运行, 程序结束时它就停止
    • 执行一个所谓的Java程序的时候, 真正在运行的是一个叫做Java虚拟机的进程
  • 退出的几种情况
    • 程序正常结束
    • 程序在执行过程中遇到异常或错误而异常终止
    • 由于操作系统出现错误而导致Java虚拟机进程终止
    • 某线程调用Runtime类或System类的exit方法, 或Runtime类的halt方法, 并且Java安全管理器允许这次exit或halt操作
    • 除此之外, JNI ( java native interface )规范描述了用JNI Invocation API来加载或卸载Java虚拟机时, Java虚拟机的退出情况

创建对象的步骤

  1. 判断对象对应的类是否加载、链接、初始化
  2. 为对象分配内存
    1. 如果内存规整
      1. 指针碰撞
    2. 如果内存不规整
      1. 虚拟机需要维护一个列表
      2. 空闲列表分配
    3. 说明
  3. 处理并发安全问题
    1. 采用CAS配上失败重试保证更新的原子性
    2. 每个线程预先分配一块TLAB
  4. 初始化分配到的空间
    1. 所有属性设置默认值,保证对象实例字段在不赋值时可直接使用
  5. 设置对象的对象头
  6. 执行init方法进行初始化

类加载子系统

类的加载过程

image.png

  • Loading 加载
    • 通过一个类的全限定名获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    • 加载class文件的方式
      • 从本地加载
      • 从网络加载,web applet
      • 从压缩包中加载
      • 运行时技术,动态代理
      • 其他文件生成,JSP
  • Linking 连接
    • Verify 验证
      • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
      • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符合引用验证。
    • Prepare 准备
      • 为类变量分配内存并且设置该类变量的默认初始值,即零值
      • 这里不用包含final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
      • 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到Java堆中
    • Resolve 解析
      • 将常量池内的符号引用转换为直接引用的过程。
      • 事实上,解析操作往往会伴随这JVM在执行完初始化之后在执行
      • 符号引用是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
      • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,对应常量池中的 CONSTANT_Clasnn_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info
  • Initialization 初始化

    • 初始化阶段就是执行类构造器方法()的过程
    • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
    • 构造器方法中指令按语句在源文件中出现的顺序执行
    • ()不同于类的构造器。(关联:构造器是虚拟机视角下的())
    • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
    • 虚拟机必须保证一个类的()方法在多线程下被同步加锁

      类加载器

      启动类加载器

      或者引导类加载器 Bootstrap ClassLoader
  • 并不继承java.lang.ClassLoader,没有父加载器

  • C/C++编写,嵌套在JVM内部
  • 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 加载拓展类和应用程序的加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加在包名为java、javax、sun等开头的类

扩展类加载器

Extension ClassLoader

  • 父类加载器为BootstrapClassLoader
  • 派生于ClassLoader
  • Java编写,由sun.misc.Launcher$ExtClassLoader实现
  • 从java.ext.dirs系统属性所指定的目录中的加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库
  • 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序加载器

AppClassLoader

  • 父类加载器为Extension ClassLoader
  • 派生与ClassLoader
  • Java编写,由sun.misc.Launcher$AppClassLoader实现
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 是程序中默认的类加载器,java应用的类都是由它来完成
  • 通过ClassLoader#getSystemClassLoader()方法可以获取该类加载器

用户自定义加载器

在日常开发中类的加载几乎是由上述三种类加载器相互配合使用,
在必要时还可以自定义类加载器

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

自定义类加载实现步骤:

  1. 通过java.lang.ClassLoder类的方式
  2. 在JDK1.2之后建议把自定义加载逻辑写在findCLass()方法中中
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求可以直接继承URLClassLoader类,避免自己去写findClass()方法及其他获取字节码流的方式
    1. public class CustomClassLoader extends ClassLoader{
    2. @Override
    3. protected Class<?> findClass(String name) throws ClassNotFoundException{
    4. try{
    5. byte[] result = getClassFromCustomPath(name);
    6. if(result == null)
    7. }
    8. }
    9. }

    双亲委派机制

    工作原理
  • 类加载器收到了类加载请求,它会把请求委托给父类加载器去执行,如果还存在父类加载器,则依次向上递归,直到最顶层的启动类加载器
  • 如果父类加载器可以完成类加载任务,就返回成功,如果所有的父类加载器无法加载,子加载器才会尝试自己加载,这就是双亲委派模式

优势

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随缘篡改

沙箱安全机制

  • 沙箱机制就是将Java代码限定在虚拟机特定的运行范围,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏
  • 沙箱主要限制系统资源访问,系统资源包括 CPU 内存 文件系统网络。 不同级别对这些资源访问的限制也可以不一样

    运行时数据区内部结构

  • 每个线程: 独立包括程序计数器/栈/本地栈

  • 线程间共享: 堆/堆外内存 ( 永久代或元空间/代码缓存 )

JVM的内存模型

image.png

线程私有的数据区

程序计数器(Program Counter Register)

  • 每个 Java 虚拟机线程都有自己的 pc(程序计数器)寄存器。
  • 在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据 时间片轮询抢夺CPU时间资源。
  • 也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。
  • 为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。
  • 程序计数器是线程私有的一块较小的内存空间,其可以看做是当前线程所执行的字节码的行号指示器。
  • 如果线程正在执行的是一个 Java 方法,计数器记录当前正在执行的 Java 虚拟机指令的地址;
  • 如果正在执行的是 Native 方法,则计数器的值为空。
  • 程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。
  • 在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。
  • 如果线程当前正在执行的方法是本机的,那么 Java 虚拟机的 pc 寄存器的值是未定义的。
  • Java 虚拟机的 pc 寄存器足够宽,可以保存特定平台上的 returnAddress 或本地指针。

    栈内存(Java Virtual Machine Stack)

  • 每个 Java 虚拟机线程都有一个私有的 Java 虚拟机堆栈,与线程同时创建,是线程私有的。

  • 每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 其中,局部变量表主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和 对象句柄,它们可以是方法参数,也可以是方法的局部变量。
  • Java 虚拟机堆栈存储帧。它保存局部变量和部分结果,并在方法调用和返回中发挥作用。
  • 因为除了推送和弹出帧外,Java 虚拟机堆栈永远不会被直接操作,因此帧可能是堆分配的。
  • Java 虚拟机实现可以让程序员或用户控制 Java 虚拟机堆栈的初始大小,以及在动态扩展或收缩 Java 虚拟机堆栈的情况下,控制最大和最小大小。
    • 栈大小参数为-Xss
  • 以下异常情况与 Java 虚拟机堆栈相关:

    • 如果线程中的计算需要比允许的更大的 Java 虚拟机堆栈,Java 虚拟机将抛出 StackOverflowError。
    • 如果 Java 虚拟机堆栈可以动态扩展,并且尝试扩展,但没有足够的内存,或者没有足够的内存来为新线程创建初始 Java 虚拟机堆栈,则抛出OutOfMemoryError。

      本地方法栈(Native Method Stack)

  • 本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务

  • 如果提供,本机方法堆栈通常在创建每个线程时为每个线程分配。
  • 该规范允许本地方法堆栈具有固定大小或根据计算要求动态扩展和收缩。
  • 如果本地方法堆栈具有固定大小,则每个本地方法堆栈的大小可以在创建该堆栈时独立选择。
  • Java 虚拟机实现可以为程序员或用户提供对本地方法堆栈的初始大小的控制,以及在不同大小的本地方法堆栈的情况下,控制最大和最小方法堆栈大小。
  • 以下异常情况与本机方法堆栈相关:
    • 如果线程中的计算需要比允许的更大的本机方法堆栈,Java 虚拟机将抛出 StackOverflowError。
    • 如果可以动态扩展本机方法堆栈并尝试扩展本机方法堆栈但可用内存不足,或者如果可用内存不足为新线程创建初始本机方法堆栈,Java 虚拟机将抛出 OutOfMemoryError .

线程共享的数据区

堆内存(Java Heap)

  • Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存
  • 从内存回收的角度看,由于现在的垃圾收集器基本都采用分代收集算法,所以为了方便垃圾回收Java堆还可以分为 新生代 和 老年代 。
  • 新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,对象就会被移入老年代。
  • 新生代又可进一步细分为 eden、survivorSpace0 和 survivorSpace1。
  • 刚创建的对象都放入 eden,s0 和 s1 都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代
  • Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的堆。
  • 堆是在虚拟机启动时创建的。
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
  • Java 虚拟机实现可以让程序员或用户控制堆的初始大小,如果堆可以动态扩展或收缩,还可以控制最大和最小堆大小。
    • 堆最大值:-Xmx 如果超过则报:out of memory(OOM错误)
    • 堆初始值:-Xms
    • 新生代最大值:-Xmn
    • -XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.
    • -XX:+PrintGC 每次触发GC的时候打印相关日志
    • -XX:+UseSerialGC 串行回收
    • -XX:+PrintGCDetails 更详细的GC日志
  • 以下异常情况与堆相关联:
    • 如果计算需要的堆比自动存储管理系统可用的多,Java 虚拟机将抛出 OutOfMemoryError。

方法区(Method Area)

  • Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。
  • 方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。
  • 它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化和接口初始化中使用的特殊方法。
  • 方法区是在虚拟机启动时创建的。
  • 尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。
  • 本规范不要求方法区域的位置或用于管理已编译代码的策略。
  • 方法区域可以是固定大小,也可以根据计算需要扩大,如果不需要更大的方法区域,可以缩小。
  • 方法区的内存不需要是连续的。
  • Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。
    • -XX:MetaspaceSize(默认值是21M, Win 21.75M)
    • -XX:MaxMetaspaceSize(默认值-1)
  • 以下异常情况与方法区相关:
    • 如果方法区域中的内存无法满足分配请求,Java 虚拟机将抛出 OutOfMemoryError
  • 方法区的回收
    • 方法区的内存回收目标主要是针对 常量池的回收 对类型的卸载。回收废弃常量与回收Java堆中的对象非常类似。
    • 以常量池中字面量的回收为例 :
      • 假如一个字符串“abc”已经进入了常量池中,没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,
      • 如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。
    • 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
    • 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。
    • 类需要同时满足下面3个条件才能算是“无用的类”:
      • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
      • 加载该类的ClassLoader已经被回收;
      • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
      • 虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

运行时常量池 (Run-Time Constant Pool )

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用
  • 如文本字符串、被声明为final的常量值等
  • 运行时常量池是类文件中 constant_pool 表的按类或按接口运行时表示。
  • 每个运行时常量池都是从 Java 虚拟机的方法区分配的。
  • 类或接口的运行时常量池是在 Java 虚拟机创建类或接口时构建的。
  • 以下异常情况与类或接口的运行时常量池的构造有关:
    • 创建类或接口时,如果构建运行时常量池需要的内存超出 Java 虚拟机的方法区域可用的内存,Java 虚拟机将抛出 OutOfMemoryError。
    • 有关构建运行时常量池的信息。

Java堆 与 方法区的区别

Java堆是 Java代码可及的内存,是留给开发人员使用的;而非堆(Non-Heap)是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存 (如JIT编译后的代码缓存)、每个类结构 (如运行时常量池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

垃圾回收

image.png

垃圾回收算法

标记-清除算法

标记-清除算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

    复制算法

    为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

    标记-整理算法

    根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

    分代收集算法

    当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
    比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

    收集器

    CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
    CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
    从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。
    整个过程分为四个步骤:

  3. 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;

  4. 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  5. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  6. 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。

image.png
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  1. 对CPU资源敏感;
  2. 无法处理浮动垃圾;
  3. 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

    G1收集器


    G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
    被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:
  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记–整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。
这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。