深入理解JVM笔记

深入理解Java 虚拟机

第二部分 自动内存管理机制

第二章 java 内存区域 与 内存溢出

概述

  1. Java 与 c++ 的一个 最主要差别在于 虚拟机内存管理 : 1. 内存动态分配 2. 垃圾收集技术。
  2. Java 虚拟机 将所管理的内存 划分 为若干个不同的区域 ,有各自的用途,创建,销毁时间。 分为下列几个数据区域:

    1. 所有线程共享的区域:
      1. 方法区
      2. 堆区
    2. 每个线程不共享的数据区域:

      1. 本地方法栈
      2. 虚拟机栈
      3. 程序计数器

JVM 运行时数据区

程序计数器:
  1. Java 代码 编译成字节码之后 需要 程序计数器 用以指向下一条待执行字节码。类似于汇编中的概念。 字节码解释器工作时 通过该变 程序计数器 来选取 下一条 需要执行的字节码
  2. Java 多线程 采用 时间片轮转, 也需要 程序计数器 记录 每个线程 执行到的位置
  3. 如果正在执行的是一个 本地方法 而不是 Java 方法 那么 程序计数器为空
  4. 程序计数器 时 Java 虚拟机规范 中 唯一一个 没有规定任何 outOfMemoryError的地方
Java 虚拟机栈:
  1. 描述Java 方法执行的内存 模型,每个方法具有一个栈帧, 方法执行的过程就是对应栈帧入栈(开始执行)出栈(执行完成的过程)
  2. 虚拟机栈帧的组成部分:1. 局部变量表 2. 操作数栈 3. 常量池引用
  3. 通常其他资料中 ,将java内存分为 堆区 和 栈区(并不准确),而栈区就是指 虚拟机栈中的局部变量表
  4. 局部变量表中 存放:
    1. 基本数据类型
    2. 对象的引用
    3. returnAddress 类型 (具体含义 待 详细学习)
  5. 此区域 具有两种异常状况:
    1. 线程请求的栈深度大于虚拟机允许深度 StackoverflowError 异常
    2. 大多数虚拟机 允许 动态拓展,如果 拓展无法申请到足够的内存 ,那么 则产生 OutOfMemoryError 异常
本地方法栈:
  1. 如果你阅读过 Java 的源码 会发现一些底层 是由 c/cpp 实现的, 这些成为 native方法
  2. 本地方法栈 类似于 虚拟机栈, 不同点在于:
    1. 本地方法栈 运行 native method
    2. 虚拟机栈 运行 Java 类文件 编译得到的 字节码
  3. 本地方法栈的实现方法,在虚拟机规范中没有强制要求,HotSpot 虚拟机甚至将虚拟机栈 和 本地方法栈和二唯一
Java堆
  1. 一般来说,Java堆是虚拟机中内存最大的一块(对象/数组的内存占用远大于基本类型)
  2. Java堆的唯一作用就是存放 对象实例 和 数组
  3. 在虚拟机规范上, 所有的对象实例 和 数组 都存放在 Java堆中
  4. 随着JIT编译器技术发展,逃逸分析技术的成熟,并不是完全所有对象和数组存放在堆中
  5. Java堆 也即 GC堆,从不同角度开看,有不同划分方法,(分为 新生代 老年代)
  6. 内存物理地址可以不连续,逻辑地址需要连续
  7. 大多数虚拟机 支持动态拓展 Java堆,当 声明的对象 或者 数组 太多,堆无法拓展时产生 OutOfMemoryError 异常
方法区 (永生代与元空间):
  1. 用于存储已被虚拟机加载的运行时常量池,静态变量,类信息,JIT编译后的字节码等,为所有线程共享
  2. Java1.7之前的hotspot虚拟机将方法区实现为为永生代(堆中有新生代,老年代),因为hotspot将GC分代垃圾回收机制拓展到方法区。但是方法区并不是 堆 中的一个代,也叫做non-heap
    1. Java1.7 将永生代中的运行时常量池(符号引用和literal)和类的静态变量移到堆中
    2. Java1.8 取消永生代,将方法区的其他信息移到元空间中
  3. 元空间元空间也对JVM规范中方法区的实现元空间与永久代最大的区别元空间不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小
  4. 元数据与class对象的关系:元数据并不是类的Class对象。Class对象是类加载中加载的最终产品,class对象存放在堆区,元数据指类信息,JIT编译后的字节码等,存放在方法区(元空间)
  5. 方法区中的数据,已加载的类信息,常量,静态变量,编译后的字节码等数据并不是永远不被回收,只是回收条件严格,不常发生。如果虚拟机不对方法区回收将导致严重bug(某些版本的hotspot中)
  6. 方法区的回收主要针对常量池,类信息:类的卸载
  7. 无法满足内存需求时产生 OutOfMemoryError 异常
