Java-JVM原理
Java内存模型 JMM
TODO
JVM内存区域

程序计数器:线程私有的,jvm通过改变计数器的值来选取下一条需要执行的字节码指令,唯一一个没有规定任何OutOfMemoryError情况的区域
Java虚拟机栈:线程私有的,每个方法执行时创建栈帧,方法被调用就是栈帧在栈中从入栈到出栈的过程。栈帧的组成部分如下,
- 局部变量表:存放编译期可知的各种jvm基本数据类型、对象引用。todo 待完善
- 操作数栈:
- 动态链接:
- 方法返回地址:
本地方法栈:线程私有的,本地(Native)方法所使用的栈
Java堆:线程共享的,分配内存创建存储对象实例的主要内存区域。在即时编译(JIT)的栈上分配、标量替换等技术下,也可以分配在其他区域
- 分配缓冲区:即TLAB(Thread Local Allocation Buffer),用于优化内存分配速度,避免线程并行导致分配内存冲突而提前为线程预留内存
方法区:线程共享的,也称永久代,用于存储已被jvm加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在jdk8以后被本地内存中实现的元空间(Meta-space)代替
- 运行时常量池:方法区的一部分。Class文件中除了类版本、字段、方法、接口等信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
直接内存:不是jvm运行时数据区的一部分,NIO中使用,基于Channel与Buffer的I/O方式,直接分配堆外内存,避免java堆和Native堆之间的数据赋值,从而显著提升性能
类加载机制
类加载时机
- new实例化类对象,读取或者设置类的静态字段(final修饰编译期把结果放入常量池的静态字段除外),调用一个类的静态方法时
- 对类型进行反射调用
- 当初始化子类的时候,发现父类还未初始化,会初始化父类
- …
类的生命周期
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载:卸载的前提有3个
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
类加载器机制
三层类加载器、双亲委派的类加载架构
类加载器(Class Loader)
- 启动类加载器(Bootstrap Class Loader):C++语言编写,JVM的一部分。责加载存放在
<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中 - 扩展类加载器(Extension Class Loader):负责加载
<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库 - 应用程序类加载器(Application Class Loader):也称为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器
双亲委派模型

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
破坏双亲委派模型
- 线程上下文类加载器
- SPI
- OSGi
类的卸载条件
对象的创建过程
- 常量池中尝试定位类的符号引用,判断类是否被加载、解析和初始化过。如果没有先做类加载
- 为对象分配内存。内存规整使用“指针碰撞”,内存零散使用“空闲列表”。处理线程申请内存的冲突,可以选择使用cas失败重试或者TLAB
- 内存空间初始化为零值,(TLAB方式也可以提前到TLAB分配时进行)
- 初始化对象头信息,类的元数据信息、哈希码、对象的gc分代年龄。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
- 执行类构造函数创建实例
垃圾回收理论
判断对象是否应该被回收
- 引用计数法:引用一次计数+1。存在循环引用的问题,比如map大对象需要嵌套引用关系
- 引用类型
- 强引用
- 软引用
- 弱引用
- 虚引用
- 引用类型
- 可达性分析:通过GC Roots根据引用关系向下搜索,到达不了的对象就是需要被回收的
- 可作为GC Roots的对象:
- 虚拟机栈中引用的对象,比如参数、局部变量、临时变量等
- 类静态属性引用的变量
- 常量引用的对象
- JNI引用的对象
- sychronized同步锁持有的对象
- …
- 可作为GC Roots的对象:
垃圾回收算法
分代收集理论:根据以下理论,一般将jvm堆划分成新生代和老年代,对不通的区域单独进行gc
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾回收的对象就越难被回收
标记-清除算法:标记阶段,标记所有需要回收的对象,回收阶段,统一回收掉所有被标记的对象。但是存在如下缺点
- 当存在大量对象需要被回收时,标记和回收的效率随着对象增多而降低
- 内存空间碎片化,大对象无法分配足够的连续内存,而引发额外的gc动作
标记-复制算法:也叫做复制算法。针对的新生代对象的存亡特征:朝生夕灭。将堆划分成一块Eden空间和两块较小的Survivor空间(默认8:1),每次只用一块Eden空间和一块Survivor空间,每次gc时将存活的对象复制到剩下的一块Survivor空间中,清除Eden空间和使用过的Survivor空间。Survivor空间不足一次gc的时候,一般会用老年代做分配的担保
标记-整理算法:针对老年代独享的存亡特征:难以回收。标记阶段同“标记-清除算法”,清理阶段让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存
注:标记-清除算法所带来的jvm停顿会更小,但是由于内存分配和访问的频率比gc要高,标记-整理算法的吞吐量会更高。所以关注延迟的CMS是基于标记-清除算法的,关注吞吐的Parallel Scavenge是基于标记-整理算法的
回收相关算法细节
- 根结点枚举:方法区比较大,查找GC Root耗时较长。使用OopMap直接存放对象引用位置,提高查找效率
- 安全点:在执行到“长时间执行”的位置生成OopMap,这些位置被称为安全点。这些位置是执行序列复用的地方,比如方法调用、循环跳转、异常跳转
- 线程执行到安全点如何停顿:抢占式中断,一般不用。主动式中断,设置标志位等待线程运行到安全点自己主动挂起
- 安全区域:未分配到时间分片的线程,如sleep或者blocked状态的线程。这种能确保在某一段代码中,引用关系不会发生变化的区域,被称为安全区域
- 记忆集与卡表:新生代和老年代之间可能存在引用关系,为了避免对新生代回收时扫描整个老年代,在新生代中建立了记忆集的数据结构。卡表是记忆集的一种具体实现,定义了记忆集的记录精度,与堆内存的映射关系等,卡表标识的每一个内存块叫卡页,存在跨代引用会将卡页刷脏,脏页在回收时是需要被扫描的
- 写屏障:写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在这个切面上可以维护卡表状态
- 并发的可达性分析:使用三色标记法实现
- 颜色种类
- 白色:对象尚未被垃圾收集器访问过。分析开始所有对象是白色,分析结束只有不可达的对象是白色
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象不可能指向白色对象
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
- 如何避免对象误标记:用户线程和标记线程并发运行,可能标记过后节点是白色但是新增了引用关系应该为黑色,如果不处理,该节点会被错误地回收掉
- 误标记出现条件
- 增加了一条或者多条从黑色对象到白色对象的引用
- 删除了全部从灰色对象到白色对象的直接或间接引用
- 解决方案:(两种方案任选其一)
- 增量更新(Incremental Update):新增黑色指向白色的引用时,记录这个引用关系。并发扫描结束之后,再以黑色对象为根重新扫描一次。CMS的实现方式
- 原始快照(SATB,Snapshot At The Beginning):删除灰色指向白色的引用时,记录这个引用关系。并发扫描结束之后,再以灰色对象为根重新扫描一次。G1、Shenandoah的实现方式
- 误标记出现条件
- 颜色种类
垃圾回收器
垃圾回收器之间的组合关系

