书籍学习-深入理解Java虚拟机(一)

本文是个人学习书籍《深入理解Java虚拟机》过程中所记录的一些笔记,内容来源于书籍

自动内存管理机制

Java内存区域与内存溢出异常

运行时数据区域

  • 线程共享

    • 方法区

      • 类信息、常量、静态变量、即时编译后的代码

      • Java7前实现是永久代(PermGen),Java8后实现是元空间(Metaspace)

      • OutOfMemoryError

        • 无法满足内存分配需求
      • 对象实例

      • OutOfMemoryError

        • 没有内存完成实力分配且无法再扩展
    • 直接内存(不是虚拟机规范中定义的内存区域)

  • 线程私有

    • 虚拟机栈

      • 栈帧

        • 局部变量表

          • 64位long和double类型的数据会占用2个局部变量空间(Slot)
        • 操作数栈

        • 动态链接

        • 方法出口

      • StackOverflowError

        • 线程请求的栈深度大于虚拟机所允许的深度
      • OutOfMemoryError

        • 扩展时无法申请到足够的内存
    • 本地方法栈

      • HotSpot将其与虚拟机栈合在一起
  • 程序计数器

    • 如果线程正在执行的是Java方法,记录的是正在执行的虚拟机字节码指令的地址

    • 如果线程正在执行的是Native方法,记录值为未定义(Undefined)

HotSpot虚拟机对象探秘

  • 对象的创建

    • 检查常量池是否有类的符号引用、检查引用代表的类是否已被加载、解析和初始化过

    • 以上检查不通过时,执行类加载过程

    • 为新生对象分配内存

      • 指针碰撞:用过的内存放在一边,空闲的内存放在另一边

      • 空闲列表:维护一个列表,记录哪些内存块是可用的

    • 初始化零值

    • 设置对象头

    • 执行方法

  • 对象的内存布局

    • 对象头

      • 对象运行时数据(Mark Word)

        • 哈希码

        • GC分代年龄

        • 锁状态标志

        • 线程持有的锁

        • 偏向线程ID

        • 偏向时间戳

      • 类型(类元数据)指针

    • 实例数据

    • 对齐填充

  • 对象的访问定位

    • 句柄:reference存储的是到对象的句柄地址

      • 到对象实例数据的指针

      • 到对象类型数据的指针

    • 直接指针:reference存储的是到对象的直接地址(HotSpot使用这种)

  • 实战OutOfMemoryError异常

    • Java堆溢出(-Xms、-Xmx)

      • -XX:+HeapDumpOnOutOfMemoryError:出现内存溢出异常时Dump出当前的内存堆转储快照

      • 通过内存映像分析工具对快照进行分析,确认是出现内存泄漏还是内存溢出

      • 如果是内存泄漏,可进一步查看泄漏对象到GC Roots的引用链

      • 如果是内存溢出,检查堆参数是否可以调大或尝试减少程序运行时的内存消耗

    • 虚拟机栈和本地方法栈溢出(-Xss)

      • 栈深度超过虚拟机所允许的最大深度,抛出StackOverflowError

      • 建立过多线程导致OutOfMemoryError,在不能减少线程数的情况下,可通过减少最大堆和减少栈容量来换取更多的线程

    • 方法区和运行时常量池溢出(-XX:PermSize、-XX:MaxPermSize、-XX:MetaspaceSize、-XX:MaxMetaspaceSize)

      • 大量字符串添加到常量池(Java7前)

      • 动态生成大量的Class

    • 本地直接内存溢出(-XX:MaxDirectMemorySize)

      • DirectByteBuffer

      • unsafe.allocateMemory

      • Heap Dump文件中不会看见明显的异常,且Dump文件较小

垃圾收集器与内存分配策略

对象存活

  • 引用计数算法

    • 给对象添加一个引用计数器,当有地方引用它时,计数器值加1,当引用失效时,计数器值减1

    • 很难解决对象间循环引用问题

  • 可达性分析算法

    • 通过一系列“GC Roots”对象为起始点向下搜索,所走过的路径称为引用链,当一个对象到”GC Roots”没有任何引用链相连时,则此对象是不可用的

    • 可作为GC Roots的对象

      • 虚拟机栈(栈帧中的本地变量表)中引用的对象

      • 方法区中类静态属性引用的对象

      • 方法区中常量引用的对象

      • 本地方法栈中JNI引用的对象

  • 引用

    • 强引用:不会被垃圾收集器回收

    • 软引用:当将要发生内存溢出时,垃圾收集器会二次回收这些对象

    • 弱引用:当垃圾收集器工作时,都会回收这些对象

    • 虚引用:无法通过引用获取对象实例,唯一的目的是能在这个对象被垃圾收集器回收时收到一个系统通知

  • 生存还是死亡

    • 宣告一个对象死亡至少需要经过两次标记过程,当没有GC Roots引用链时会进行第一次标记,执行finalize方法(对象覆盖finalize方法且虚拟机未调用过)后会进行第二次标记,如果对象被重新关联到GC Roots上,则不用回收
  • 回收方法区

    • 回收废弃常量和无用的类

    • 无用的类

      • 类所有实例都已被回收

      • 加载该类的ClassLoader已被回收

      • 类对应的Class对象没有在任何地方被引用