运行时常量池
  1. 运行时常量池是方法区的一部分,且每个类只有一个(一个运行时常量池 对应 一个 class常量池)
  2. class常量池和运行时常量池的关系:类文件编译后有一项是 class常量池(不同于运行时常量池),其中包括符号引用和literal,类文件中的常量池 在类加载后 存入 运行时常量池
  3. class常量池到运行时常量池详解:
    1. 类的生命周期包括(加载,验证,准备,解析,初始化,使用,和卸载)
    2. 在解析时,JVM将class常量池中的内容放到运行时常量池中,符号引用替换为直接引用(也就3. 解析和初始化可以颠倒(动态绑定)
    3. 解析的过程会去查询全局字符串池,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的
    4. 类中的静态方法,私有方法,实力构造方法,父类方法不被修改的方法,先于初始化绑定,前绑定
    5. 类中的其他方法是在第一次调用时进行绑定
  4. 运行时常量池 具有一定动态性, Java语言不一定要求常量在编译时才能产生,在运行时也可以将其 加入 运行时常量池 成为常量。 如 String.intern() 详见 (intern() 实现首先检查字符串常量池是否存在该实例,如果存在返回常量池中的引用如果不存在复制实例
  5. 等同于 方法区 无法满足内存需求时产生 OutOfMemoryError 异常
直接内存
  1. 不是虚拟机运行时 内存区域的一部分,不是Java虚拟机规范中定义的内存区域
  2. 但是使用频繁,会导致OutOfMemoryError 异常
  3. Java 1.4 中的 NIO(new input/output)类,引入 基于 通道 channel 和 缓冲 buffer 的io方式, 可以直接分配 堆外内存 (使用 native方法),通过directByteBuffer对象作为直接内存的引用进行管理。
  4. 直接内存 不受 堆大小 限制,受到实际内存大小的限制 包括 swap 交换区

三种常量池

全局字符串池 string pool
  1. 类加载的准备阶段之后中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中
  2. HotSpot VM里实现的string pool功能的是一个StringTable类,整个VM全局只有一份
class文件常量池

详见

运行时常量池

详见

对象的创建,内存布局,访问方式

#####实例对象的创建(HotSpot)

  1. 类加载检查:当虚拟机遇到一个new指令时

    1. 检查能否在常量池中定位到 这个对象所属类的 符号引用
    2. 检查该符号引用 所代表的类 是否已经被加载,解析和初始化,若没有进行类加载过程
  1. 为实例对象分配内存,大小在类加载后能够确定,

    1. 分配内存就是在堆中分配一块区域给对象,有以下几种方式:
      1. 指针碰撞:若收集器带有压缩整理,则堆中内存规整,将指示堆空间分解点的指针向后移动相应位置
      2. 空闲列表:若收集器不带有压缩整理,则堆中内存不规整,虚拟机维护一个列表,记录堆中空间利用情况,给新生对象分配内存就是更新这个列表。

    2. 对象创建时的线程安全: 同一时间可能有多个线程,创建对象,上述两种方式中的指针或者空闲列表成为临界资源。保证线程安全的两种方式:

    1. CAS+失败重试 使用硬件支持的CAS(详见多线程设计虚拟机部分)对分配内存空间的动作进行同步处理,保证更新操作的原子性
    2. 将内存分配的工作划分在不同的空间中进行:每个线程拥有一个 TLAB 本地线程分配缓冲 (thread local allocation buffer) (在Eden堆空间中),保证线程安全。只有当TLAB用尽时,进行CAS+失败重试:并分配新的TLAB空间
  2. 初始化零值

    此时内存分配完成,将分配到的空间初始化为默认0值(回忆Java语言,对象不初始化是具有默认值)

  3. 设置对象的对象头

    对象所属的类类的元数据信息,对象的hash codeGC年龄带,是否启用偏向锁等。

  4. 从虚拟机的视角来看,对象已经产生,从Java程序的角度来看,对象创建刚刚开始,此时类的实例构造方法还未执行,接下来执行程序员定义的实例构造方法,初始化对象

#####对象的内存布局

  1. 对象的内存布局 可以分为三块 1. 对象头 2. 实例数据 3. 对齐填充
    1. 对象头包括两个部分: 对象的运行时数据 和 对象的类型指针
      1. 对象的运行时数据: hash code,GC分代年龄,锁状态等。这部分数据和虚拟机的位数有关 如 32位虚拟机中 为32bit,并且采用类似编码的方式,使其根据对象自身状态复用这个空间,以容纳更多信息。
      2. 类型指针,也即对象指向其 类的元数据的指针。多数虚拟机通过这个指针确定对象所属的类。如果对象是数组,还应包括数组长度
    2. 实例数据:也即对象在程序代码中定义的各种字段的内容。
      1. 从父类继承而来的数据 和 子类中定义的数据 都需记录
      2. 存储顺序收到虚拟机分配策略的参数和 字段在Java code 中定义的顺序影响
    3. 对齐填充 padding : 以hotspot为例,要求对象起始地址为8的整数倍,因此需要padding
对象的访问方式:

前面讲述了 在Java虚拟机栈区的局部变量表中存放了 对象的引用。Java虚拟机规范没有规定引用以何种方式定位对象,主要有以下两种:

  1. 通过句柄访问:

对空间中有一个句柄池,引用指向句柄池中的一个句柄,句柄包含两个部分:

​ 1.指向对象实例的指针

​ 2. 指向对象 类型数据的指针

使用句柄的好处在于:在GC过程中,对象内存地址经常变化,而本地变量表中的引用不变,改变的是句柄中的实例数据指针

  1. 通过直接引用访问:

    通过直接引用,本地变量表中的引用直接指向堆中,对象的实例数据,但是为了要获得对象的类型数据,对象实例数据中要包含对象实例数据的指针。

    通过直接引用的方式,最大的好处在于速度,相比于句柄访问的方式(类似于二次间址),减少一次指针定位的开销,整个程序中,Java对象访问非常常见,积少成多,速度客观,是hotspot使用的方式

  2. 此前在对象的内存布局中提到 对象的对象头 不一定包括 类型信息,因为在局部变量表中的引用中,往往可以获得对象的类型信息

  3. 从这里可以看出,Java中的引用 并不 等价于 指针。 在语言层面类似,但实际上指针只是其中一个组成部分

内存溢出 与 内存泄露

  1. 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用
  2. 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
  3. memory leak会最终会导致out of memory!

第三章 垃圾回收 与 内存分配

概述

  1. GC 需要完成的三个问题:

    1. 回收哪些内存 what
    2. 回收内存的时机 when
    3. 回收内存的方式 how
  2. 虚拟机内存区域中:

    1. 本地方法栈,虚拟机栈,程序计数器的内存分配与回收具有确定性
      1. 与线程的诞生和消亡同步进行创建和回收
      2. 本地方法栈的虚拟机栈中的栈帧入栈出栈 与 方法的调用与返回 同步
      3. 栈帧中分配的内存大小在类结构确定下后 就 大致 确定
    2. Java 堆 和 方法区 则 不同
      1. 对于堆 只有在运行时 才知道 会创建哪些(条件语句) 多少(容器)对象
      2. 对于方法区,一个接口具有多个不同的类实现,不同实现占用内存也不同,只有运行时才能确定。

判断对象是否存活

引用计数法
  1. 算法: 给对象添加一个引用计数器,每当有一个地方引用其时计数器值加一,当一个引用失效时计数器值减一,当计数器值为0时表示对象不可用
  2. 优点:简单,效率高
  3. 缺点:无法解决对象之间的引用循环问题
  4. 应用范围:Python等脚本语言使用该方法 Java虚拟机没有采用该方法
可达性分析算法
  1. 算法:虚拟机具有一个GC Roots 对象,将其作为起点,以对象作为节点,对象之间的引用关系作为边,构建一个 引用关系图,从GC Roots 到某个对象的路径成为引用链,当对象和GC Roots之间不可达,也即不存在引用链时,对象死亡,将被回收。
  2. 显然 能够解决 对象间循环引用的问题
  3. 可以作为GC Roots的对象包括(也即可以出现在引用关系图中,也即Java对象)以下几种:
    1. 虚拟机栈(栈帧中本地变量表)中引用的对象/ 一般对象
    2. 方法区中 类静态属性引用的对象/类的静态成员对象
    3. 方法区中 常量 引用的对象/ 常量对象
    4. 本地方法栈中引用的对象
引用的四种类型
  1. 强引用:拥有强引用的对象不会被GC回收,最普遍和一般的引用

    Object obj = new Object();

  2. 软引用:

    1. 在GC回收时,首先回收弱引用,和虚引用,此时若仍没有足够内存,即将产生内存溢出异常,那么GC对拥有软引用的异常进行回收。
    2. 被软引用关联的对象只有在内存不够的情况下才会被回收。
  3. 弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。 不会对其生存时间构成影响。

  4. 虚引用:被虚引用关联的对象一定会被回收,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

  5. 四种引用 强弱程度一次递减

对象回收的两次标记 / 和 软引用能扛过两次GC不同
  1. 在可达性算法中,不可达的对象并不一定被回收,对象被回收需要经过两次标记
  2. 第一次标记:在可达性算法中,对象不可达,与GC Roots没有引用链
  3. 检查对象时候具有finalize()方法(是否有资源在该方法中清理),若有将对象放入 F-Queue中执行finalize()方法(finalize()方法可以用来清理 不是由new 分配的资源)
  4. GC对 F-Queue中对象进行第二次标记。
finalize()自救
  1. 在F-Queue中的对象,具有finalize()方法,在该方法中将对象自身(如this指针)被其他变量引用,能够逃脱被清理,实现自救
  2. 因为GC只会自动调用finalize()方法一次,因此自救只能进行一次。
  3. finalize()类似 C++ 的析构函数,是早期Java对C++的妥协,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。也不要自救
回收方法区 与 类的卸载

方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高

方法区存放 1. 字面量 和 2. 类,接口,字段的符号变量

  1. 常量的回收 : 与堆中 对象的回收 类似

  2. 无用类的回收 也即 判断类的卸载 需要满足三个条件

    1. 类的所有对象都被回收
    2. 该类的类加载器 ClassLoader已被回收
    3. 该类对应的java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

但是满足上述三个条件只是表明可以被回收,但是否被回收还与虚拟机参数有关

垃圾回收算法

mark sweep 标记清除
  1. 算法: 维护一个空闲列表(free list),标记要回收的对象,然后将需要回收的对象加入到空闲列表中

    标记过程就是前述GC二次标记的过程

  2. 缺点:1. 标记清除的效率低 2. 会产生大量碎片,导致无法对大对象分配内存

mark copy 复制
  1. 基本算法:将内存等分为两块,使用其中一块,当内存耗尽时,将仍存活的对象复制到另一块
  2. Eden:将内存等分两块,空间利用率太低
  3. 现代商业虚拟机采用复制算法的变体来回收新生代
  4. 新生代大部分对象存活时间短,因此划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
  5. 如果回收时一块 Survivor 不够用,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
  6. HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%
mark compact 标记整理

基本算法:首先对对象进行标记,将存活对象向一端移动,然后直接清理掉端边界以外的内存。

generational collection 分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般把Java堆分为新生代和老年代。新生代采用复制算法,老年代采用标记整理算法。

垃圾收集器

垃圾回收算法是GC内存回收的方法论,而垃圾收集器则是不同方法论的具体实现。

并不存在最优的垃圾收集器,需要根据使用场景选择合适的垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

  • 串行与并行:串行指的是垃圾收集器使用的线程数为1
  • 并发与非并发:用户线程 与 垃圾回收线程 并发执行,GC时不需要暂停用户线程(但不一定是并行,可能是交替执行),只有CMS和G1是并发的
Serial 收集器
  1. 最基本,历史最悠久单线程非并发收集器,采用 复制算法

  2. hotspot虚拟机在 Client模式下默认的新生代垃圾收集器 。(因为client模式下 新生代大小不是很大,用户线程停顿时间可以接受)

ParNew收集器
  1. 就是serial收集器的 多线程 版本,与serial收集器别无二致,因此 非并发,采用 复制算法

  2. Server 场景下默认的新生代收集器。除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。

parallel scavenge 收集器
  1. 多线程 新生代 复制算法 收集器,“吞吐量优先”收集器,目标是达到一个可控制的吞吐量,吞吐量指 CPU 用于运行用户程序的时间占总时间的比值
  2. 其他收集器的目标往往是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
  3. parallel scavenge 适合 server模式 但是 却不是server 虚拟机的默认收集器 原因在于 不支持和 CMS 同时工作
  4. 能够调整停顿时间和吞吐量两个参数 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
  5. 可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
serial old 收集器
  1. serial 收集器的老年代版本,因此 单线程非并发标记整理算法。
  2. 主要用于 client模式,在server 模式下 能够

    1. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。(不是用parallel old 的原因·在于它是吞吐量优先的收集器)

    2. 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)与 Parallel Scavenge 收集器搭配使用。

parallel old 收集器
  1. Parallel Scavenge 收集器的老年代版本 。多线程 标记整理算法

  2. Parallel Scavenge 搭配使用 于 注重吞吐量以及 CPU 资源敏感的场合

cms收集器
  1. CMS(Concurrent Mark Sweep),第一个支持并发(粗略意义),标记清除,以 停顿时间短 为 目标,适用于 B/S系统 服务端

  2. 支持并发的大致思想是:在用户线程运行时,进行GC链的tracing,标记仍存活对象,然后停止用户线程,修正此前的结果。接着在用户线程运行时,开始清理。分为四个阶段:

    1. 初始标记:标记GC Roots直接能连接的对象。(暂停用户线程,极短时间)
    2. 并发标记:进行GC Roots tracing,标记仍存活对象(并发,占用计算资源)
    3. 重新标记:暂停用户线程,修正 并发标记(时间长于初始标记,但也很短)
    4. sweep 清理:不暂停用户线程,清理(会导致 浮动垃圾)
  3. 缺点:

    1. 并发导致的计算资源敏感,占用CPU,降低吞吐量
    2. 并发导致的浮动垃圾。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。
    3. 标记清除算法导致的大量空间碎片以及对大对象的不友好,当因为空间碎片导致新一次GC时,默认进行内存碎片合并整合
  4. tips:CMS的垃圾标记回收 同 用户线程并发执行,因此需要预留一部分空间给用户线程,不能等到老年代完全用尽再进行GC,如果预留空间无法满足用户线程需要,产生 concurrent mode failure,此时VM 启动后备预案 serial old收集器 进行另一次Full GC。

G1收集器

特点:

  1. 面向服务端应用。
  2. 并行与并发:并行:G1垃圾标记和回收时使用多个线程 并发:从整体上来看,G1在垃圾回收时与用户线程并发
  3. 分代收集:其他收集器只能用于新生代或老年代之一,G1能作用于整个内存空间,并取消新生代和老年代的物理隔离,使用Eden space,Survivor space,Old generation 的三种不同区域 region划分 VM内存。
  4. 空间整合,无空间碎片:从局部来看,在两个region之间, G1采用复制算法,从整体来看 G1采用 标记整理算法,因此不会产生碎片
  5. 停顿可预测,与CMS一样,G1追求降低停顿时间,但是他还能简历可预测的停顿事件模型。(region 的划分 是主要原因,通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。)
  6. 通过 remembered set 记录 当前 region 外的 对象的引用信息,即可保证部队全堆扫描也不会有遗漏

G1垃圾回收的步骤:

  1. 初始标记:标记GC Roots直接能连接的对象。修改 TAMS值

  2. 并发标记:进行GC Roots tracing,标记仍存活对象

  3. 最终标记修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。

  4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 与CMS的区别在于 这一阶段 G1不并发,CMS并发

内存分配 和 回收 策略

Minor GC 和 Full GC:

Minor GC:回收新生代,新生代对象朝生夕死,存活时间段,Minor GC会频繁执行,执行速度也比较快

Major GC/Full GC: 回收老年代,往往伴随新生代的GC。Full GC的速度 比 Minor GC的速度 慢10倍以上+

1. 对象优先在Eden 空间中分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

2. 大对象直接进入老年代

大对象:需要大量 连续内存空间的Java对象,如(长字符串大数组)。

  1. 大对象若在新生代,在Minor GC时,会反复从 Eden 空间 和 Survivor空间中之间复制。大对象直接进入老年代能够避免。-
  2. 大对象直接进入老年代后,如果存活时间短,会导致Full GC的频繁发生。因此编程时 应该 避免存活时间短的大对象创建。
  3. XX:PretenureSizeThreshold 大于此值的对象直接在老年代分配
3.长期存活的对象进入老年代

VM为对象定义年龄计数器,对象每经历过一次 Minor GC 年龄就 +1

当大于 -XX:MaxTenuringThreshold 定义的年龄阈值时,移动至老年代

4.动态对象年龄判定

大于 -XX:MaxTenuringThreshold 定义的年龄阈值 并不是将对象 移入老年代的唯一方式。

如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代。

周老师的书 与实际 是有出入的 只是大体上正确。动态年龄判定的真实情况

5.空间分配担保
  1. 老年代担保:在新生代Minor GC之前,会检查 老年代连续可用内存空间是否大于 新生代对象之和,若大于则担保成功
  2. 如果担保失败:查看HandlePromotionFailure 设置值是否允许担保失败,如果允许,则只需满足 老年代连续可用内存空间大于 历次新生代晋升对象大小和 的均值(用经验值进行估计),进行有风险的担保(可能会Minor GC失败)
  3. 如果担保失败或者 Minor GC失败,则进行 Full GC

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足以及Java 7 之前的永生带不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

Full GC 时间很久,应该避免创建朝生夕死的大对象。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。

4. concurrent mode failure

CMS因为并发 需要 保留一部分老年代空间给用户线程,如果concurrent mode failure 需要调用备案 serial old 收集器,重新 Full GC

第三部分:虚拟机执行子系统

第六章 类文件结构

概述

Java 代码 经过编译 后 生成 在 VM 上执行的 字节码

C++ 代码 经过 编译后 生成 同平台(OS与 CPU指令集)相关本地码

无关性的基础

无关性有两个方面:平台无关性 和 语言无关性

平台无关性:

Java 代码经过 编译后的字节码 可以 运行在 不同 平台上的 JVM上

语言无关性:

语言无关性的基础是虚拟机和字节码存储格式:

  1. 虚拟机不和任何语言绑定,Java规范分为 Java语言规范 和 Java 虚拟机规范

  2. 字节码(类似汇编)本身的语义描述能力强于Java语言或其他语言 且 具有严格的格式约束,因此可以用来表述任何一门功能性语言。

类文件的结构

  1. 任何一个 class文件 唯一对应 一个类或接口的定义 但是 反之不成立,比如 类或者接口 可以通过 类加载器直接生成
  2. class 文件是 以8位字节为单位二进制流,没有任何分隔符
  3. Class 文件中 只有两种 数据类型 : 无符号数 和 表,u1,u2,u4,u8 表示 相应字节数的无符号数
  4. 当出现连续多个同类型数据时,使用一个 前置容器计数器加上 多个数据项 的形式,称之为 该类型的 集合

1.魔数 magic number

  1. class文件的 起始4个字节
  2. 作用:确定是否是一个可被VM接受合法Class文件
  3. 值为: 0xCAFEBABE

2. class 文件的版本

  1. 接下来四个字节是版本号,次版本号(5,6字节) + 主版本号(7,8字节)
  2. Java1.1版本号 为 45 Java 1.2 为46 以此类推 其中Java1.8 为52,Java 11 版本号为55 (Java已经走过十个大版本啦!)
  3. 高版本JDK 向下兼容 低版本 Class 文件

3. 常量池

  1. Class文件中的资源仓库,与其他项目关联最多,占据空间最大的项目之一
  2. 用以存放两类常量:
    1. 字面量 literal :字符串,final常量等
    2. 符号引用(编译原理概念)
      1. 类和接口的全限定名
      2. 方法的名称和描述符
      3. 字段的名称和描述符
  3. 常量池入口为 u2类型的 常量池容量计数器,由1开始计数,0用来表示 :“不引用任何一个常量池项” (其他计数器仍 以0 开始计数)

4. 访问标志

  1. 紧接着的 两个字节,用于标识 类或接口层次访问信息,包括:是类 或者 接口,是否为 public,是否为 abstract,是否为 final等。
  2. 两个字节,每个字节8bit,因此共有16 个标志位可用

5. 类索引,父类索引,接口索引集合

  1. 类以及父类索引是u2 类型数据,接口索引是u2类型数据的集合。
  2. 类索引 确定 类的全限定名
  3. 父类索引 确定 父类的全限定名,(Java 中 除了Object类,都有父类,单根继承体系)
  4. 接口集合 确定 类实现哪些接口 (按照类后面 implements 语句的顺序)

6. 字段表集合

  1. 用于描述接口或者类中声明的变量,包括类级static变量非static实例级变量,以及某些Java代码不存在的字段(内部类为了保持外部类访问性,添加指向外部类实例的字段)不包括方法中的局部变量 以及 继承而来的字段
  2. 字段表的结构 和 所描述的信息
    1. access_flags:访问标志;存放字段的修饰符:如 作用域修饰符 public/protected/private;static修饰符;final修饰符;volatile修饰符;transient修饰符
    2. name_index: 字段简单名称索引,指向常量池中的值
    3. descriptor_index: 描述符索引;描述符 描述 字段的数据类型,使用常量池中常量表示
    4. 属性表集合:描述其他额外信息 如 :final static int number = 100; 则 属性表中包含 100

7. 方法表集合

  1. 方法表集合 和 字段表 集合 结构完全一致,用来描述类中的方法,包括:static和非static 方法,以及某些 Java自动生成的方法 (如类构造器 和 实例构造器),override 后的父类方法不包括**未经重载的父类方法**。
  2. 方法表的结构和描述信息:
    1. access_flags: 访问标志修饰符,相比于字段表集合 增加了一些标志位
    2. name_index: 存放方法的简单名称
    3. descriptor_index:以 (参数列表) 返回值 的形式 存放 参数列表和返回值
    4. 属性表集合:其中的Code属性 存放 方法的代码

8. 属性表集合

  1. Class 文件,字段表,方法表等都能携带自己的属性集合
  2. 属性集合中的属性表 不再要求具有严格顺序,只需 属性名不重复,Java虚拟机只识别其认识的属性
  3. 常见的属性 有 Code;Exceptions(用以列举可能出现的异常 throws)属性等
  4. Java虚拟机执行字节码 基于栈
  5. 异常处理表 是 code 属性中的一项,用以描述如何进行异常处理

全限定名/简单名称/描述符

全限定名:类全名中的. 替换成 / 比如 java.lang.Object 的全限定名 位 java/lang/Object

简单名称: 没有类型及参数修饰的方法以及字段名 比如 int cal(); 方法的简单名称是 cal ;字段 int res 的简单名称是 res

描述符:描述字段的数据类型,方法的参数列表和返回类型(先参数列表 后 返回类型)

初识字节码指令

Java字节码初始:一个Java字节码指令由 一个操作码和n(can be zero)个操作数组成。

  1. 操作码:一个字节,因此Java字节码指令至多有256条
  2. 操作数:Java虚拟机的架构面向 操作数栈,而不是类似汇编面向寄存器,因此大多数指令没有 操作数

缺点:

1. 操作码数量少 
2. 由于class文件编译后操作数长度不对齐,因此处理超过一个字节的操作数时需要重建数据结构。如16位unsigned 用两个 8位unsigned表示 需要重建:`(byte1 <<8)|byte2`,造成性能损失

优点:

  1. 操作数不对齐,省略padding
  2. 操作码数量少,更加精简
  3. 达到小数据量,高效传输,适应Java设计之初面向智能硬件和网络,这是最主要的设计原因

字节码指令的执行模型

类似 汇编:

字节码与数据类型

  1. 在java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,但是 不同数据类型相同操作的指令符可能在虚拟机内部可能是同一段代码实现的。例如iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
  2. 只有部分数据类型具有对应的操作码(只有256个操作码),但是byte、char、short、boolean类型大部分指令没有专门的操作码,实际上是通过转型位int 使用 int型对应的操作码操作。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表操作数栈之间来回传输。

iload/fload/lload 等load指令 :将局部变量 加载 至 操作数栈

istore/fstore/lstore 等store指令:将操作数 存储至 局部变量表

bipush/sipush/ldc 等指令: 将常量 加载到 操作数栈

扩充局部变量表的访问索引: wide

运算指令

  1. 运算或算术指令用于对操作数栈上的值进行某种特定运算,并把结果重新存入操作栈顶

  2. 大体上算术指令可以分为两种:对整型数据和对浮点数据进行运算指令。(由于没有byte、char、short、boolean类型,所以对这类数据的运算应使用int类型指令代替)

类型转换指令

  1. 类型转换指令可以将两种不同的数值类型进行相互转换。
    1. 用于代码中的显式类型转换 (int—>float)
    2. 用于解决之前所属的字节码指令类型和数据类型 无法一一匹配的问题 (byte —> int)
  2. 小范围到大范围类型安全转换(宽化类型转换),无需显式的转换指令,JVM直接支持
  3. 大范围到小范围类型安全转换(窄化类型转换),必须显式的使用转换指令来完成。

对象创建与访问指令

虽然类实例和数组都是对象,但java虚拟机对类实例和数组的创建和操作使用了不同的字节码指令。因为 数组和普通类对象的创建过程不同

操作数栈管理指令

如同操作数据结构中的栈一样,java虚拟机也提供了一些用于直接操作操作数栈的指令,如:

控制转移指令:

作用是使得JVM有条件或者无条件的运行指定位置的指令,而不是运行字节码流中下一条指令。可以简单地理解为:修改PC程序计数器的值。

方法调用和返回指令

  • invokevirtual 指令用于调用对象的实例方法
  • invokeinterface指令用于调用接口方法
  • invokespecial指令用于调用一些需要特殊处理的实例方法(初始化等)
  • invokestatic指令用于调用类方法(static方法)
  • invokedynamic指令用于在运行时动态解析出调用点限定符所使用的方法。

异常处理指令

  1. 显示抛出的指令:(throw 语句 )都由 athrow指令实现
  2. 异常处理:(cache语句)不是由字节码指令实现,由异常表(属性表集合中的code 属性的一部分)完成

同步指令

Java虚拟机可以支持方法级的同步方法内部一段指令序列的同步,这两种同步结构使用管程(Monitor)来支持的。

公有设计与私有实现

Java 虚拟机规范描绘: 1. Class文件格式 2. 字节码指令集

JVM可以(也被鼓励)具有不同的实现,只需其接口遵守这两个规范,内部实现有很大的自由空间,以优化适应不同的场景和目的。

虚拟机实现主要有两种方式:

  1. 虚拟机将Java代码在加载或者执行的过程中, 翻译另一种虚拟机的指令集
  2. 虚拟机将Java代码在加载或者执行的过程中,翻译平台相关本地指令集。 (JIT代码生成技术

第七章 虚拟机的类加载机制

  1. Java中,类型的加载,连接,初始化都是动态的,在程序运行期间完成,以稍微增加一些性能开销的代价换来 天生可以动态拓展的灵活性

  2. 类是在运行期间第一次使用时动态加载的,而不是一次性加载。因为如果一次性加载,那么会占用很多的内存

类的生命周期

  1. 包含以下七个阶段:1 加载 2 验证 3 准备 4 解析 5 初始化 6 使用 7 卸载
  2. 其中 初始化 可以 在 解析之前开始,从而支持 运行时绑定(动态绑定)
  3. 上述顺序 只是开始的顺序 ,并不是完成的顺序,这些阶段往往混合进行

类初始化时机

  1. 类加载的第一个过程: 加载开始的时机没有在虚拟机规范中约束,由不同虚拟机自由把握
  2. 虚拟机规范规定 对类 进行 主动引用时 需要对进行类加载过程中的 初始化阶段

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

1. 主动引用 / 四字诀 new 反 父 主

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):这些场景被称为主动引用

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

  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 Class.forName("com.mysql.jdbc.Driver")

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

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

  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

2. 被动引用 / 三字诀 数 静 常

除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  1. 通过子类引用父类的静态字段不会导致子类初始化

  2. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法

    声明一个类型为A的数组,不会触发类A的初始化,因为生成的对象是数组对象,不是类A的对象,只是数组中存放的类型是A

1
SuperClass[] sca = new SuperClass[10];
  1. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此使用类中的常量不会触发定义常量的类的初始化
1
System.out.println(ConstClass.HELLOWORLD);

类加载过程

包含了加载、验证、准备、解析和初始化这 5 个阶段。

1. 加载 // 类加载相当于 Class 对象的加载,类在第一次主动引用时才动态加载到 JVM 中

加载类加载的一个阶段,注意不要混淆。该阶段使用用户自定义的类加载器
加载过程完成以下三件事:

  1. 通过类的完全限定名称获取定义该类的二进制字节流
  2. 将该字节流表示的静态存储结构转换为方法区的运行时存储结构
  3. 在内存中生成一个代表该类的 Class 对象作为方法区中该类各种数据的访问入口

其中二进制字节流可以从以下方式中获取:

  1. ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  2. 网络中获取,最典型的应用是 Applet
  3. 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  4. 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
  5. 从数据库中读取,比较少见,某些中间件服务器可以选择把程序安装到数据库中完成在集群中的代码分发

2. 验证

  1. 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
  2. 从字节码层面上来看,一些Java代码中非法的事情可以做到,如数组的越界访问,如果不对字节流进行验证,可能会使JVM崩溃。
  3. 加载未结束 就已经开始
  4. 验证阶段主要包括一下四个动作:
    1. 文件格式校验:检验字节流是否符合能被当前版本的虚拟机接受 Class文件格式规范
    2. 元数据验证:对字节码描述的信息/类的元数据信息进行语义分析,以保证描述的信息符合Java语言规范要求
    3. 字节码验证:对数据流和控制流进行分析,确定 程序语义是合法和符合逻辑的
    4. 符号引用验证: 看作是对类自身以外的信息进行匹配性校验

3. 准备

准备阶段为对象的类变量( 被 static 修饰的变量)分配内存(在方法区)并设置初始值(该数据类型下的零值,如 Boolean 为 false)。

  1. 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

  2. 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。

    public static final int value = 123;

4. 解析

将常量池的符号引用替换为直接引用的过程。

其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定

符号引用和直接引用:

符号引用:
  1. 符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定义到目标
  2. 与虚拟机的内存布局无关
  3. 引用的目标不一定已经加载到内存中
  4. 符号引用的字面量形式明确的定义在JVM规范中,因此不同虚拟机所能接受的符号引用是一致的
直接引用:
  1. 直接引用可以是指向目标的指针,相对偏移量 或者是 一个能够直接定位到目标的句柄
  2. 与虚拟机的内存相关
  3. 引用的目标已经加载到内存中
  4. 因为直接引用是虚拟机相关的,因此不通虚拟机上的直接引用一般不同

5. 初始化

  1. 初始化阶段才真正开始执行类中定义的 Java 程序代码。根据程序员通过程序制定的主观计划去初始化类变量和其它资源。(执行类中的静态变量初始化和静态语句块)
  2. 初始化阶段是虚拟机执行类构造器 <clinit >() 方法的过程。在准备阶段,类变量已经赋过默认零初始值
  3. 初始化之前的阶段,除了加载阶段可以用户自定义加载器之外,这些阶段都是由JVM主导。
类构造器<clinit >() 方法:
  1. 编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。
  2. 编译器收集的顺序由语句在源文件中出现的顺序决定
  3. 静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

    1
    2
    3
    4
    5
    6
    7
    public class Test {
    static {
    i = 0; // 给变量赋值可以正常编译通过
    System.out.print(i); // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
    }
  4. 类构造器<clinit >() 不是 类的构造函数类的构造函数是指 实例构造器<init >()方法虚拟机会保证父类的类构造器在子类的类构造器执行前已经执行完毕。(由于单根继承结构,虚拟机中第一个执行类构造器方法的类是java.lang.Object类

  5. 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B); // result is 2 rather 1
}
  1. 只有接口或者类中有 静态变量赋值或者静态语句块时,才具有类构造器<clinit>()方法
  2. 接口不可以使用静态语句块可以有静态变量赋值,因此会生成 <clinit>() 方法。
  3. 但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
  4. 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类加载器

  1. 类加载器完成:通过一个类的全限定名 获取 该类的二进制字符流

  2. 该动作在JVM外进行,因此程序可以自由决定获取方式,也就是自由选取类加载器

类与类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间

这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

类加载器的传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

类加载器分类

从JVM的角度来讲,只存在以下两种不同的类加载器:

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

  • 所有其它类的加载器,使用 Java 实现独立于虚拟机继承自抽象类 java.lang.ClassLoader

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader)负责加载 JVM 运行时核心类,这些类位于 rt.jar 中,我们常用内置库 java.xxx. 都在里面,比如 **java.util.、java.io.、java.nio.、java.lang.* 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」,启动类加载器无法被 Java 程序直接引用*用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可*。
  • 扩展类加载器(Extension ClassLoader)负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)
    • 由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器
    • 负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,*如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    • 是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的

