《Java虚拟机》学习简记

时间:2022-03-03 09:51:36

开始学习 Java虚拟机的原理,参考《深入理解Java虚拟机 第二版》周志明一书和“chjttony”博友所总结的简版学习笔记《深入理解java虚拟机》学习笔记,边学习边记忆,好记性不如烂笔头,记录如下。

一、Java内存模型—原书第2章

  1. 内存模型总览

    《Java虚拟机》学习简记

    其中方法区和堆是线程共享内存,Java虚拟机栈、本地方法栈以及PC是线程私有区域。

  2. 程序计数器
    • 当前线程的字节码行号指示器
    • 控制程序的分支、循环、跳转、异常处理及线程恢复等基础功能
    • Java虚拟机的多线程是通过线程轮流切换并分配处理器时间片来实现
    • 是唯一一个没有规定OOM的内存区域
  3. Java虚拟机栈
    • 生命周期和线程相同
    • 描述了Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧来存放局部变量表操作数栈动态链接方法出口等信息,当前正在执行的方法成为当前帧,一个方法的执行过程,其实就是方法帧入栈和出栈的过程
    • 局部变量表存放了:基本数据类型数据、对象引用、方法返回地址,而且局部变量表的内存在进入方法时是完全确定的,不会动态修改
    • *Error异常:线程请求的深度大于虚拟机所允许的最大深度
    • OutOfMemoryError异常:虚拟机栈动态扩展时没有可用内存时
  4. 本地方法栈
    • 类似Java虚拟机栈,区别是 本地方法栈是为操作系统的本地方法服务
    • HotSpot虚拟机将两者合二为一

    • 对象实例及数组对象的内存都在此分配,也是GC的主要区域
    • OutOfMemoryError
  5. 方法区
    • 存储已被虚拟机加载的类信息常量静态变量类的Class对象引用等信息
    • 运行时常量池:存放编译期生成的各种字面变量符号引用直接引用
    • OutOfMemoryError
  6. 直接内存
    • 不属于JVM的运行时数据区
    • NIO可以只要Native方法直接分配对外内存,然后使用DirectByteBuffer进行引用。

二、HotSpot虚拟机的对象–原书第2.3章节

  1. 对象创建
    • 遇到new指令时,首先去方法区的常量池里查找该类的符号引用,并坚持该类的类型数据是否已经被加载、解析和初始化过;如果没有则进行类加载;
    • 类加载通过后,在堆中为对象分配内存:指针碰撞法、空闲列表法
    • 对属性进行初始化,赋初值
    • 设置对象头
    • 执行方法
  2. 对象的内存布局
    • 对象头:
    • 存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标识、线程持有的锁的名号
    • 类型指针:对象指向它的类元数据的指针,虚拟机通过该指针来确定对象是哪个类的实例
    • 实例数据
    • 填充数据
  3. 对象的访问定位:Java栈的本地变量表里存储着对对象实例的引用(reference),引用可能指向句柄,也可能直接指向实例地址
    • 句柄
    • 直接访问

三、垃圾回收算法

垃圾回收算法要解决三个问题:
1.哪些内存需要回收?
2.什么时候回收?:内存不足,引发GC动作
3.如何回收

对于第一个问题,有如下两种方法:

  • 引用计数算法:对象被引用+1,引用失效-1,当引用为0时,被标记为可回收
    • 优点:实现简单,效率很高
    • 缺点:难以解决对象之间的相互循环引用问题
  • 可达性分析算法:以“GC Roots”为起点,向下搜索,走过的路径称为引用链,如果一个对象到GC Roots没有引用链,则可以标记为可回收。可以作为GC Roots的对象:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中静态变量引用的对象
    • 方法区中常量引用的对象
    • JNI引用的对象

对于第三个问题,算法如下:

  1. 标记-清除算法:第一步,根据可达性分析,标记处所以需要回收的对象;第二步,回收标记的对象内存
    缺点:

    • 效率不高,无论是标记还是回收;
    • 产生空间碎片,容易再次引起GC动作
  2. 复制算法:把内存分为相同的两块儿,每次使用其中一块,标记完成后,把存活的对象复制到另一块内存中
    优点:

    • 提高了效率
    • 解决了内存碎片化问题
      缺点:内存浪费严重
  3. 改进的复制算法:改进了复制算法的空间浪费,将内存分为8:1:1的三份,分别命名为Eden、Survivor From和Survivor To,其中的Eden+Survivor两块内存用于分配内存和GC,标记以后,把存活的对象复制到另一块儿Survivor的内存上,加入Survivor内存不够存放这些存活的对象,则向老年代进行分配担保。 适用于新生代这种绝大部分对象都是朝生暮死的情况,对于老年代这种就不适用了。

  4. 标记-整理算法:类似 标记-清除 算法,与之不同的是,标记之后不是直接清除,而是将存活的对象向一端移动,然后清除掉边界以外的内存。

  5. 分代收集算法:即对象存活率高的地方,如老年代,使用标记-整理算法,称为Major GC/Full GC;对象存活率低的地方,如新生代,使用改进的复制算法,称为Minor GC