垃圾收集算法

  • 标记-清除算法

    • 首先标记出所有需要回收的对象,标记完成后进行统一回收

    • 不足点

      • 效率问题

      • 空间问题:内存碎片

  • 复制算法(为解决标记-清除的效率问题)

    • 将可用内存分成大小相等的两块,每次只使用其中的一块,当这一块内存用完后就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉
  • 标记-整理算法

    • 与标记-清除类似,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界外的内存
  • 分代收集算法

垃圾收集器

  • Serial + Serial Old

    • 单线程,新生代采用复制算法,老年代采用标记-整理算法,用于Client模式(桌面应用)较多
  • ParNew

    • 简单理解为多线程版本的Serial收集器,用于新生代,常与CMS搭配使用
  • Parallel Scavenge + Parallel Old

    • 关注点为达到一个可控的吞吐量,而不是停顿时间
  • CMS(Concurrent Mark Sweep)

    • 基于标记-清除算法实现,可分为4个步骤

      • 初始标记(STW)

        • 标记GC Roots能直接关联到的对象
      • 并发标记

        • 进行GC Roots Tracing的过程
      • 重新标记(STW)

        • 修正并发标记期间因用户线程导致标记变动的那一部分对象的标记记录
      • 并发清除

    • 缺点

      • 对CPU资源敏感

      • 无法处理浮动垃圾(垃圾出现在标记过程之后,无法在当次收集中处理),可能出现”Concurrent Mode Failure”失败而导致另一次Full GC的产生(当CMS运行期间预留的内存无法满足程序需要时,就会出现一次”Concurrent Mode Failure”失败,这时虚拟机将临时启用Serial Old来重新进行老年代的垃圾收集)

        • -XX:CMSInitiatingOccupancyFraction设置老年代使用了多少(百分比)空间后触发CMS收集
      • 空间碎片

        • -XX:+UseCMSCompactAtFullCollection设置CMS在进行Full GC时开启内存碎片的合并整理,默认是开启的

        • -XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的Full GC后,跟着来一次带压缩的,默认值是0,表示每次都进行

  • G1

    • 特点

      • 并行与并发

      • 分代收集

      • 空间整合

        • 整体看是基于标记-整理算法,局部(两个Region之间)看是基于复制算法
      • 可预测的停顿

        • 在后台建立一个Region优先列表,每次根据允许的收集时间,优先回收价值最大的Region
    • 步骤

      • 初始标记(STW)

        • 标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top to Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象
      • 并发标记

        • 从GC Roots开始对堆中对象进行可达性分析,找出存活的对象
      • 最终标记(STW)

        • 修正在并发标记中因用户程序继续运行而导致标记产生变动的那一部分标记记录
      • 筛选回收(STW)

        • 对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划

内存分配与回收策略

  • 对象优先在Eden分配

    • 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  • 大对象直接进入老年代

    • **-XX:PretenureSizeThreshold **设置内存大于这个值的对象直接在老年代分配,只对Serial和ParNew两款收集器有效
  • 长期存活的对象进入老年代

    • **-XX:MaxTenuringThreshold **设置对象年龄大于这个值的对象晋升到老年代
  • 动态对象年龄判定

    • 如果在Survivor空间中相同年龄所有对象大小综合大于Survivor空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代
  • 空间分配担保

    • 老年代最大可用的连续空间 > 新生代所有对象总空间

    • 老年代最大可用的连续空间 > 历次晋升到老年代对象的平均大小

虚拟机性能监控与故障处理工具

JDK的命令行工具

  • jps:虚拟机进程状况工具

    • 可以列出正在允许的虚拟机进程,并显示虚拟机执行主类名以及传递给主类的参数或启动时的JVM参数
  • jstat:虚拟机统计信息监视工具

    • 用于监视虚拟机各种运行状态信息,如类加载、内存、垃圾收集、JIT编译等
  • jinfo:Java配置信息工具

    • 实时地查看和调整虚拟机各项参数
  • jmap:Java内存映像工具

    • 用于生成堆转储快照,或查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的哪种收集器等
  • jhat:虚拟机堆转储快照分析工具

  • jstack:Java堆栈跟踪工具

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

JDK的可视化工具

  • JConsole:Java监视与管理控制台

    • 基于JMX的可视化监控、管理工具
  • VisualVM:多合-故障处理工具