双亲委派模型 Parents Delegation Model

1. 概述
  1. 应用程序是由三种类加载器互相配合从而实现类加载
  2. 除此之外还可以加入自己定义的类加载器
  3. 除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器
  4. 类加载器之间的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)
  5. 几乎应用于所有的Java程序之中,但不是一个强制性的约束模型。

2. 工作过程

一个类加载器首先将类加载请求转发到父类加载器只有当父类加载器无法完成时才尝试自己加载。(因此所有的加载请求最终都被传送到顶层的启动类加载器,如果上层的父加载器无法完成,则将这个加载请求下发到子加载器。请求先由下至上,再由上至下流动)

3. 好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为

1. *无论是加载哪一个Obejct类,首先加载请求会被发送至顶层的bootstrap ClassLoader,那么他会加载在rt.jar中的Object*
2. *rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。*
4. 实现

双亲委派模型对于保证Java程序的稳定运作非常重要,其实现却非常简单,双亲委派模型的代码在 java.lang.ClassLoader.loadClass() 方法中,其运行过程如下:

  1. 先检查类是否已经加载过
  2. 如果没有则让父类加载器去加载
  3. 父类加载器加载失败时抛出 ClassNotFoundException
  4. 此时尝试自己去加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;

// The classes loaded by this class loader. The only purpose of this table
// is to keep the classes from being GC'ed until the loader is GC'ed.
private final Vector<Class<?>> classes = new Vector<>();

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{
return defineClass(name, b, off, len, null);
}
}

