类加载
类加载的时机,类加载的过程,每一阶段大致做的事
类加载的时机
New 对象的时候,调用静态变量,静态方法的时候、子类被加载的同时也会加载父类,main方法所在的类、反射调用
类加载的过程
加载、链接(验证、准备、解析),初始化
加载主要是将二进制流读取到jvm中
验证是验证class文件的版本、基本语法条件等,准备是给类变量分配内存并赋予默认值,如果有些被定义为final的类变量就直接赋值了,解析是将一些符号引用转变为直接地址引用。
初始化主要是执行一些init方法, 主要是类的静态代码块
类加载器是什么,分哪几种,双亲委派模型是什么
类加载器就是执行类加载功能的组件, 负责将class文件二进制流转换为jvm中的类对象。
主要分为引导类加载器(bootstrap classloader), c++实现, 主要加载一些自身jvm需要的类和一些核心类。加载lib下的类(Object 、 String 等类), 扩展类加载器加载extend路径下的类,应该是jdk团队鼓励大家将一些通用的类放这。 应用程序类加载器, 主要加载用户路径中指定的类, 如果没有特殊指定,一般使用这个
双亲委派模型是指刚才说的三种类加载器存在加载顺序, 默认的类加载时,会判断当前类是否已经加载,如果未加载会判断是否存在父类加载器,如果存在就使用父类加载,如果不存在则使用引导类加载器加载,如果两个都不能加载则自己加载。
主要是两方面,一是保障核心类库的安全性, 只能由jvm自己的类加载器加载,二是防止重复加载,提高性能。
如何破坏双亲委派, 一是继承classloader类,重写loader class接口。 二是java 自己提供的spi机制, 像jdbc等接口, 由各业务方自己实现响应逻辑。 但spi接口是放在核心库中的, 引导类加载器加载的时候找不到实现,父类加载器无法加载子类的代码。 jvm是通过线程上下文类加载器实现的, 在jvm中会将当前类加载器加载不了的类交给线程上下文类加载器, 默认返回应用程序类加载器。
jvm相关
jvm 内存模型, 什么样的垃圾需要回收, 什么时候回收, 怎么回收
jvm内存模型,每个区域大致存放什么东西
运行时的数据区域
各个工作线程共有的:
堆
堆是jvm内存模型中最大的一个区域, 理论上所有的对象都在此处分配。 我们常说的gc算法基本都是对此处内存进行处理。 一般的oom也是经常发生在此处。
元空间(永久代、方法区)
主要存储一些固定信息, 像被加载的类信息,一些静态变量和常量等
各个线程独有的
栈
每个方法的调用对应着方法栈的入栈和出栈动作,出入栈的对象是栈帧,存储着本次方法调用的现场信息, 包括局部变量表,方法出口,操作数栈等。 局部变量表代表着方法内需要多少局部变量,底层是使用变量槽来表示, 基本类型和引用类型占用的变量槽数量是固定的, 所有每个方法的局部变量表大小也是固定的。
本地方法栈
与栈类似, 区别是调用的本地方法。
程序计数器
代码编译后会形成字节码命令交于jvm执行,每行命令都有行号, 这个程序计数器记录了每个工作线程的下一个指令行, jvm基于此实现循环、条件语句等
垃圾认定算法,引用类型,gcroot,三色标记, stopword, 卡表问题
垃圾认定算法
随着程序的执行, 各个工作线程在堆中一批批的创建对象,但是由于jvm中堆内存的总数量是固定的,所以需要一种重复利用机制, 将不再使用的对象占用的内存回收,分配给新对象使用, jvm内部定义为了垃圾回收机制。
引用计数法
基本思想是在每个对象头上记录该对象被引用的次数, 当被引用次数为0时,可以认为没有对象再使用它,可以回收该对象。 这种方式的好处是精准高效,能极高效率的回收数据,但缺点也很明显, 对于循环依赖问题有点无能为力。
可达性分析
可达性分析是采用使用关系来确定对象是否需要回收, 简单描述就是判断一个对象是否正在被使用。 处理方式是以一些固定的根节点开始扫描, 被根节点关联的对象不可以被回收。 这些根节点一般是维系服务正常运行的必须节点,若回收掉这些可能程序会出现异常。
包括一些jvm内部使用的数据,像基本类型的Class对象,常见的异常对象,还有类加载器等。
第二部分是需要长久存活的对象,像类变量、常量,被锁住的对象。
第三部分是正在运行的方法需要的变量,像局部变量表中的临时变量、入参变量、还有本地变量表中的变量。
引用类型
原始的引用类型基本只有两种, 要么是被引用,要么是不被引用。 但是jvm希望有一些对象能够在内存不够的时候回收,内存够的时候就不回收, 那么原先的引用定义方式就不支持了。 后来jvm定义了四种引用类型, 强软弱虚,来应对各个场景。
强引用是原因定义的直接引用,只要存在着引用关系, 就不会被回收。也是堆中最多最常见的引用类型。
软引用是一种控制资源量的引用, 内存够的情况下不回收,适合做缓存, guava和mybatis的缓存都是基于软引用实现的, jvm也提供了softhashmap的实现。 一般soft reference 的构造函数除了entry 还有一个队列, jvm会把回收的对象引用放在此队列中,对应的缓存一般要对此对象进行处理
弱引用是为了解决一些大集合中的元素清理问题, 集合中的元素虽然不再使用,但因为集合持有着他的强引用,永远不会被回收。 弱引用的作用就是清理掉这些不再使用的集合元素。 他的处理规则是: 如果一个对象仅仅被弱引用持有, 那么下次垃圾回收时直接清除。 最典型的例子就是threadlocal 中的 localmap。
虚引用的作用, 虚引用被描述为一个幽灵引用,无法通过这个引用获得对象实例,他的作用是为了在对应的内存被回收时得到一个系统通知, 一般用在堆外内存的处理上。
垃圾回收的过程。并发标记问题,stw,跨代引用问题
垃圾回收的过程就是找到垃圾对象并回收的过程。 不同的垃圾处理器的垃圾处理过程不同,大致可以分为两个阶段, 一个是定位垃圾对象, 一个是回收垃圾对象。
定位垃圾对象
定位垃圾对象的流程就是使用可达性分析来定位,其中有几个重要概念:
stop the word, 收集grroot时,会停止所有用户线程。因为不暂停的话gcroot一直处于变化状态。无法准确的收集。
二次标记, 不可达的对象不会立即认定为无用对象, 而是看一下该对象是否实现finalize方法,如果实现了该方法会将其放入一个队列中, 由jvm的一个低优先级的方法执行,但不保证执行完成。在二次标记的过程中。 如果在finalize方法中将该对象重新引用, 比如将this赋值给类变量,该对象就不会消亡。
三色标记,为了辅助标记对象是否可达的状态,主要是应用于并发标记, 像serieal 这种单线程处理的,只要标记垃圾和非垃圾就行, 但是并发标记是和用户一块进行的, 标记过程需要时间,所以会存在中间状态。 三色是黑灰白, 其中黑代表标记完成且非垃圾对象, 灰色代表标记中, 白色代表还未标记或为垃圾对象。
并发标记, 像cms 和 G1这些注重低延迟的垃圾处理器, 标记阶段不会暂停用户线程, 会和用户线程并发执行, 那么在执行过程中就会因为对象引用关系发生变化出现种种问题。 可以分为两类, 一是本该被销毁的标记为存活。 二是本该存活的标记为销毁。第一个场景其实问题没那么严重,顶多是躲过了本次垃圾收集, 成为浮动垃圾。 但第二种情况就很严重, 因为会导致正在使用的对象被回收。 经过验证, 如果要出现第二种错标情况,需要满足两个必要条件, 一是黑节点插入了一个白节点的引用,就是该黑节点已经扫描完成了, 但又来了个白色节点, 不会再次被扫描。 二是该白节点断开了所有白灰节点的引用,即不会再有未扫描的节点来扫描他了。 这种情况就会导致该节点到最后依然是白节点。 从而被回收掉。 理论上来说只要有并发标记,就一定有几率出现这种问题。 cms和g1 针对该问题有不同的解决方案。cms是破坏了第一个充分条件, 只要有白节点插入黑节点,就将黑节点变成灰色,等后续阶段重新扫描灰色节点,这种方案称之为增量更新。 而g1采用的方案是破坏条件2, 如果断开了某一节点关系, 他会记录源引用关系, 最后依然按照原引用关系进行扫描,这种方案称之为一致性快照(STAB)。 两种方案都是基于自身的垃圾收集器架构来设计的,没有具体的优劣之分。因为G1是基于内存分区模型, 每个region都维护着和其他region的卡表, 如果使用增量更新, 扫描的范围会很大。
安全点,安全区域
之前说到gcroot扫描,jvm不可能扫描整个堆来查找gcroot,需要有一个map来存储当前指令对应的gcroot,考虑到性能和开销,不可能为每条指令都生成oopmap, 只能选取一些特定的指令来生成,一般选取可以让程序长时间执行, 引用关系变化不大的指令, 像方法调用,循环,异常调整等指令, 这种指令一般称为安全点, 为了确保所有的线程都跑到安全点, 有两种解决方案, 一种是抢占式中断, 先中断所有线程, 判断是否到达安全点, 如果没到达就唤醒使其跑到安全点。二是主动式中断,线程不断去轮训一个中断标志,发现中断标志为true时主动中断,一般都采用主动中断。 安全区域其实是拉长的安全点,区域内的引用关系不会发生变化,像线程正在休眠,可以认为到达安全区域, 不用再将它唤醒后轮训标志。
跨代引用问题
跨代引用一般指老年代持有新生代的引用,理论上来说,如果老年代持有新生代的引用,那么也需要将老年代加入gcroot进行扫描,来防止新生代对象被误回收, 但这种实现过于复杂和耗时,需要扫描整个老年代, 所以一般会采用记忆集的方式来解决, 将老年代对象和新生代对象的引用关系存储起来。 用的时候直接用,不用存储整个老年代的gcroot。 根据存储的引用精度不同,记忆集有三种模式,一个是字节维度,直接记录了那一块地址存在引用。 第二个是对象维度,记录了某一个老年代对象持有哪些对象引用。 第三个是内存块维度, 他将老年代内存分块, 标记哪一块内存有跨代引用。 Jvm 使用的是第三种精度模式。 称为卡表。 将内存区域分成一个个的内存块, 每个内存块用一个元素表示, 为1代表此内存区域存在跨代引用,称为脏卡。 在回收的时间将此区域的gcroot加入即可。 jvm使用整形数组来存储这些元素。
垃圾回收算法
标记-清除, 直接清除掉指定内存, 优点是垃圾回收过程快,耗时低。缺点是容易产生内存碎片,空间不连续,如果接下来无法存放大对象还得额外整理空间。
标记-复制,将存活对象复制到另一区域内存,然后整体清理当前区域。 如果存活的对象很少, 那么此方案执行效果很高, 而且内存回收后比较规整。 缺点是如果存在大量存活对象, 复制会带来很大开销。 其次这种方式几乎只有一半内存可用,另一半需要预留复制,整体可用内存减半。
标记-整理,将存活对象往一端移动,保障内存空间的连续性。开销较大,但可以保持内存空间规整,方便下次回收, 而且能利用全部内存空间。
分代收集,关于垃圾回收方向有两个理论, 一是98%的对象都是朝生夕死的, 很快就消亡掉。 二是经历的gc次数越多,越难被回收, 基于这两个理论产生了分代收集算法, 将整个内存区域分为新生代和老年代, 新生代处理那些很快消亡的对象, 采用标记-复制算法, 每次只复制很少的数据,速度较快且能保持内存空间连续。 老年代采用标记-清除或标记整理算法。处理那些很难消亡的对象。 新生代与老年代的比例为1:2, 新生代中s0,s1和edan的分布为1:1:8.
新生代晋升老年代的场景
长久存活的对象进入老年代,经历多次gc还能存活的对象进入老年代, 这个值为15,可以根据参数调整
动态年龄判定,jvm会监控新生代所有对象的年龄, 若s区中的相同年龄的对象占用内存超过s区内存的一半,直接全部进入老年代。
大对象直接进入老年代,如果有新生代不够分配的大对象, 直接进入老年代。
分配担保机制, 在新生代gc 的时候, 如果一个s区无法放置gc后的存活对象, 直接进入老年代, 由老年代对s区做分配担保。
各垃圾收集器的工作原理和特点
Serial / serial old , 整个过程都需要stw,新生代采用标记复制,老年代使用标记整理。 在线程核心数少的情况下是个不错的选择, 因为没有额外线程开销, 在cliet 这些客户端应用上是个不错的选择
parNew 是serial 的多线程版本, 整个过程也需要stw, 采用标记复制算法。 多线程的情况下比serial快,标记和复制过程都是多线程, 主要是新生代中只有他可以跟cms配合
Parallel sconvge/ parallel old, 一个专注吞吐量的收集器, 可以设置gc的最大时间,主要是控制gc时间来提高代码运行的时间, 适用于和用户交互少的后台计算应用,尽可能的多执行用户代码。 离线任务的首选。同样也是新生代标记复制, 老年代标记整理
与 parallel sconvge 不同, cms更关注于用户的体验, 尽可能的减少用户等待时间。 与上面几种收集器不同的事, 他的标记和清除阶段是和用户一块进行的,仅仅是初始stw的时候暂停一下用户线程。 他的工作流程可以分为四步, 一是初始标记, 来确定gcroot, 二是并发标记, 进行可达性分析确定垃圾对象。 三是重新标记, 主要是处理增量更新的数据, 会将新增的引用节点黑变灰,所以需要重新标记下那些灰节点。 四是并发清理。 来回收掉那些垃圾对象。 cms的优点很明显, 就是用户gc停顿时间短,用户体验好。 缺点一是占用cpu开销高, cms默认启动的gc线程几乎等同于核心数, 会抢占执行用户程序的资源,造成吞吐量下降, 二是由于cms是采用标记清除算法, 会产生很多内存碎片, 造成内存空间的浪费, 也可能会导致 young gc 后无法存储存活对象而进行一次FULLgc。 所以需要清除几次后进行一次整理动作,将空间整合, 这个参数可以调整, 默认是0次, 每次都进行整理。 最后是并发清除阶段是和用户线程一起工作的, 所有不能等到老年代满了才进行清除,而是达到一定比例就进行old gc,会造成老年代空间的提前回收,间接上使某些老年代空间不可用。
G1,G1是cms的一种继承和优化,设计目标是能保持cms的短暂停顿的特点同时,提高系统的吞吐量。 在多cpu大内存的情况下G1的效果会很好,而且他不必配合其他新生代收集器, 自己就能完成整个分代收集。 G1 将内存结构做了重新划分, 将整个内存划分成了一个个的region, 每个region可以代表不同的角色, edan、old、S、h。默认的region数量是2048个, 可以根据G1heapRegionsize参数调整, 每个region的大小可以是1,2,4,8,16,32m, 最小1m最大32m,如果没有指定region的大小, jvm会按照堆大小和region数计算region大小。
他保持短暂停顿的方案和cms一致,都是使用并发处理, 和用户线程一起工作。 而提高吞吐的方式则是两个技术点实现, 一个是将内存细化处理, 在各个region之间采用复制算法, 在整个堆的维度上采用近似整理算法,能够一定程度上减少内存碎片率, 顶多在各个region之间有部分碎片,但是能以region维度回收内存。 二是建立有效的回收预测模型, 能够计算每个region回收的收益,可以在较短的时间内回收高收益的那些region,实现较高的内存回收率。
总体工作流程也是分四步, 一是初始标记, 标记那些gcroot, 二是并发标记阶段,跟cms相同也是进行可达性分析。三是最终标记阶段, 根据一致性快照标记那些引用关系发生删除的节点,四是筛选回收, 计算各个region回收收益,筛选高收益的region进行回收, 此操作不是与用户并发执行的, 特性放在了zgc中
G1 一定程度上能代替cms。但G1 的额外开销特别大,像各个region都要维护一份卡表, 每次更新region的数据, 都要写很多关联的region。 在cpu和内存资源比较紧张的 情况下, 部分书籍上根据经验给出了这个阈值, cpu 4核,内存4g, 低于这个值可能cms会优于G1, 这个没有实践过,接触到的服务很少有低于这个值的。