文章目录
类加载
- 类加载过程
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成)
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
- 类加载器
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要我们自己写的类
- 自定义加载器:负责加载用户自定义路径下的类包
- 双亲委派机制
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性
- 自定义类加载器
- 重写findClass方法
- 打破双亲委派机制
- 重写loadClass方法
- Tomcat打破双亲委派机制
JVM内存模型(JDK1.8)
- 线程私有
- 线程栈
- 栈帧(局部变量表、操作数栈动态链接、方法出口)
- 本地方法栈
- 程序计数器
- 线程栈
- 线程共有
- 堆(JDK1.8默认配置)
- 年轻代
总内存三分之一
- Eden
年轻代内存80%
- 正常新对象在Eden区分配
- Survivor0
年轻代内存10%
- 对象通过Minor GC 拷贝过来
- Survivor1
年轻代内存10%
- 对象通过Minor GC 拷贝过来
- Eden
- 老年代
总内存三分之二
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 对象动态年龄判断,提前进入老年代
- 老年代空间分配担保机制
Full gc
- 年轻代
- 元空间
- 如果默认没有设置最大值,元空间大小会根据GC后动态扩缩容
- 堆(JDK1.8默认配置)
对象创建
内存分配
- 划分内存
- 指针碰撞(Bump the Pointer)
默认用指针碰撞
- 适用于已使用和未使用内存是绝对规整的
标记整理、标记复制
- 分配内存就是把指针向空闲空间那边挪动一段与对象大小相等的距离
- 适用于已使用和未使用内存是绝对规整的
- 空闲列表(Free List)
- 适用于已使用和未使用内存是不规整的存在内存碎片
标记清除
- 虚拟机维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
- 适用于已使用和未使用内存是不规整的存在内存碎片
- 指针碰撞(Bump the Pointer)
- 内存分配并发问题
- CAS
- 失败重试
- 本地线程分配缓冲
- 提前预分配空间
- CAS
- 逃逸分析
栈上分配
- 标量替换
对象在内存中存储的布局
- 对象头(Header)
- 无锁态
- HashCode
- GC分代年龄
- 是否偏向锁
- 锁状态标志
- 偏向锁
- 持有锁线程ID
- 分带年龄
- 是否偏向锁
- 锁状态标志
- 轻量级锁
- 指向栈中锁记录的指针
- 锁状态标志
- 重量级锁
- 指向重量级锁的指针
- 锁状态标志
- GC标记
- 无锁态
- 实例数据(Instance Data)
- 对齐填充(Padding)
指针压缩
- 64位操作系统压缩节约内存空间
- 通过对对象指针的压缩编码、解码方式进行优化使得jvm 只用32位地址就可以支持更大的内存配置(小于等于32G)
- 堆内存小于4G时,jvm会直接去除高32位地址
- 堆内存大于32G时,压缩指针会失效
对象内存回收
- 引用计数法
- 互循环引用问题无法解决
- 可达性分析算法
- GC Roots
- 线程栈的本地变量
- 静态变量
- 本地方法栈的变量等
- GC Roots
- 常见引用类型
- 强引用
- 成为垃圾后可能被GC回收
- 软引用
- GC后空间依然不足,则回收
- 弱引用
- 每次GC都会直接回收
- 虚引用
- 强引用
- finalize()
- 当对象没有覆盖finalize方法,对象将直接被回收
- 一个对象的finalize()方法只会被执行一次,执行后可能会使对象不被回收
垃圾收集
垃圾收集算法
- 标记-复制算法
- 标记-清除算法
- 标记-整理算法
基础垃圾收集器
- Serial收集器
- 单线程
- 全程STW
- 新生代采用复制算法,老年代采用标记-整理算法
- Parallel Scavenge收集器
JDK1.8默认垃圾收集器
- 多线程
- 全程STW
- 新生代采用复制算法,老年代采用标记-整理算法
- 内存4G以下的推荐
- ParNew收集器
- 多线程
- 全程STW
- 新生代采用复制算法,老年代采用标记-整理算法。
- 兼容CMS收集器
CMS收集器
- 垃圾收集流程
- 初始标记 STW: 并记录下gc roots直接能引用的对象,速度很快。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程
- 重新标记 STW: 修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。主要用到 三色标记 里的 增量更新算法
- 并发清理: GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象不会被垃圾清理,会存在没被回收的浮动垃圾
- 并发重置:重置本次GC过程中的标记数据
- 在上一次垃圾回收还没执行完,然后垃圾回收又被触发(Full GC),此时会进入STW,用serial old垃圾收集器来回收
- CMS的相关核心参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92%)
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc
- 三色标记
- 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有直接引用都已经被扫描过
- 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过
- 白色: 表示对象尚未被垃圾收集器访问过,若在分析结束的阶段,仍然是白色的对象, 即代表不可达(垃圾)
- 内存4-8G推荐ParNew+CMS
G1收集器JDK1.9默认
- 可预测的停顿时间
核心卖点
- 设置太短会导致频繁GC,浪费计算性能
- 设置太长对用户体验不友好
- 一般200ms附近
- G1将Java堆划分为多个大小相等的独立区域
最多2048
- G1保留了年轻代和老年代的概念,物理上是可以 不连续 的Region的集合
- Region的区域功能 可能会动态变化
- G1有专门分配 大对象的Region叫Humongous区,一个大对象如果太大,可能会横跨多个Region来存放
超过了一个Region大小的50%就是大对象
- Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收
- 垃圾收集流程
有一点类似CMS
- 初始标记 STW: 同CMS的初始标记
- 并发标记:同CMS的并发标记
- 最终标记 STW:同CMS的重新标记
- 筛选回收 间歇性STW:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
逻辑上就是在规定时间内释放更多的空间
- 回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次
- GC方式
- YoungGC:Eden区放满后会计算当前Eden区回收大概时间,如果回收时间远远小于设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC
- MixedGC:老年代的堆占有率达到参数设定的值则触发,回收所有的 Young和部分Old区,以及大对象区(此处会去尽力满足设置的期望停顿时间值)。复制拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
- Full GC:停止系统程序,然后进行标记、清理和压缩整理
这就是灾难
- G1的相关核心参数
- -XX:+UseG1GC:使用G1收集器
- -XX:ParallelGCThreads:指定GC工作的线程数量
- -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
- -XX:G1MaxNewSizePercent:新生代内存最大空间
- -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%)
- 内存大于8G推荐
ZGC收集器JDK11 出现 目前尚在实验阶段
- 支持TB量级的堆
- 最大GC停顿时间不超10ms
- 奠定未来GC特性的基础
- 最糟糕的情况下吞吐量会降低15%
- 不分代
目前是这样的
- Region区有大中小之分
- 小型Region: 容量固定为2MB, 用于放置小于256KB的小对象
- 中型Region: 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象
- 大型Region: 容量不固定, 可以动态变化, 但必须为2MB的整数倍,用于放置4MB或 以上的大对象,每个大型Region中只会存放一个大对象
- NUMA-aware (Non Uniform Memory Access Architecture):每个CPU对应有一 块内存,且这块内存在主板上是离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了
-
- 垃圾收集流程
- 并发标记:初始标记 (Mark Start)和最终标记(Mark End)会出现短暂的停顿,ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
- 并发预备重分配:统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集
- 并发重分配:重分配集的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个 转发表(Forward Table),记录从旧对象到新对象的转向关系
此时如果有用户线程访问被重分配的对象会通过读屏障修正指针
- 并发重映射:该流程因为有读屏障存在所以并不是迫切的需要执行,所以合并到了下一次垃圾收集循环中的并发标记阶段里去完,这样合并就节省了一次遍历对象图的开销
但是这样也导致转发表(Forward Table)在垃圾收集完成后不能立即删除
- 垃圾收集流程
- 颜色指针
- 每个对象指针中都会有两个辅助GC的mark标识
- GC把对象移动了,读屏障也会发现并修正指针
不需要STW
- 目前实验阶段不推荐使用,如果真要用应该是超大的内存场景因为ZGC会存在大量的浮动垃圾
读、写屏障
- 写屏障
- 增量更新(Incremental Update)
- 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象
- 并发扫描结束后进行重新扫描
- 原始快照(Snapshot At The Beginning,SATB)
- 灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来
- 并发扫描结束后进行重新扫描,使其在本轮GC中使其存活下来
- 增量更新(Incremental Update)
- 读屏障
- 对象指针是Good Color:指针有效正常往下执行
- 对象指针是Bad Color:指针无效,立即根据对应的转发表记录将访问转发到新复制的对象上,并同时修正该引用的值,使其直接指向新对象。
跨代引用
- 记忆集
- 记录从非收集区到收集区的指针集合,避免把整个 老年代加入GCRoots扫描范围
- 卡表
hotspot实现方式
- 用一个字节数组,每个元素对应内存区域一块特定大小的内存块
- 只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,也就是变脏
- 写屏障维护卡表状态