Serial收集器

特点:gc时必须暂停其他工作线程直至gc结束
优点:简单高效,额外内存开销小,适用于资源受限的环境。客户端模式下可以使用,比如桌面应用
缺点:停顿时间较长,体验较差
ParNew收集器

特点:Serial收集器的多线程并行版本。除了Serial收集器外,目前只有它能与CMS收集器配合工作
Parallel Scanvenge收集器
特点:新生代收集器,基于标记-复制算法实现,也是能并行收集的多线程收集器。该收集器目标是达到一个可控制的吞吐量。-XX:MaxGCPauseMillis设置最大停顿时间,设置越小相对地gc越频繁,-XX:GCTimeRatio设置gc时间占总时间的比例。除此之外,-XX:+UseAdaptiveSizePolicy可以设置自适应调节,无需指定Eden,Survivor区大小,收集器会自动调节以做到最优吞吐
Serial Old收集器

特点:是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。该收集器的主要意义也是供客户端模式下的JVM使用。服务端模式下,有两种用途,一是JDK 5以及之前的版本中与Parallel Scavenge收集器搭配,二是另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器

特点:Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑这个组合
CMS收集器
特点:一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现的
步骤:
1. 初始标记:标记一下GC Roots能直接关联到的对象,停顿较短
2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图,与用户线程并发
3. 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿较长
4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,与用户线程并发
初始标记、重新标记两步需要“Stop The World”
优点:
- 并发收集,低停顿
缺点:
- 对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分cpu资源而导致应用程序变慢,降低总吞吐量
- 无法处理“浮动垃圾”,可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。CMS运行期间预留的内存无法满足程序分配新对象,即“Concurrent Mode Failure”,需要临时启用Serial Old收集器来重新进行老年代的垃圾收集
浮动垃圾:并发标记和并发清理阶段,用户线程还在继续运行,程序在运行不断产生新的垃圾对象,但这部分垃圾无法在当次GC被标记处理,只好留待下一次GC时再清理。这一部分垃圾就称为“浮动垃圾”
- 收集结束时会有大量空间碎片产生。碎片过多时,往往会出现老年代还有很多剩余空间,但无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。-XX:+UseCMS-CompactAtFullCollection参数可以指定full gc时整理内存碎片,-XX:CMSFullGCsBefore-Compaction参数可以指定进行n次不整合碎片的full gc之后,下次full gc之前先整理碎片
Garbage First收集器(G1)

特点:基于Region的内存布局形式,面向服务端应用的垃圾收集器。建立起“停顿时间模型”,把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region根据需要,扮演新生代的Eden空间、Survivor空间,或老年代空间。将Region作为单次回收的最小单元,后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis),优先处理回收价值收益最大的那些Region。每个Region都维护有自己的记忆集,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作
停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标
步骤:
1. 初始标记:标记一下GC Roots能直接关联到的对象,停顿较短
2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图,与用户线程并发
3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录,停顿较短
4. 筛选回收:更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据期望的停顿时间制定回收计划,把决定回收的部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,涉及存活对象的移动,必须暂停用户线程
G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量
优点:
- 可以指定最大停顿时间、分Region内存布局、收益动态确定回收集合
- 从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,不会产生内存空间碎片
缺点:
- 额外内存占用和执行负载比CMS要高(Region粒度的卡表,以及卡表更复杂的维护)