JVM

215 阅读9分钟

类加载

1.1 jvm中类加载过程

  • 概述:类加载过程就是把class文件加载到内存(运行时数据区)内

  • 过程:加载、连接、初始化,其中链接可以进一步划分为验证、准备、解析

  • 加载:通过类的全限定名获取到类的二进制字节流,转化为方法区的运行时数据区

  • 验证:验证类的格式符合虚拟机要求,包括文件格式验证、元数据验证、字节码验证、符号引用验证

  • 准备:给类变量分配内存,进行零值(默认)初始化

  • 解析:把符号引用转化为直接引用

  • 初始化:执行类的构造器方法clinit(),该方法时编译器自动生成,内容是类变量和静态代码块

1.1.1 类加载过程的顺序

  • 基本顺序是按照加载、验证、准备、解析、初始化
  • 解析可能会发生在初始化之后

1.1.2 验证哪些内容?

  • 包括四种验证:文件格式验证、元数据验证、字节码验证和符号引用验证

1.1.3 final修饰的类变量在准备阶段怎么处理?

  • 显式初始化

1.1.4 符号引用和直接引用

  • 符号引用是指描述引用目标的符号,符合可以是任何字面量,只要能够找到被引用目标即可
  • 直接引用是指能够直接指向目标的指针、句柄或者偏移量

1.2 双亲委派机制

  • JVM的类加载器自上而下包括启动类加载器(bootstrap classloader)、扩展类加载器(extension classloader)、系统类加载器(application classloader)

  • 类加载器在加载类的时候,会把请求委托给父类加载器;依次向上直到启动类加载器;父类加载器不能加载,则类加载器才会尝试自己去加载,这种就是双亲委派机制

  • 好处是避免类的重复加载;保护核心api

    JVM判断两个class对象是否相同的条件

    • 类的全限定名必须相同
    • 加载类的ClassLoader必须相同

1.3 类的主动使用(导致类的初始化)

  • 共计七种情况:

  • 创建类的实例

  • 访问类的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射

  • 初始化一个类的子类

  • 启动类

  • 动态语言支持

《深入理解Java虚拟机 第三版》p263

运行时数据区

1.1 介绍运行时数据区

  • JVM把内存划分为5个部分:程序计数器、虚拟机栈、本地方法栈、堆和方法区

  • 前三者是线程私有的,后两者是线程共享的

  • 随着JDK版本迭代,发生变化最大的是方法区

    • JDK6之前,方法区的实现是永久代
    • JDK7,把静态变量和字符串常量池移到了堆
    • JDK8,改用元空间来实现方法区,静态变量和字符串常量池仍然保留在堆里
  • 除了JVM划分的5部分内存,还有直接内存

1.2 程序计数器作用

  • 程序计数器记录了当前线程所执行的字节码的行号指示器

  • 字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令

2.2.1 为什么程序计数器要设置为线程私有的

  • 程序计数器记录了每个线程运行的行号
  • 多线程下会频繁进程线程切换,切换后需要根据程序计数器来找到执行的位置

1.3 虚拟机栈介绍

  • 虚拟机栈描述的是方法的执行

  • 栈中保存的是栈帧,每个方法的调用就对应栈帧入栈,方法的结束就对应栈帧出栈

  • 两种异常:StackOverFlow和OutOfMemory

    • StackOverFlow:虚拟机栈大小固定,请求栈容量大于最大容量

    • OutOfMemory:可动态扩容,扩容失败且请求量大于最大容量

2.3.1 栈帧的组成结构

  • 包括局部变量表、操作数栈、方法返回地址、动态链接和一些附加信息

  • 局部变量表:

    • 数据结构为数组,存储的是方法的参数和定义在方法内的局部变量
    • 局部变量表大小是在编译期确定的,执行期间不会改变局部变量表大小
    • 局部变量的基本单位是slot(槽)
  • 操作数栈:

  • 方法返回地址

  • 动态链接

  • 一些附加信息

2.3.2 设置栈的大小

  • -Xss:栈帧的最大内容

字符串常量池

垃圾回收

1. 垃圾回收算法

  • 共计4种,标记-清除、标记-复制、标记-整理和分代收集算法

  • 标记清除:分为两个阶段,标记阶段和清除阶段,该算法效率不高,会出现碎片

  • 标记复制(s0和s1采用的):将内存分为两部分,回收时把存活对象复制到另一块空间,该算法需要两倍空间,但不会出现碎片

  • 标记整理:两个阶段,标记存活的对象,把存活的对象整理到内存的一端,清理另一端死去的对象

  • 分代收集:针对不同的生命周期对象,采用不同的收集方式

