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,有一个内存窗格的说法,保障内存中存在的对象是固定的。