自定义类加载器实现

FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

双亲委派模型的三次破坏

第一次破坏:

对于双亲委派模型提出之前的已经存在的用户自定义类加载器代码做出兼容性妥协,在 java.lang.ClassLoader中添加了一个新的protected方法findClass()

第二次破坏:

由于双亲委派模型没有考虑到基础类需要调用用户代码的情况,引入了线程上下文类加载器 克服这个缺陷。 JNDI,JDBC,JCE,JAXB和JBI等都采用这种方式

第三次破坏:

由于用户对于程序动态性的追求,产生了OSGi环境。

程序动态性是指:代码热替换,模块热部署(对于企业软件有很大吸引力)等等。

Class.forName(“class.name”)

  1. 查看类是否已经加载:以ClassLoader和类全名作为key去SystemDictionary 查询类是否存在,如下图所示:

这里写图片描述

  1. 若没有加载则调用loader.loadClass(name)进行类型加载;

https://blog.csdn.net/yangguosb/article/details/77990131

第四部分 虚拟机性能监控与故障处理工具

JDK命令行工具

jps 虚拟机进程状况监控

Java命令参考了 linux命令, jps的功能类似于 ps,即显示正在运行的JVM进程

参数:1568361246491

jstat 虚拟机统计信息监控

https://www.hollischuang.com/archives/481