1.1 标记算法(如何判断对象存活还是死亡?)

  • 常见的有两种方式:引用计数法和可达性分析

  • 引用计数法

    • 每个对象保存一个引用计数器,有对象引用了该对象就加一;引用失效就减一
    • 引用计数器减到0,对象没有被使用,可以进行回收
  • 可达性分析(Java采用的)

    • 从GC Roots开始,根据引用关系从上到下搜索,搜索路径称为引用链

    • 对象没有引用链和GC Roots相连,则对象可回收

1.1.1 引用计数法的问题

  • 无法解决循环引用问题
  • 循环引用是指两个对象互相引用着对方,导致引用计数器都不为零,无法进行垃圾回收

1.1.2 可以作为GC Roots的对象

(记忆顺序:程序计数器、虚拟机栈、本地方法栈、堆、方法区)

  • 虚拟机栈引用的对象
  • 本地方法栈中引用的对象
  • 方法区中的静态变量、常量引用的对象
  • 加synchronized锁的对象
  • 虚拟机内部的引用:基本类型的class对象、异常对象、系统类加载器
  • 临时加入作为GC Roots:收集新生代,老年代就可以作为GC Roots

1.2 finalization机制

  • 作用:在销毁对象之前可以自定义处理逻辑,对象垃圾回收之前会先调用对象的finalize方法

  • finalize方法一般进行资源的释放,比如文件关闭、数据库连接关闭等

1.2.1 引入finalization机制后,对象的状态

  • 三种状态:可到达的、可复活的、不可到达的
  • 可到达的:在引用链上的对象
  • 可复活的:不在引用链上,但有finalization机制可能复活
  • 不可到达的:不在引用链上,没有finalization或者使用finalization也没有复活

1.2.2 引入finalization后,对象回收过程

  • 可达性分析后,不在引用链上,进行第一次标记
  • 重写了finalized方法,且没有执行过,则将对象插入到F-queue队列中
  • 在finalization过程中对象与引用链上的对象建立联系,则对象不会被回收,否则则会被回收
  • finalized只会被调用一次

1.3 标记清除的清除

  • 不是删除原有内存空间内容
  • 是将地址保存在空闲列表中

1.4 分配内存空间方式

  • 指针碰撞
  • 分配空间就是修改指针的偏移量

2.垃圾回收相关概念

2.1 内存溢出和内存泄漏

  • 内存溢出是指内存不足没有空闲内存可以用来分配
  • 内存泄漏是指没有被使用的对象一直不被GC回收

2.2 STW

  • 全称为Stop-The-World,是指在GC过程中,引用程序线程被暂停,感觉好像整个程序都停下来了

  • 会导致STW的事件

    • 可达性分析枚举GC Roots过程

2.3 引用

  • 分为四种:强软弱虚,引用强度依次降低
  • 强引用:不回收,内存不足会报异常
  • 软引用:内存不足会回收
  • 弱引用:发现即回收
  • 虚引用:可以看作没有,仅仅用来在回收之后得到一个通知

3.垃圾回收器

  • 共7种经典的垃圾回收器

  • 串行回收器:Serial、Serial Old

  • 并行回收器:ParNew、Parallel Scavenge、Parallel Old

  • 并发回收器:CMS、G1

3.1 介绍Serial收集器

  • 最早的垃圾收集器,用于新生代垃圾回收
  • Serial Old用于老年代垃圾回收
  • 采用的标记-复制算法,串行回收

3.2 介绍ParNew收集器

  • Serial的多线程版本,也适用于新生代收集
  • 采用标记-复制算法,并行回收

3.3 介绍Parallel Scavenge收集器

  • 和ParNew相似,采用标记-复制算法
  • 吞吐量优先
  • 提供了Parallel Old用于老年代回收,替代Serial Old,采用标记-整理算法

3.4 介绍CMS收集器

  • CMS(Concurrent Mark Sweep),是并发回收器

  • 采用标记-清除算法,也会导致STW

  • 用于老年代收集

3.4.1 CMS收集过程

  • 分为4个阶段:初始标记、并发标记、重新标记、并发清除
  • 初始标记:标记出GC Roots直接关联的对象,耗时很短,此时会导致STW
  • 并发标记:从直接关联对象遍历整个对象图,耗时很长,但不会暂停用户线程,用户线程和垃圾收集线程并发执行
  • 重新标记:修正并发标记阶段,用户线程运行导致标记变动的那部分,也会导致STW,耗时比初始标记要长,但远低于并发标记
  • 并发清除:清除标记为已经死亡的对象,和用户线程并发执行

3.4.2 CMS的缺点

  • 会产生内存碎片
  • 占用CPU资源,导致用户线程变慢
  • 无法处理浮动垃圾

3.5 介绍G1收集器

  • // 待完成

执行引擎