虚拟机执行子系统

类文件结构

无关性的基石

  • 虚拟机和字节码存储格式

    • 平台无关性

    • 语言无关性

Class类文件的结构

  • 采用类C语言结构体的伪结构来存储数据,只有两种数据类型

    • 无符号数:属于基本的数据类型,以u1/u2/u4/u8表示n字节的无符号数,用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串值

    • 表:由多个无符号数或其他表作为数据项构成的复合数据类型,以”_info”结尾,用来描述有层次关系的复合结构的数据

  • 魔数与Class文件的版本

    • Class文件头4个字节为魔数,用来确定这个文件是否为一个能被虚拟机接受的Class文件

    • 魔数后4个字节为Class文件的版本号(次版本号+主版本号)

  • 常量池

    • 主要存放两大类常量:字面量和符号引用
  • 访问标志

    • 常量池后2个字节为访问标志,用于识别一些类或接口层次的访问信息,如Class是类还是接口,是否public、abstract、final等
  • 类索引、父类索引与接口索引集合

    • 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用来描述这个类实现/继承了哪些接口
  • 字段表集合

    • 字段表用于描述接口或类中声明的变量(类变量+实例变量,不会列出从父类继承来的变量),每一项由字段修饰符+字段的简单名称+字段/方法的描述符(+属性表集合)组成
  • 方法表集合

虚拟机类加载机制

类加载的时机

  • 类加载生命周期

    • 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
  • 必须立即对类进行”初始化”的情况

    • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时

    • 使用java.lang.reflect包的方法对类进行反射调用时

    • 当初始化一个类时发现父类还没有初始化,则需先初始化父类

    • 用户指定的执行主类(main方法的那个类)

    • 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄对应的类没有进行初始化,则需先触发

  • 类加载的过程

    • 加载

      • 通过类全限定名获取二进制字节流

      • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构

      • 在内存(HotSpot存放在方法区)生成一个Class对象,作为方法区这个类的各种数据的访问入口

    • 验证(-Xverify:none可以关闭大部分的类验证)

      • 确保Class文件的字节流中包含的信息符合当前虚拟机的要求

      • 验证动作

        • 文件格式验证(基于二进制字节流进行)

          • 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
        • 元数据验证(基于方法区的存储结构进行)

          • 对字节码描述的信息进行语义分析,以保证符合Java语言规范的要求
        • 字节码验证(基于方法区的存储结构进行)

          • 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
        • 符号引用验证(基于方法区的存储结构进行)

          • 对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
    • 准备

      • 正式为类变量分配内存并设置类变量初始值(零值)的阶段,这些类变量所使用的内存都将在方法区中进行分配
    • 解析

      • 将常量池内的符号引用替换为直接引用的过程
    • 初始化

      • 执行类构造器方法的过程

      • 方法的细节

        • 由编译器自动收集所有类变量的赋值动作和静态语句块中的语句合并产生,静态语句块中只能访问定义在它之前的变量,不能访问但可赋值定义在它之后的变量

        • 虚拟机会保证在执行子类的方法前,父类的方法已经执行完毕

        • 如果一个类没有静态语句块也没有对类变量的赋值操作,编译器可以不为这个类生成方法

        • 执行接口(接口的实现类)的方法不需要先执行父接口的方法,只有当父接口中定义的变量使用时,父接口才会初始化。

        • 虚拟机会保证一个类的方法在同一类加载器下只会执行一次

类加载器

  • 类和类加载器

    • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一类加载器,都拥有一个独立的类名称空间
  • 双亲委派模型

    • 系统提供的类加载器

      • 启动类加载器

        • 加载<JAVA_HOME>/lib目录(或-Xbootclasspath参数指定的路径)下虚拟机所识别(通过文件名识别)的类库
      • 扩展类加载器

        • 加载<JAVA_HOME>/lib/ext目录(或java.ext.dirs系统变量指定的路径)下的所有类库
      • 应用程序(系统)类加载器

        • 加载用户类路径(Classpath)下的所有类库
    • 双亲委派模型(非强制性的约束模型)

      • 除顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里的父子关系一般是以组合的关系来复用父加载器的代码

      • 双亲委派工作流程:一个类加载器收到类加载的请求时,会把这个请求委派给父类加载器去完成,只有当父类加载器反馈无法完成加载请求时,子加载器才会尝试自己去加载

      • 自定义类加载器(遵循双亲委派模型):继承ClassLoader类,重写findClass方法

    • 破坏双亲委派模型

      • 自定义类加载器(破坏双亲委派模型):继承ClassLoader类,重写loadClass方法

      • ServiceLoader使用线程上下文加载器去加载SPI代码(即父类加载器请求子类加载器完成类加载)

加载评论