开始学习 Java虚拟机的原理,参考《深入理解Java虚拟机 第二版》周志明一书和“chjttony”博友所总结的简版学习笔记《深入理解java虚拟机》学习笔记,边学习边记忆,好记性不如烂笔头,记录如下。
一、Java内存模型—原书第2章
-
内存模型总览
其中方法区和堆是线程共享内存,Java虚拟机栈、本地方法栈以及PC是线程私有区域。
- 程序计数器
- 当前线程的字节码行号指示器
- 控制程序的分支、循环、跳转、异常处理及线程恢复等基础功能
- Java虚拟机的多线程是通过线程轮流切换并分配处理器时间片来实现
- 是唯一一个没有规定OOM的内存区域
- Java虚拟机栈
- 生命周期和线程相同
- 描述了Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧来存放局部变量表、操作数栈、动态链接、方法出口等信息,当前正在执行的方法成为当前帧,一个方法的执行过程,其实就是方法帧入栈和出栈的过程
- 局部变量表存放了:基本数据类型数据、对象引用、方法返回地址,而且局部变量表的内存在进入方法时是完全确定的,不会动态修改
- *Error异常:线程请求的深度大于虚拟机所允许的最大深度
- OutOfMemoryError异常:虚拟机栈动态扩展时没有可用内存时
- 本地方法栈
- 类似Java虚拟机栈,区别是 本地方法栈是为操作系统的本地方法服务
- HotSpot虚拟机将两者合二为一
- 堆
- 对象实例及数组对象的内存都在此分配,也是GC的主要区域
- OutOfMemoryError
- 方法区
- 存储已被虚拟机加载的类信息、常量、静态变量、类的Class对象引用等信息
- 运行时常量池:存放编译期生成的各种字面变量、符号引用、直接引用等
- OutOfMemoryError
- 直接内存
- 不属于JVM的运行时数据区
- NIO可以只要Native方法直接分配对外内存,然后使用DirectByteBuffer进行引用。
二、HotSpot虚拟机的对象–原书第2.3章节
- 对象创建
- 遇到new指令时,首先去方法区的常量池里查找该类的符号引用,并坚持该类的类型数据是否已经被加载、解析和初始化过;如果没有则进行类加载;
- 类加载通过后,在堆中为对象分配内存:指针碰撞法、空闲列表法
- 对属性进行初始化,赋初值
- 设置对象头
- 执行方法
- 对象的内存布局
- 对象头:
- 存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标识、线程持有的锁的名号
- 类型指针:对象指向它的类元数据的指针,虚拟机通过该指针来确定对象是哪个类的实例
- 实例数据
- 填充数据
- 对象的访问定位:Java栈的本地变量表里存储着对对象实例的引用(reference),引用可能指向句柄,也可能直接指向实例地址
- 句柄
- 直接访问
三、垃圾回收算法
垃圾回收算法要解决三个问题:
1.哪些内存需要回收?
2.什么时候回收?:内存不足,引发GC动作
3.如何回收
对于第一个问题,有如下两种方法:
- 引用计数算法:对象被引用+1,引用失效-1,当引用为0时,被标记为可回收
- 优点:实现简单,效率很高
- 缺点:难以解决对象之间的相互循环引用问题
- 可达性分析算法:以“GC Roots”为起点,向下搜索,走过的路径称为引用链,如果一个对象到GC Roots没有引用链,则可以标记为可回收。可以作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- JNI引用的对象
对于第三个问题,算法如下:
-
标记-清除算法:第一步,根据可达性分析,标记处所以需要回收的对象;第二步,回收标记的对象内存。
缺点:- 效率不高,无论是标记还是回收;
- 产生空间碎片,容易再次引起GC动作
-
复制算法:把内存分为相同的两块儿,每次使用其中一块,标记完成后,把存活的对象复制到另一块内存中。
优点:- 提高了效率
- 解决了内存碎片化问题
缺点:内存浪费严重
改进的复制算法:改进了复制算法的空间浪费,将内存分为8:1:1的三份,分别命名为Eden、Survivor From和Survivor To,其中的Eden+Survivor两块内存用于分配内存和GC,标记以后,把存活的对象复制到另一块儿Survivor的内存上,加入Survivor内存不够存放这些存活的对象,则向老年代进行分配担保。 适用于新生代这种绝大部分对象都是朝生暮死的情况,对于老年代这种就不适用了。
标记-整理算法:类似 标记-清除 算法,与之不同的是,标记之后不是直接清除,而是将存活的对象向一端移动,然后清除掉边界以外的内存。
- 分代收集算法:即对象存活率高的地方,如老年代,使用标记-整理算法,称为Major GC/Full GC;对象存活率低的地方,如新生代,使用改进的复制算法,称为Minor GC
HotSpot的算法实现
垃圾收集器
- Serial收集器:最古老的单线程收集器,作用于新生代,使用复制算法;工作时需暂停其他所有工作线程;可以与CMS收集器配合使用
- 优点:单CPU或客户端时,简单而高效
- 缺点:在多核CPU中表现不好
- ParNew收集器:Serial的多线程版本,作用于新生代,可与CMS收集器配合使用
- Parallel Scavenge收集器:类似ParNew的多线程收集器,作用于新生代;关注点在于系统的吞吐量,使用最大停顿时间和吞吐量大小两个参数来控制;GC的自适应调节策略。
- 停顿时间越短就越是和与用户交互的程序,提升用户体验
- 高吞吐量可以高效地利用CPU时间,尽快完成计算
- Serial Old收集器:单线程的收集器,作用于老年代,用于标记-整理算法。
- Parallel Old收集器:注重吞吐量的多线程收集器,作用于老年代,可以和Parallel Scavenge配合使用
-
CMS收集器:Concurrent Mark Sweep收集器,注重于缩短回收停顿时间;使用标记-清除算法,步骤如下:
- 初始标记:标记GC Roots可以直关联到的对象,速度很快;需要Stop The World
- 并发标记:标记引用链,并发执行,不需要暂停
- 重新标记:修正并发标记期间的变动;Stop The World
- 并发清除:耗时最长,并发执行
优点:并发收集、低停顿
缺点:- 对CPU资源敏感:降低系统吞吐量
- 无法收集浮动垃圾:并发收集过程中,用户现场会产生新的垃圾,被称为“浮动垃圾”,无法再本次收集过程中进行清除,而出现Concurrent Mode Failure导致再次Full GC;阈值68%的时候启动收集,可调节,不宜太高;
- 基于标记-清除算法,导致内存碎片无法为大对象分配内存而再次Full GC;合并整理参数;
-
G1收集器:Garbage First,1.7正式商用。
特点:- 并行与并发:多线程,低停顿
- 分带收集:独立管理老年代和新生代
- 空间整理:整体上看是 标记-整理算法;从局部(两个Region之间)看是 复制 算法,不会产生空间碎片
- 可预测的停顿
内存结构:将堆内存分为多个大小相等的独立区域(Region),保留新生代和老年代的概念,但物理上是不再隔离了,是一部分Region的集合;可以有计划地避免全区域的GC,使用Remembered Set来记录引用信息,避免全堆扫描。
步骤:- 初始标记:
- 并发标记:
- 最终标记:
- 筛选回收:
内存分配和回收策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判断
- 空间分配担保
四、虚拟机类加载机制–原书第7章
- 加载过程解析
- 类加载器
- 类和类加载器:对于任意一个类,都需要由加载它的类加载器和类本身一起来确定这个类在JVM中的唯一性
- 双亲委派模型:使用组合关系而非继承来确定父子关系和复用父类加载器的代码;加载请求都先交给父类加载器进行加载,如果父类加载器表示无法加载,子类加载器才尝试加载,这样会保证一种具有优先级的层次关系。越基础的类由越上层的类加载器进行加载
五、JIT
六、Java内存模型和线程并发
- 内存模型:线程只和自己对于的工作内存交互,对于变量的读取和写入由工作内存和主存进行交互
- 内存见交互操作(原子操作)
- lock(锁定):作用于主存变量,把一个变量标志为一条线程独占的状态
- unlock(解锁):
- read(读取):作用于主存的遍变量,把一个变量的值从主存中传输到线程的工作内存,以便load动作使用
- load(载入):作用于工作内存的变量,把read读取的变量放入工作内存的变量副本(拷贝)中
- user(使用):作用于工作内存的变量,把工作内存中的变量的值传递给执行引擎
- assign(赋值):作用于工作内存的变量,把执行引擎的值赋值给工作内存中的变量
- store(存储):作用于工作内存的变量,把工作内存中的变量的值传递到主存中,以便后续的write操作使用
- write(写入):作用于主存的变量,把store的变量放入主存的变量中
- volatile关键字:轻量级锁,强制读取主存变量的值,并强制立即写回主存