JVM笔记
[TOC]
一 走近java
1.1 java技术体系发展趋势
二 自动内存管理机制
2.1 虚拟机区域划分,内存溢出常见原因
2.1.1 运行时数据区域
程序计数器
可以看作是当前线程所执行的字节码的行号指示器
线程私有
如果正在执行java方法,记录的是虚拟机字节码指令的地址;如果正在执行的是Native方法,即java程序调用C程序的方式,计数器值为空。
唯一不存在OutOfMemoryError的内存区域
虚拟机栈:服务于java方法
描述java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
方法调用到执行完成的过程,对应一个栈帧在虚拟机入栈到出栈的过程。
局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用和returnAddress类型(指向了一条字节码指令的地址)。
- 局部变量表所需空间在编译器完成分配
- long和double占用2个局部变量空间(slot),其余只占用一个
- 运行期间不改变局部变量表大小
虚拟机栈不可动态拓展:线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError
虚拟机栈可动态拓展==大多可拓展==:拓展时无法申请到足够内存,抛出OutOfMemoryError
本地方法栈:服务于Native方法
类似虚拟机栈
java堆
存放实例对象
虚拟机启动时创建
线程共享
虚拟机内存中最大的一块
垃圾收集器管理的主要区域,又称“GC堆”
可动态拓展(-Xmx和-Xms控制)
没有内存完成实例分配,且堆无法再拓展,抛出OutOfMemoryError
方法区
存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
线程共享
JVM[^hotSpot] 使用永久带实现,有上限(-XX:MaxPermSize)
无法满足内存分配需求时,抛出OutOfMemoryError
运行时常量池
方法区的一部分,用于存放编译期生成的各种字面量和符号引用
符号引用+翻译出来得直接引用
可动态拓展,无法申请内存时,抛出OutOfMemoryError
直接内存
虚拟机之外的系统内存
NIO操作中,引入一种基于通道(channel)与缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配的堆外内存。然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。==避免了在java堆和Native堆中来回复制数据==,显著提高性能
容易被忽略的内存空间。当虚拟机各区域内存总和大于物理内存限制,或剩余直接内存过小,会导致动态拓展失败出现OutOfMemoryError
2.2 JDK提供的几种垃圾收集算法,验证自动内存分配及回收的主要规则
GC的三个目标:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
2.2.1 对象是否死亡
- 引用计数法
- 有地方引用它,计数器值加1
- 引用失效,计数器减1
- 计数器为0对象不能再被使用
- 无法解决对象循环引用问题,内存泄漏
- 可达性分析算法
- 可作为GC Roots的对象包括
- 虚拟机栈(本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- java引用概念的扩充
- 强引用
- 常见对象创建赋给变量属于此类,存在强引用就不会回收
- 软引用
- 有用但非必须
- 内存溢出前会尝试回收这部分空间
- 弱引用
- 非必须对象
- 只能生存到下一次垃圾收集发生前
- 虚引用
- 不对对象生存时间构成影响
- 目的:对象呗回收时,收到一个系统通知
- 强引用
- 回收方法区
- 可以回收,回收效率低
- 废弃常量
- 无用的类
- 该类所有实例已回收
- 该类的ClassLoader已回收
- 对应Class对象没有被任何地方引用,无法通过反射访问
- 常见大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader场景。需要卸载类,保证永久带不溢出
2.2.2 垃圾收集算法
- 标记-清除算法(Mark-Sweep)
- 标记、清除过程效率不高
- 产生大量不连续内存碎片,导致之后分配较大对象时,无法找到足够的连续内存。提前触发另一次垃圾收集操作
- 复制算法
- 将内存分为相等的两块,当一块的内存用完,就将还存活的对象复制到另一块上面;然后再把使用过的内存一次清理掉
- 目的:提高效率;内存空间连续
- 缺点:只能使用一半内存空间,浪费
- 优化:将内存分割为一块较大的eden和两块较小的survivor。
- HotSpot虚拟机默认Eden : Survivor = 8 : 1,即只有10%的内存空间被“浪费”
- Survivor空间不够时,将使用老年代内存空间分担
- 标记-整理算法
- 目的:解决“复制算法”极端情况下,对象100%存活问题。
- 为老年代设计
- 类似“标记-清除”算法,只是清除后,存活对象往一边移动,空出另一侧
- 分代收集算法
- 划分新生代、老年代
- 新生代:每次都有大批对象死去,“复制算法”
- 老年代:对象存活率高、没有额外空间为其分担,“标记-清理” 或者 “标记-整理”
2.2.3 HotSpot的算法实现
- 每局根节点
- 可达性分析时,为保证 “一致性” 。所有java执行线程将暂停,又称“stop the world”
- 优化:用一组OopMap数据结构保存,哪些地方存放着对象引用
- 安全点
- 所有线程到达安全点才能开始GC
- 安全区域
- 针对sleep、blocked状态的线程
2.2.4 垃圾收集器
2.2.5 g1收集器
2.2.6 理解gc日志
2.2.7 垃圾收集器参数总结
2.2.8 内存分配与回收策略
- 对象优先在Eden分配
- 新生代 = Eden + Survivor
- 空间不够时,发起GC
- 打印内存回收日志,-XX:+PrintGCDetails
- 大对象直接 进入老年代
- 很长的字符串以及数组
- -XX:PretenureSizeThreshold,大于该阀值的对象直接进入老年代
- 长期存活的对象将进入老年代
- 为每个对象定义一个年龄(Age)计数器
- 对象在Eden出生,经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,设置对象年龄为1。
- 对象在Survivor中每“熬过”一次Minor GC,年龄就加1。默认达到15,就会被晋升到老年代中。
- -XX:MaxTenuringThreshold设置几岁晋升老年代
- 动态对象年龄判定
- Survivor空间中,相同年龄的所有对象大小总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就可以直接进入老年代
- 空间分配担保
- Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象之和;如果成立,那么Minor GC是安全的;如果不成立,虚拟机查看参数(HandlePromotionFailure)设置是否允许担保失败。
- 如果允许担保失败,那么继续检查老年代最大可用连续空间是都大于历次晋升到老年代对象平均大小;如果大于,尝试一次Minor GC,失败Full GC;如果小于,直接Full GC
- 如果不允许担保失败,直接Full GC
2.3 虚拟机性能监控与故障处理工具
2.3.1 JDK命令行工具
- jps:jvm process status tool
- 显示指定系统内所有hotspot虚拟机进程
- jstat:jvm statistics monitoring
- 用于收集hotspot虚拟机各方面运行数据
- jinfo:configuration info for java
- 显示虚拟机配置信息
- jmap:memory map for java
- 生成虚拟机的内存转储快照(heapdump文件)
- jhat:jvm heap dump browser
- 用于分析heapdump文件,建立一个web服务,让用户通过浏览器查看分析结果
- jstack:stack trace for java
- 显示虚拟机的线程快照(threaddump文件)
2.4 故障处理、调优实战
三 虚拟机执行子系统
3.1 Class文件组成,各部分定义、数据结构、使用方法;演示储存于访问
3.2 类加载过程:加载、验证、准备、解析、初始化;类加载器工作原理及其对虚拟机的意义
比较两个类是否相等:
- 类限定名相同
- 类加载器相同
类加载器双亲委派模型:
- 从当前类加载器出发
- 委派给父类加载器加载(从bootstarp classloader开始寻找)
- 最后到当前类加载器,一旦找到就返回
破坏双亲委派模型:
- 兼容JDK1.2以前版本
- 线程上下文类加载器(thread context classloader)
- 创建线程时未设置,从父线程继承
- 应用程序全局范围未设置,则默认为application classloader
- 用于SPI,例如JNDI、JDBC等
- OSGI(模块化)
- 通过替换类加载器,实现可插拔
3.3 虚拟机在执行代码时如何找到正确的方法,如何执行方法内的字节码,以及执行代码时涉及的内存结构
3.4 实战 + 类加载及执行子系统案例
四 程序编译与代码优化
4.1 分析java语言中泛型、主动装箱和拆箱、条件编译等语法糖;实战演示如何使用插入式注解处理器实现一个程序命名规范的编译器插件
4.2 虚拟机热点探测方法、hotSpot即时编译、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。常见编译优化技术
五 高效并发
5.1 虚拟机内存模型结构及操作,以及原子性、可见性和有序性在java内存模型中的体现;先行发生原则的规律及使用,线程在java语言中是如何实现的
5.1.1 内存模型
- 处理器对输入代码,乱序执行优化
- 虚拟机编译时,指令重排序优化
主内存与工作内存
- 线程对变量的所有操作,都必须在工作内存中完成
- 不同线程,无法直接访问对方工作内存中的变量
内存间交互操作:原子的、不可再分的
主内存与工作内存
lock:作用于主内存,把一个变量标志位一条线程独占
unlock:作用于主内存,把一个变量解锁,其他线程可访问
read:作用于主内存,从主内存读到工作内存,便于随后load
load:作用于工作内存,从主内存得到的变量值放入工作内存副本
use:作用于工作内存,把工作内存中变量传递给执行引擎
assign:作用于工作内存,执行引擎的变量赋值给工作内存
store:作用于工作内存,把工作内存中变量传递给主内存,便于随后write
write:作用于主内存,把store操作传递的变量写入主内存
要完成变量复制,就要顺序执行(read、load)(store、write)操作。
然而,java内存模型只要求顺序执行,不是连续执行。所以,指令之间会插入其他指令。
内存交互规则
对于volatile型变量的特殊规则
- 保证可见性:每次读拿到的都是最新值
- 不保证原子性:写操作可能相互覆盖
- 禁止指令重排:设置内存屏障,后面指令不能排到屏障之前
- 使用场景:
- 运算结果不依赖当前值
- 能够保证只有单一的线程改变变量值
- 变量不需要与其他状态变量共同参与不变约束
- ==规则==:
- 保证read、load、use连续一起出现(每次使用变量都必须从主内存刷新)
- 保证assign、store、write连续一起出现(每次修改变量后立即同步回主内存)
- 禁止指令重排
原子性、可见性、有序性
- 原子性:操作中不会插入其他指令
- 可见性:一个线程修改了共享变量,其他线程立即知晓
- 有序性:本线程内有序;一个线程观察另一个线程表现为无序(指令重排,工作内存与主内存同步引起)。使用volatile和synchronized保证有序性
先行发生原则
- 程序次序规则:代码依次执行
- 管程锁定规则:同一个锁,unlock发生于lock之前
- volatile变量规则:volatile变量写操作,发生于后面读操作之前
- 线程启动规则:thread的start()方法,发生于线程每一个动作之前
- 线程终止规则:线程中所有操作,发生于对此线程终止检测之前
- 线程中断规则:线程interrupt()方法,发生于检测线程中断代码之前
- 对象终结规则:一个对象的初始化,发生于它的finalize()之前
- 传递性:A先于B,B先于C;则A先于C
5.2 线程安全、同步实现的方式以及虚拟机底层运作原理;虚拟机实现高效并发所采取的一系列锁优化措施
5.2.1 线程的实现:CPU调度的基本单位
- 使用内核线程实现
- 直接由操作系统内核支持的线程
- 每个内核线程可视为一个内核分身
- 内核通过调度器(scheduler)调度线程,分配到各个处理器
- 程序一般不直接 使用内核线程,而是使用内核线程的一种高级接口–轻量级进行(light weight process, LWP)
- 内核线程与轻量级进程之间 1 : 1的线程模型
- 局限性
- 线程操作都需要进行系统调度,代价较高:需要在用户态、内核态中来回切换
- 消耗内核资源(内核线程栈空间)
- 支持线程数有限
- 用户线程实现
- 完全建立在用户空间的线程库上
- 线程操作完全在用户态完成,低消耗
- 线程与用户线程之间 1:N
- 局限性
- 线程创建、切换、调度都需要用户程序自己处理
- 阻塞处理、多处理器跟线程映射 解决困难
- 程序复杂
- 用户线程+轻量级进程混合实现
- 线程创建等在用户线程
- 线程调度、处理器映射等在轻量级进程
- java线程的实现
- 基于操作系统原生线程模型实现
- 不同平台不一致
- 对于sun JDK来说,win,linux中均为一对一模型,即一条java线程映射到一条轻量级进程
5.2.2 java线程调度
- 协同式线程调度
- 抢占式线程调度(java使用的)
- 线程执行时间系统控制
- 不会一个线程导致整个进程阻塞
- 通过设置线程优先级,可以倾斜资源分配
5.2.3 状态转换
- new:新建,未启动
- runnable:正在执行、或等待系统分配资源
- waiting:无限期等待,直到被其他线程唤醒
- timed waiting:限期等待,一定时间后自动唤醒
- blocked:阻塞,等待锁释放
- terminated:线程终止状态
5.2.4 线程安全的实现方法
- 互斥同步:悲观锁
- 非阻塞同步:乐观锁(发现不一致,重试)
5.2.5 锁优化(JDK完成)
- 自旋锁与自适应自旋
- 场景:共享数据的锁只会持续很短时间,挂起、回复线程并不值得
- 并发请求同一个 锁,后一个线程为了占有处理器执行时间,让线程执行一个忙循环,称为“自旋锁”
- -XX:+UseSpinning参数开启,JDK1.6默认开启
- -XX:PreBlockSpin参数设置自旋次数,默认10次
- 自适应自旋
- 根据前一次在同一个锁上自旋时间及锁拥有者状态来决定
- 如果,同一个锁对象上,自旋等待刚刚获得成功,并且持有锁的线程正在运行中,那么判定这次自旋也很有可能成功
- 如果,某个锁,自旋很少获得过成功。那以后获取这个锁,很可能省略掉自旋过程。
- 锁消除
- 编译器检测到不可能存在共享数据竞争的锁进行消除
- 例如:方法内局部变量,使用了stringBuffer
- 锁粗化
- 扩大锁范围
- 例如:循环中不断加锁、解锁,会将锁范围扩大到循环外
- 轻量级锁
- 记录在对象(锁对象)头部
- 通过CAS操作,设置失败,且锁不归属当前线程,则升级为重量级锁
- 经验判断:绝大部分锁,在整个同步周期内,不保存在竞争。避免使用重量级锁,减少开销
- 偏向锁
- 消除数据在无竞争情况下的同步
- 相比于轻量级锁,连CAS都不做
- -XX:+UseBiasedLocking,默认开启
- 通过在对象头设置偏向锁标记,CAS记录第一个获取到锁的线程ID实现
- 一旦另外一个线程获取锁,则偏向锁宣告结束
参考信息
[^hotSpot]: JDK中JVM的默认实现,java -version查看.