一般使用jstat -gcutil <pid>:统计gc信息

功能本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据

使用方式:jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

  • Option — 选项,我们一般使用 -gcutil 查看gc情况
  • vmid — VM的进程号,即当前运行的java进程号
  • interval– 间隔时间,单位为秒或者毫秒
  • count — 打印次数,如果缺省则打印无数次

选项

jstat -gcutil <pid>:统计gc信息

1
2
3
4
> [root@vultr ~]# jstat -gcutil 2250
> S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
> 0.68 0.00 65.48 50.05 87.76 85.45 178 1.052 31 3.002 4.054
>

S0 年轻代中第一个survivor(幸存区)已使用的占当前容量百分比 S1 年轻代中第二个survivor(幸存区)已使用的占当前容量百分比 E 年轻代中Eden(伊甸园)已使用的占当前容量百分比 O old代已使用的占当前容量百分比 P perm代已使用的占当前容量百分比 YGC从应用程序启动到采样时年轻代中gc次数 YGCT 从应用程序启动到采样时年轻代中gc所用时间(s) FGC 从应用程序启动到采样时old代(全gc)gc次数 FGCT 从应用程序启动到采样时old代(全gc)gc所用时间(s) GCT 从应用程序启动到采样时gc用的总时间(s)

jstat –class<pid> : 显示加载class的数量,及所占空间等信息