HotSpot的算法实现

垃圾收集器

  1. Serial收集器:最古老的单线程收集器,作用于新生代,使用复制算法;工作时需暂停其他所有工作线程;可以与CMS收集器配合使用
    • 优点:单CPU或客户端时,简单而高效
    • 缺点:在多核CPU中表现不好
  2. ParNew收集器:Serial的多线程版本,作用于新生代,可与CMS收集器配合使用
  3. Parallel Scavenge收集器:类似ParNew的多线程收集器,作用于新生代;关注点在于系统的吞吐量,使用最大停顿时间和吞吐量大小两个参数来控制;GC的自适应调节策略。
    • 停顿时间越短就越是和与用户交互的程序,提升用户体验
    • 高吞吐量可以高效地利用CPU时间,尽快完成计算
  4. Serial Old收集器:单线程的收集器,作用于老年代,用于标记-整理算法。
  5. Parallel Old收集器:注重吞吐量的多线程收集器,作用于老年代,可以和Parallel Scavenge配合使用
  6. CMS收集器:Concurrent Mark Sweep收集器,注重于缩短回收停顿时间;使用标记-清除算法,步骤如下:

    • 初始标记:标记GC Roots可以直关联到的对象,速度很快;需要Stop The World
    • 并发标记:标记引用链,并发执行,不需要暂停
    • 重新标记:修正并发标记期间的变动;Stop The World
    • 并发清除:耗时最长,并发执行

    优点:并发收集、低停顿
    缺点

    • 对CPU资源敏感:降低系统吞吐量
    • 无法收集浮动垃圾:并发收集过程中,用户现场会产生新的垃圾,被称为“浮动垃圾”,无法再本次收集过程中进行清除,而出现Concurrent Mode Failure导致再次Full GC;阈值68%的时候启动收集,可调节,不宜太高;
    • 基于标记-清除算法,导致内存碎片无法为大对象分配内存而再次Full GC;合并整理参数;
  7. G1收集器:Garbage First,1.7正式商用。
    特点:

    • 并行与并发:多线程,低停顿
    • 分带收集:独立管理老年代和新生代
    • 空间整理:整体上看是 标记-整理算法;从局部(两个Region之间)看是 复制 算法,不会产生空间碎片
    • 可预测的停顿

    内存结构:将堆内存分为多个大小相等的独立区域(Region),保留新生代和老年代的概念,但物理上是不再隔离了,是一部分Region的集合;可以有计划地避免全区域的GC,使用Remembered Set来记录引用信息,避免全堆扫描。
    步骤:

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

内存分配和回收策略

  1. 对象优先在Eden分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判断
  5. 空间分配担保

四、虚拟机类加载机制–原书第7章

  1. 加载过程解析
  2. 类加载器
    • 类和类加载器:对于任意一个类,都需要由加载它的类加载器和类本身一起来确定这个类在JVM中的唯一性
    • 双亲委派模型:使用组合关系而非继承来确定父子关系和复用父类加载器的代码;加载请求都先交给父类加载器进行加载,如果父类加载器表示无法加载,子类加载器才尝试加载,这样会保证一种具有优先级的层次关系。越基础的类由越上层的类加载器进行加载

五、JIT

六、Java内存模型和线程并发

  1. 内存模型:线程只和自己对于的工作内存交互,对于变量的读取和写入由工作内存和主存进行交互
  2. 内存见交互操作(原子操作)
    • lock(锁定):作用于主存变量,把一个变量标志为一条线程独占的状态
    • unlock(解锁):
    • read(读取):作用于主存的遍变量,把一个变量的值从主存中传输到线程的工作内存,以便load动作使用
    • load(载入):作用于工作内存的变量,把read读取的变量放入工作内存的变量副本(拷贝)中
    • user(使用):作用于工作内存的变量,把工作内存中的变量的值传递给执行引擎
    • assign(赋值):作用于工作内存的变量,把执行引擎的值赋值给工作内存中的变量
    • store(存储):作用于工作内存的变量,把工作内存中的变量的值传递到主存中,以便后续的write操作使用
    • write(写入):作用于主存的变量,把store的变量放入主存的变量中
  3. volatile关键字:轻量级锁,强制读取主存变量的值,并强制立即写回主存

七、线程安全和锁优化