JVM
简述
JVM 将 java 等编程语言的 class 文件通过解释器或者 JIT 生成字节码用于和硬件设施交互。java 程序通过生成在 JVM 虚拟机运行的字节码,JVM 虚拟机通过字节码去和硬件进行交互,屏蔽了很多的操作系统平台相干信息,保障了 java 的跨平台运行
Java 内存区域
Java 内存区域和内存模型是不一样的货色,内存区域是指 JVM 运行时将数据分区域存储 ,强调对内存空间的划分;内存模型(Java Memory Model,简称 JMM)是 定义了线程和主内存之间的形象关系 ,即 JMM 定义了 JVM 在计算机内存(RAM) 中的工作形式
-
程序计数器
- 线程公有
- 线程是占用 CPU 执行的根本单位,多线程的实现是 CPU 通过工夫片轮转形式来让线程轮询占用,以后线程的工夫片应用完之后就须要让出 CPU,等下次轮到本人再执行。程序计数器就是用来记录线程让出 CPU 时的执行地址的(线程公有的起因)
- 如果执行的是 Java 办法,则程序计数器记录的是下一条指令的地址;如果执行 Native 办法,记录的是 undefined 地址
-
虚拟机栈
- 线程公有
- 寄存线程的 局部变量、调用栈帧。每个办法执行时会在虚拟机栈生成一个栈帧,一个办法就是一个栈帧从入栈到出栈的过程
-
本地办法栈
- 线程公有
- 与虚拟机栈相似,本地办法栈对应 Native 办法,虚拟机栈对应 java 办法
-
堆
- 线程共享
- 寄存对象实例
-
办法区
- 线程共享
- JDK7 之前:应用永恒代实现;寄存 JVM 加载的类型信息、字符串常量池和动态变量
- JDK7:字符串常量池和动态变量移至 Java 堆
- JDK8:废除永恒代概念,改用间接内存中实现元空间 (Meta-space),将残余局部(次要是类型信息) 移到元空间
- 永恒代和元空间是办法区的两种实现形式
永恒代:存储包含类信息、常量、字符串常量、类动态变量、即时编译器编译后的代码等数据。能够通过
-XX:PermSize
和-XX:MaxPermSize
来进行调节。当内存不足时,会导致 OutOfMemoryError 异样。JDK8 彻底将永恒代移除出 HotSpot JVM,将其原有的数据迁徙至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)元空间(Metaspace):元空间是办法区的在 HotSpot JVM 中的实现,办法区次要用于存储类信息、常量池、办法数据、办法代码、符号援用等。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代之间最大的区别在于:元空间并不在虚拟机中,而是 应用本地内存。实践上取决于 32 位 /64 位零碎内存大小,能够通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
配置内存大小。
类加载过程
- loading
类加载器。双亲委派模型:查找负责加载的类时从下至上查找,加载类时从上至下询问(bootstracp -> extension -> application -> 自定义)
益处:1. 爱护 java 外围类不被篡改;2. 避免反复加载
-
linking
- Verification:校验类是否合乎 JVM 规定
- Preparation:动态变量成员赋默认值
- Resolution:将类、办法、属性等符号援用解析为间接援用;常量池中的各种符号援用解析为指针、偏移量等内存地址的间接援用
- Initializing
调用类初始化代码,给对象赋初始值
对象创立过程
-
具体过程
- 类加载查看,没有执行类加载则执行类加载过程
- 给对象分配内存
- 给对象赋默认值
- 设置对象头,例如这个对象是哪个类的实例,如何能找到类的元数据信息,GC 年龄,是否启用偏差锁等 (哈希码要在执行了 Object::hashCode() 办法才生成)
- 执行 <Init> 办法,给对象赋初始值
-
内存分配原则
-
调配形式
- 指针碰撞:假如 Java 堆中内存相对规整,被应用的内存和没有被应用的内存被放在两侧,两头用指针作为分界器,每次分配内存就只须要将指针向右挪动对象内存大小相等的间隔即可
- 闲暇列表:如果 Java 堆内存不规整,就须要保护一个记录未应用内存区域的列表,每次分配内存时从列表中找出一块足够大小的空间调配给对象
-
- 取决于内存空间是否规整,也就是取决于应用的垃圾回收器
-
解决调配导致的并发问题
分配内存是个高频操作,会有线程平安的问题,并且频繁申请堆来获取内存调配太耗时也太耗资源
- TLAB(Thread Local Allocation Buffer 本地线程调配缓冲),JVM 会先从 java 堆中调配肯定的内存空间给每个线程,线程分配内存时先从 TLAB 中调配,如果 TLAB 不够再从堆上去调配
- CAS+ 重试机制
对象构造
- 对象头
Mark Word:8 字节(1 位 = 8 字节),寄存 hashcode,偏差锁标识,锁类型标识,GC 分代年龄等
Classpointer:类指针,指向类的元数据信息,表明对象所属类。默认压缩 4 字节,不压缩 8 字节
数据长度:4 字节,数组对象才有(因为通过元数据信息是晓得对象长度,然而不能晓得数组长度)
- 实例数据
String/ 援用数据 /Oops(ordinary object pointer 一般对象指针,对象援用的句柄) 4 位,int 8 位
- 对齐填充
对齐 Padding(保障对象位数是 8 的倍数)
对象定位
从栈中的援用定位到堆中具体数据
- 句柄定位:栈存句柄的地址,堆布局一个句柄池区域,句柄池存对象数据指针和对象类型指针
- 栈中存对象数据的地址,堆的对象数据外面划分一个区域来存指向 办法区 外面的对象类型的地址,定位疾速(HotSpot 实现)
垃圾的定义
JVM 中如果没有任何援用指向某个对象,这个对象就被视为垃圾,会被 JVM 内存回收机制回收
垃圾对象断定形式
- 根可达算法
从 GC Roots 对象登程开始查找援用,没有在援用链上的对象则被断定为垃圾对象。
- GC Roots
虚拟机栈,本地办法栈,运行时常量池(属于办法区),办法区外面的 Reference 对象(常量池对象,线程池对象,JNI)
- HotSpot 实现
实际上不会将所有的 GCRoots 作为起源去遍历援用链,因为耗费太大,HotSpot 应用 oopMap 的数据结构来间接失去哪些地方寄存有对象援用,在类加载实现后,就会记录下对象内什么偏移量是什么类型的数据计算出来,而不须要一个不漏从 GC Roots 开始查找
对象从调配到被回收的过程
常见的垃圾回收算法
- 标记革除
- 复制
- 标记整顿
罕用的回收算法都是分代回收,即针对年老代和老年代对象的不同采纳不同的回收算法,年老代划分一个 Eden 区和两个 Survivor 区
平安点,平安区域
平安点:HotSpot 只有在平安点的中央才会生成 oopMap,程序只有在运行到平安点的时候能力进行垃圾回收,采纳主动式中断让线程本人去轮询标记,当标记为真就本人在最近的平安点上被动中断挂起。
平安区域:因为有些程序处于 sleep 或者 blocked 状态,没法中断挂起本人。平安区域可能确保在某一段代码片段中,援用关系不发生变化,相当于扩大的平安点。
记忆集、卡表
- 解决什么问题
GC 时,因为有些新生代对象会被老年代对象援用 ( 跨代援用 ,次要问题产生在老年代对象援用新生代对象),那么当产生 YGC 时,就须要把这些老年代对象退出到 GC Roots,那么 如果不分明新生代对象被哪些老年代对象援用,就须要将整个老年代退出 GC Roots 扫描范畴,根节点援用链工夫耗费过长,会导致 GC 效率低。
- 原理简述
JVM 应用了 记忆集 来记录那些从 非收集区域 指向 收集区域 的指针汇合的形象数据结构,保障每次回收时间接把记忆集中的对象退出 GC Roots 即可。记忆集的实现有很多的维度,卡表是记忆集的一种实现 。基于卡表会把堆空间划分为一系列的卡页组成,一个卡表对应一个卡页,HotSpot JVM 的卡页大小为 512 字节, 卡表被实现成一个简略数组。当产生跨代援用时,援用对象所在卡表会被变 ” 脏 ”(dirty),每次只须要将这些卡表外面的对象退出 GC Roots 即可
-
实现细节
- 对于用卡表实现记忆集颗粒度的问题
首先要回归咱们解决问题的实质是为了避免 GC 时遍历整个非手机区域的对象来寻找跨代援用对象,如果咱们准确到一个类,一个对象来说,其实保护老本又会很大,而卡表准确到一块内存区域的形式保障了疾速 GC,也避免记忆集保护老本过高
- 卡表在什么时候变“脏”?以什么形式?
产生在其余分代区域的对象援用本区域对象时,工夫点是 援用类型字段赋值 的时候
通过 写屏障 保护,相似于援用类型字段赋值这个操作的 AOP 切面,G1 之前的垃圾回收器都是用的写后屏障
G1 的记忆集比拟非凡,每个 Region 别离保护着本人的记忆集,记录下别的 Region 指向本人的指针,并且标记这些指针别离在哪些卡页的范畴内(传统的垃圾回收器的记忆集就是只须要保护一块一块的卡表即可,不须要和 G1 一样每个 Region 离开保护,所以 G1 的内存占用比其余传统的垃圾回收高)。理论是一个哈希表构造,key 是别的 Region 的起始地址,value 是一个汇合,存储着卡表的索引号。
- 对于用卡表实现记忆集颗粒度的问题
常见的垃圾回收器
- serial+serial old 单线程
- po+ps:parallel old、parallel scanvenge 多线程,不容许并行,JDK7,8 默认
- CMS(老年代)+pn(parallel new 就为了配合 cms) 收集过程;呈现的问题两种:浮动垃圾(有一个内存阈值的设置,目前默认 92%,就是总内存 (?) 达到 92% 触发 FGC,因为对象基本上属于常常被回收,“朝不保夕”,所以 92% 也没啥子问题。然而要是预留给浮动垃圾的空间不够了,间接报”并发回收失败“,而后 GC 就会把并发标记给关了,而后启动 CMS 备用计划——serial old,间接单线程来回收,重大影响效率了),内存碎片(因为是标记革除算法嘛,有设置,多久进行一次标记整顿)
- G1:初始标记(STW)、并发标记、最终标记(STW)、筛选回收(复制)
-
并发标记阶段的算法
并发标记耗费回收 80% 的工夫,决定了 GC 快慢的因素。并发标记因为援用会变动,可能会产生漏标的状况,如果是该被回收的被标记成存活的问题不大,下次回收就行;然而如果是原本该存活的对象被标记成死亡就有问题了。
会产生“对象隐没”的状况须要满足两个条件:
1. 重新加入了一条从彩色对象指向红色对象的新援用
2. 所有灰色对象到该红色对象的间接或间接援用被删除了
这样自身该红色对象之前被标记为死亡,在退出彩色对象援用之后,就会被标记为存活,然而只会标记一次,所以这个对象就会被回收。
解决办法:加强更新和原始快照,两种形式都是通过 写屏障 实现的
- 增量更新(CMS)
毁坏第一个条件,当彩色对象插入新的指向红色对象的援用关系时,记录这些插入援用,等并发扫描完结后,再讲这些援用关系中的彩色对象为根,从新扫描一次。能够了解为彩色对象一旦新插入了指向红色对象的援用,这个红色对象就变成灰色对象,所以叫 增量更新。
- 原始快照(G1、Shenandoah)
毁坏第二个条件,当灰色对象要删除指向红色对象的援用时,记录这些援用,并发扫描完结后,将这些援用关系中的灰色对象为根,从新扫描一次。能够了解为产生删除援用关系的时候,都依照刚开始扫描的那一刻的对象图快照来进行搜寻,所以叫 原始快照。
- ZGC:colorpointer+ 写屏障有工夫再理解
- 增量更新(CMS)
对象进入老年代的形式
- 对象太大,找不到足够的内存区域进行调配
- 分代年龄达到升级限度,除了 CMS 是 6, 其余默认 15
- 动静年龄断定:进 survivor 区的某个年龄的对象中大于 survivor 区域内存大小的一半,那么大于等于这个年龄的对象都间接进入老年代
- 空间调配担保:年老代 GC 采纳复制算法进行 GC 回收时,当其中一个 Survivor 区不足以包容存活的对象,就须要老年代提供内存空间来寄存 Survivor 区无奈包容的多进去的对象。这就是 担保 的概念。这时候有一个问题:咱们无奈在对象回收前得悉有多少对象会存活下来 。如果每次咱们为了确保老年代空间能齐全包容新生代 GC 后幸存的对象,就须要每次都进行 Full GC,然而每次都进行 Full GC 耗时过长而导致进展工夫增长,用户体验很不好。针对这种状况,Hotspot 采纳的办法是:在垃圾回收之前,取 之前每一次回收降职到老年代的对象容量的均匀大小作为参考,老年代残余空间大于这个值或者大于新生代对象总大小则进行 Minor GC,小于这个值则进行 Full GC。尽管这样依然会呈现“担保失败”的状况,然而防止了过于频繁的 Full GC。
JVM 的内存模型(JMM,Java Memory Model)
每个 java 线程有本人的工作内存,线程只能操作本人的工作内存,只能通过 save 和 load 操作,对主内存的数据更新来实现不同线程之间的数据同步
JVM 调优
调优包含:预调优(事先预计批改配置)、JVM 运行环境问题(卡顿问题)、运行时呈现的问题(OOM 这些)
- top
- top HP - 线程 pid
- jmap histo 这个命令用来看占用
OutOfMemory 问题
导出大量 Excel 数据导致间接挂了,先是运维报警,运行迟缓,而后就 OOM,把 OOM 的那个服务器剥离进去(因为做了高可用,所以不影响),利用 jmap histo 查看对象占用,发现大多数数据是行列对象。解决形式:批改 sql,改成多线程模式查问,导出时用 poi 的 Hssfbook,有一个内存窗格的说法,保障内存中存在的对象是固定的。