1
2
3
4
> [root@vultr ~]# jstat -class 2250
> Loaded Bytes Unloaded Bytes Time
> 9679 18154.1 1558 3225.2 15.29
>

Loaded 装载的类的数量 Bytes 装载类所占用的字节数 Unloaded 卸载类的数量 Bytes 卸载类的字节数 Time 装载和卸载类所花费的时间

jstat -compiler <pid>显示VM实时编译的数量等信息

1
2
3
4
>[root@vultr ~]# jstat -compiler 2250
>Compiled Failed Invalid Time FailedType FailedMethod
> 8285 1 0 34.77 1 org/springframework/boot/loader/jar/JarURLConnection get
>

Compiled 编译任务执行数量 Failed 编译任务执行失败数量 Invalid 编译任务执行失效数量 Time 编译任务消耗时间 FailedType 最后一个编译失败任务的类型 FailedMethod 最后一个编译失败任务所在的类及方法

jstat -gc <pid>: 可以显示gc的信息,查看gc的次数,及时间

1
2
3
4
>[root@vultr ~]# jstat -gc 2250
>S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC > YGCT FGC FGCT GCT
>8512.0 8512.0 58.2 0.0 68288.0 44661.7 170688.0 85421.8 51200.0 44935.0 6400.0 >5468.6 178 1.052 31 3.002 4.054
>

S0C 年轻代中第一个survivor(幸存区)的容量 (字节) S1C 年轻代中第二个survivor(幸存区)的容量 (字节) S0U 年轻代中第一个survivor(幸存区)目前已使用空间 (字节) S1U 年轻代中第二个survivor(幸存区)目前已使用空间 (字节) EC 年轻代中Eden(伊甸园)的容量 (字节) EU 年轻代中Eden(伊甸园)目前已使用空间 (字节) OC Old代的容量 (字节) OU Old代目前已使用空间 (字节) MC Meta(元空间)的容量 (字节) PU Meta(元空间)目前已使用空间 (字节) YGC 从应用程序启动到采样时年轻代中gc次数 YGCT 从应用程序启动到采样时年轻代中gc所用时间(s) FGC 从应用程序启动到采样时old代(全gc)gc次数 FGCT 从应用程序启动到采样时old代(全gc)gc所用时间(s) GCT 从应用程序启动到采样时gc用的总时间(s)

还有一些其他用法查看参考链接

jinfo Java配置信息工具

https://www.hollischuang.com/archives/1094

以键值对的形式打印出JAVA系统参数及命令行参数的名称和内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
[root@vultr ~]# java -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
[root@vultr ~]# clear
[root@vultr ~]# jinfo 2250
Attaching to process ID 2250, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11
Java System Properties:

java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.221-b11
sun.boot.library.path = /usr/local/java/jdk1.8.0_221/jre/lib/amd64
java.protocol.handler.pkgs = org.springframework.boot.loader
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = :
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level = unknown
sun.java.launcher = SUN_STANDARD
user.country = US
user.dir = /
java.vm.specification.name = Java Virtual Machine Specification
PID = 2250
java.runtime.version = 1.8.0_221-b11
java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
os.arch = amd64
java.endorsed.dirs = /usr/local/java/jdk1.8.0_221/jre/lib/endorsed
line.separator =

java.io.tmpdir = /tmp
java.vm.specification.vendor = Oracle Corporation
os.name = Linux
sun.jnu.encoding = UTF-8
java.library.path = /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
spring.beaninfo.ignore = true
java.specification.name = Java Platform API Specification
java.class.version = 52.0
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
os.version = 3.10.0-229.1.2.el7.x86_64
user.home = /root
user.timezone = UTC
catalina.useNaming = false
java.awt.printerjob = sun.print.PSPrinterJob
file.encoding = UTF-8
java.specification.version = 1.8
catalina.home = /tmp/tomcat.5754194024101937262.8080
user.name = root
java.class.path = /root/onlineIDE/OnlineExecutor/target/online-executor-0.0.1-SNAPSHOT.jar
java.vm.specification.version = 1.8
sun.arch.data.model = 64
sun.java.command = /root/onlineIDE/OnlineExecutor/target/online-executor-0.0.1-SNAPSHOT.jar
java.home = /usr/local/java/jdk1.8.0_221/jre
user.language = en
java.specification.vendor = Oracle Corporation
awt.toolkit = sun.awt.X11.XToolkit
java.vm.info = mixed mode
java.version = 1.8.0_221
java.ext.dirs = /usr/local/java/jdk1.8.0_221/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path = /usr/local/java/jdk1.8.0_221/jre/lib/resources.jar:/usr/local/java/jdk1.8.0_221/jre/lib/rt.jar:/usr/local/java/jdk1.8.0_221/jre/lib/sunrsasign.jar:/usr/local/java/jdk1.8.0_221/jre/lib/jsse.jar:/usr/local/java/jdk1.8.0_221/jre/lib/jce.jar:/usr/local/java/jdk1.8.0_221/jre/lib/charsets.jar:/usr/local/java/jdk1.8.0_221/jre/lib/jfr.jar:/usr/local/java/jdk1.8.0_221/jre/classes
java.awt.headless = true
java.vendor = Oracle Corporation
catalina.base = /tmp/tomcat.5754194024101937262.8080
file.separator = /
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.cpu.isalist =

VM Flags:
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=16777216 -XX:MaxHeapSize=262144000 -XX:MaxNewSize=87359488 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=5570560 -XX:OldSize=11206656 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line:

jmap Java内存映像工具

https://www.hollischuang.com/archives/303

jmap [java memory map] 是获取java 堆内存 映像相关信息的命令:包括

  1. 堆整体使用情况 jmap -heap
  2. 堆中对象的

场景:如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况

步骤:

  • jmap -heap [pid] 查看堆heap的使用情况

  • jmap -histo [pid] 查看堆种对象的数量以及大小

    jmap -histo:live 这个命令执行,只统计存活的对象;JVM会先触发gc,然后再统计信息

  • jmap -dump:format=b,file= [pid] dump JVM堆

  • 使用 jhat命令/ Jprofile可视化工具 查看堆的情况

jhat 虚拟机堆转储快照分析工具

https://www.hollischuang.com/archives/1047

用法:jhat

一般使用 jProfile等现代工具 进行堆栈分析

jstack 虚拟机堆栈跟踪工具

https://www.hollischuang.com/archives/110

jstack命令主要用来查看Java线程的调用栈,可以用来分析线程问题(如死锁)

线程的调用修饰

locked <地址> 目标:使用synchronized申请对象锁成功,监视器的拥有者。

waiting to lock <地址> 目标:使用synchronized申请对象锁未成功,在迚入区等待。

waiting on <地址> 目标:使用synchronized申请对象锁成功后,释放锁幵在等待区等待。

parking to wait for <地址> 目标

线程Dump的分析分析原则:

wait on monitor entry:

  1. 线程状态BLOCKED,线程动作wait on monitor entry,调用修饰waiting to lock一起出现;线程在等待某个资源,资源间存在调用冲突,应该查看代码能否优化
  2. 一个线程锁住某个对象,该对象被多个线程需要,造成多个线程同时阻塞

runnable : 注意IO线程

IO操作是可以以RUNNABLE状态达成阻塞。例如:数据库死锁、网络读写。 格外注意对IO线程的真实状态的分析。 一般来说,被捕捉到RUNNABLE的IO调用,都是有问题的

in Object.wait(): 注意非线程池等待

hsdis jit生成代码反汇编

功能:HotSpot虚拟机JIT编译代码的反汇编插件。我们有了这个插件后,通过JVM参数-XX:+PrintAssembly就可以加载这个HSDIS插件,然后可以把JIT动态生成的那些本地代码还原成汇编代码,然后打印出来

原因:

为什么要做反汇编呢?

当你分析代码运行状况时,通过字节码指令来分析,势必不是最真实的运行细节,因为现在的很多虚拟机的具体实现已经和虚拟机规范相去略远,规范逐渐变成了一个概念模型(只要具体虚拟机实现做出对等的效果就可以了)

Jprofile工具

https://blog.csdn.net/AlbertFly/article/details/78686408

功能:

  1. 加载堆dump
  2. 分析 对象的大小 数量 引用关系等

#参考资料

如果觉得有用的话,打赏我吧~