深入理解JVM
这里写目录标题
- 深入理解JVM
- 一级目录
-
- 二级目录
-
- 入门篇
-
- JVM内存模型
- JVM内存参数
- Java对象创建及内存分配机制
-
- 对象的骚操作
-
- 指针压缩:
- 为什么要指针压缩
- 压缩指针实现原理
- 对象的内存分配
- 对象的内存回收
- 对象的四种引用
- 判断一个类是否无用
- 进阶篇
-
- 垃圾回收算法
-
- 垃圾收集器
-
- **Serial收集器(**-XX:+UseSerialGC -XX:+UseSerialOldGC)
- **Parallel Scavenge收集器(**-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
- **ParNew收集器(**-XX:+UseParNewGC)
- **CMS收集器(**-XX:+UseConcMarkSweepGC(old))----重点
- 三色标记
-
- 多标----浮动垃圾:
- 漏标 --- 读写屏障,
- 记忆集与卡表
- G1收集器
-
- ZGC收集器
-
一级目录
二级目录
三级目录
入门篇
JVM内存模型
众所周知的东西,
运行时数据区:堆,栈,方法区,本地方法栈,程序计数器;
其他:类装载系统,字节码执行引擎;
JVM内存参数
- 堆:
-Xms -Xmx
新生代:-Xmn
- 方法区(元空间):
-XX:MetaspaceSize -XX:MaxMetaspaceSize
Java对象创建及内存分配机制
对象的创建过程
- 类加载检查 :首先要检查这个类是否被加载了,如果没有的话说明这个对象是第一次创建,执行其类加载过程判断该类是否符合双亲委派机制;
- 分配内存:为对象分配一块内存空间地址;用来存放;但是内存划分可能存在并发问题;如何解决?
划分内存的方法
- 指针碰撞:用一个指针移动划分空闲空间和非空间空间,为对象划分内存是将指针往后移动即可
- 空闲列表:记录空闲区域;在分配内存是从列表中找出一块符合空间的内存块;
解决并发问题
- 设置对象头:在上一步初始化之后,属性变量变为0或者null;虚拟机要进行必要的对象设置,要知道这个对象是哪个类的实例,如何才能找到类的元数据信息;对象的哈希码,对象的分代年龄;这些信息放到对象的对象头Header中;对象在内存中的组成有三部分:对象头+实例数据+对齐填充;
对象头包含信息如下:
通过锁标志位即可判断对象的状态,对齐进行加锁或升级操作,
- 属性赋值init<>:执行对象的构造方法,对该对象进行赋值操作;
对象的骚操作
指针压缩:
了解这个问题之前需要知道一个对象通常有多大,比方有以下对象属性
int a ; // 4字节
byte b; // 1字节
String name;//8字节;
共 13字节;加上对象头 MarkWord(8字节(64) or 4字节(32)) + Klass Point类型指针 8 = 16 + 13 = 29B;
- 开启指针压缩后对象大小:引用属性 8 --> 4; KlassPoint: 8 —> 4;
为什么要指针压缩
-
要知道计算机底层是用一个个格子来存储数据的,一个格子有两种状态,高电频和低电频,也就是常说的1 和0;那么一个格子就是占用1bit空间,
-
位:他的意思其实是是2的多少次幂;如:232次方就代表32位,能存放232个地址,但是2^32次方换算过来=4294967296bit,也就是这么多个格子;那么换算成MB-----》= 4294967296bit /8 = 536870912byte / 1024 = 524288KB / 1024 = 512 MB;(1MB = 1024KB, 1KB = 1024B, 1B = 1byte = 8byte);
- 显然,32位的机器只有2^32个格子能存放512MB;那么何来32位能存储4GB内存呢?---->4GB 内存要的位数:4GB /1024 /1024/8 = 2^35次方所以需要最少35位才能表示4GB内存,
- 为了解决这个问题,计算机底层其实并不是以一个格子来存储地址,这样很不方便,而是采用8个格子也就是1byte来做一个存储单元,所以在235次方基础上需要除以8得到byte单位;也就是232次方,
- 综上:所谓的32位在计算机底层其实是以一个字节位存储单位,32位能表示的地址也就是232个,每一个地址的大小都是1字节8bit;所以32为系统共有232B个地址单元,操作系统采用十六进制为每一个存储单元编号,
-
所以现在再来探讨这个问题;
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据, 占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm
只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好
压缩指针实现原理
首先先知道JVM对象分配的另一个骚操作
- 对齐填充,JVM希望对象的内存大小希望是8的整数;如果不是的话就会增加到8的整数倍容量,这样做的目的是加快对象的寻址速度;因为在64位系统中,cpu一次寻址宽度是64bit也就是8字节,
所以,首先JVM在申请内存是就会将内存划分为每8字节为一个存储单元,就像操作系统将每1字节做一个存储单元一样,然后再将这每一个存储单元映射为一个地址编号,上文我们知道共有2^32次方个地址数量;JVM又强行把每一个地址对应8字节,所以原本能存4GB 的容量现在又扩大了8倍就变成了32G,
这就是为什么超过32GB后压缩指针会失效的原因;
对象的内存分配
学JVM的都知道,对象在堆上分配,但是,这是绝对的嘛?
- 栈上分配;JVM通过逃逸分析,方法里面的对象不会逃逸出方法外供其他使用就可以直接分配在栈上自己使用;方法执行完毕后直接销毁,从而节省eden区压力,减少minorGC的次数;
- 如果栈空间不够分配这个不逃逸得对象就会使用标量替换
- 大对象直接进入老年代:通过设置参数
-XX:PretenureSizeThreshold
,设置大对象的容量使其直接进入老年代,目的就是防止大对象在S1区和S2区频繁赋值移动降低性能;
- 长期存活的对象直接放入老年代:懂得都懂;
- 对象动态年龄判断;现在有一个对象要放到S1或者S2区,发现这个区里边有一批对象的内存大小和已经大于了这个区的50%,就会把大于等于这批对象中的最大年龄放入老年代;这个操作一般发生在minor GC 后;
- 担保机制;每次minorGC 之前老年带都会判断年轻代里面所有对象大小和有没有超过老年区剩余空间;就是防止全部对象进入老年代导致OOM而来不及FullGC,如果超过了就会检查是否配置了担保机制,没配置就直接先FullGC,配置了的话就计算之前平均放入老年代的大小会不会导致OOM;不会就触发minor GC ,会就直接FullGC;
对象的内存回收
- 引用计数法:给对象添加一个引用计数器,被引用就+1,取消引用就-1,等于0 就可以回收;但是一般不用这个方法;因为很难解决对象之间的互相引用;
- 可达性分析;
对象的四种引用
- 强引用: String s = new String();
- 弱引用:public static WeakReference user = new WeakReference(new User());GC直接回收
- 软引用: public static SoftReference user = new SoftReference(new User()); 没有空间就会回收;
- 虚引用:几乎不用
判断一个类是否无用
同时满足三个条件:
- 该类的ClassLoader已经被回收
- 该类的所有实例被回收
- 该类的Java.lang.Class对象没有被引用,否则可以通过反射获取对象;
进阶篇
这一部分开始JVM的高级部分,包括垃圾回收算法,垃圾回收器,以及各种各样的垃圾收集器之间的区别,优缺点,以及使用的算法和会遇到的问题,如何优化,如何设置参数等;
垃圾回收算法
- 标记-清除:也就是利用可达性分析,将还在被引用的对象(非垃圾对象)全部标记起来;然后清除未标记的垃圾对象;缺点就是对象过多效率慢,同时产生大量的内存碎片;
- 标记-整理:由于上一个算法会导致很多内存碎片;可以使用标记整理算法将标记对象全部往一端移动(如果移动的位置有垃圾对象占用直接覆盖),然后将最后一个标记对象的另一端直接清空;
- 标记-复制:标记算法还是一样,只不过开辟一块空间,将标记的对象复制一份整齐排列到该空间;这样就不会有碎片产生;
如何选择(分代收集理论)?
见名知意;分代收集就是在年轻代和老年代各自选择不同算法进行配合,从而使得系统效率更高
- 比方在年轻代:空间富足就可以使用标记复制,牺牲对象的复制成本;但是在老年代就不适合使用,老年代的对象一般都不轻易回收,并且没有富足的空间供复制使用
- 所以在老年代,我们没有办法必须选择”标记清除“或“标记整理”算法,该两种算法效率比标记复制慢10倍;
垃圾收集器
上面介绍了回收算法的理论;那么该由谁来实现这些思想呢?当然是垃圾收集器;由于三种回收算法各有各的优点;所以就产生如下几种垃圾收集器,针对不同的代或不同场景使用效率更高
Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
采用算法:年轻代:复制;老年代:整理; serial:串行,由此可知他是一个单线程的
Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
采用算法:和serial一样;
- 实现思路:触发GC----STW----多线程回收;可以发现和serial的唯一区别就是多线程并行执行;
- 优点:多线程并行执行,效率更高,前提是cpu是多核的;默认线程数是cpu的核数;可以修改,但没必要
- 特点:它关注的是CPU的吞吐量,目的提高CPU的利用率;
ParNew收集器(-XX:+UseParNewGC)
采用算法:和Serial一样;但是他只能在年轻代使用,老年代的话需要配合CMS或Serial OLd使用;
- 实现思路:和Parallel一样;
- 优点:和parallel一样;区别就是它能配合CMS使用,实现真正意义上的并发;
CMS收集器(-XX:+UseConcMarkSweepGC(old))----重点
采用算法:CMS(concurrent Mark Sweep),见名知意:标记-清除;
- 实现思路:分阶段执行,比较复杂,但是实现了真正意义上的并发;
-
初始标记:利用可达性分析标记GCroots;也只标记GCROOTs;这个过程 会stw但是标记非常快;
-
并发标记:与用户线程并发执行;开始从GCroot往下标记;
-
重新标记:并发标记时会产生的问题:多标(从root结点往下标记过程中gcroot引用取消了,就会导致垃圾对象被标记成了非垃圾对象)、漏标(标记过程中还没来得及标记的结点被修改成了已经标记结束的结点引用导致他再也不会被扫描到从而遗漏,但实际上他又不是垃圾对象,在清理是因为没被标记而被清除,造成bug);为了解决这些问题就需要stw重新标记发生变化的问题,主要解决漏标,因为多标的在下一次会被回收,问题不大;
-
并发清理:这个阶段执行回收清理操作;
-
并发重制:一次清理结束,当然要重制标记操作,好进行下一次GC;
- 优缺点:并发收集,低停顿;stw时间短,用户体验好;缺点如下:
- 并发抢资源;
- 无法处理浮动垃圾,在并发标记过程中产生的垃圾,只能等下一次清理
- 产生空间碎片:因为使用标记清除,不过JVM提供参数供我们使用,可以选择在gc次数过后使用标记整理算法整理空间;
- 执行过程是并发的,所以可能出现回收过程还没结束就会又一次触发FUllGC;出现"concurrent mode failure“事故;此时就会stw,利用serial old回收器来进行回收;
三色标记
既然CMS重新标记阶段会产生问题,那么应该如何解决呢?答案是三色标记;
在重新标记阶段:需要把GCRoots可达性分析过程中遇到的对象按照是否访问过将对象标记为三种颜色;
- 黑色:就像一颗二叉树,如果该对象的所用引用对象都访问过了,那么久把它标记为黑色,转化为二叉树的思想就是,他的所有子节点都被访问过了,所以如果下次可达性分析来到这个黑色结点就不应往下进行了;
- 灰色:代表他被访问过了,但是他的引用没有完全被访问,所以就不能标记成黑色,只能标记成色,等什么时候他的子节点完全被访问过了之后就会标记为黑色;
- 白色:可达性分析开始前都是白色,分析结束后还是白色的话说明不可达,毋庸置疑就是垃圾对象,可以回收;
多标----浮动垃圾:
标记完成后根引用置空了,导致后面一串引用着的都变成垃圾对象,这就是多标,不会被当中垃圾,无非就是下次一清除罢了; 另外再并发标记和清理期间产生的对象一律标记为黑色,不会清除,
漏标 — 读写屏障,
- 为什么会产生漏标呢,还不是因为再并发标记过程中本来已经标记为黑色的对象新增了引用指向其他对象(假设为对象D);如果这个D被标记了还好,到时如果没被标记的话说命它还是白色,可是理论上他也不会在有被访问的机会了(因为引用他的是一个黑色对象,标记过程中遇到黑色对象是不会再往下的);
- 解决办法:既然是因为并发期间操作对象发生读写导致的问题,那就在并发过程中对所用执行读写的对象都做一个记录,比如D对象,在把它赋值给一个对象的引用时把他们之间的引用关系记录下来放到一个集合或者队列;等重新标记的时候再对他们进行处理;JVM有两种解决办法应对这种情况:
- 增量更新:如果一个黑色对象增加引用;就更新他的颜色为灰色,在重新标记阶段是要对灰色对象重新可达性分析的!CMS使用的就是这个
- 原始快照SATB:另可放过一千,不可错杀一个!在灰色对象要删除对白色对象的引用时,我们把它们的关系记录下来,到时候根据记录就能找到这个被删除的引用,并把它标记为黑色,放到下一轮在处理;虽然它有可能也是浮动垃圾,但是我们没有百分之百的确定她是垃圾,就不能错杀!
以上两种解决方案在底层都是通过写屏障来实现的;
我们无非是想在删除引用或者增加引用的时候把他们的信息存储起来,那我们在执行删除或增加的前一行或者下一行插入一下操作不就好了?(这不就是AOP)
记忆集与卡表
在对对象进行标记过程中如果入到跨代引用,很难处理,就需要用一种数据结构来维护这种引用关系;就是记录集,在年轻代使用,老年代使用卡表;
G1收集器
采用算法:复制算法;
实现思路:该收集器专为大型服务器大内存准备;
- 颠覆内存管理结构,将内存划分为等块的空间,一个空间称为一个Region;最多可以分为2048个块;因为内存空间足够大;所以一个region能分配1M-32M之间;每个region可以是年轻代,也可以是老年代,更可以是S1、S2区,eden 与 S区同样维护8:1:1;不再物理上的分代而是转为逻辑上的分代思想;另外它新增了一个代,专门放大对象的(超过该region50%就判定位大对象);结构图:
-
G1一次收集运作过程:
- 初始标记:stw标记gcroots;速度很快;同CMS;
- 并发标记、最终标记:同CMS;
- 筛选回收:准备工作准备就绪;垃圾准备完毕;终于可以回收了;从图中可以看到;这个阶段是要stw的;这还比CMS多了一个阶段的stw;为什么却比CMS牛逼呢,原来是因为筛选;为什么筛选,筛选目的就是为了选出在同等时间内,回收哪一块的垃圾效益更高,然后先将高收益的回收先处理;这一阶段并不是全部回收;而是内部计算了一个回收时间,默认是200ms,达到就停止回收;剩下的就下一次;相当于把一次分成了多次;把一次stw的时间也分成了多次,这样在用户感受就像没有发生Stw一样;当然这样做的前提是他得拥有足够的内存空间,同时他面向的也是大型的项目,产生的也不过是一些朝生夕死的对象;同时因为采用复制算法,不会产生空间碎片;这样留下的空间也更方便使用;
-
特点
- 可以发现,G1基于大内存(8G)以上,采用分块思想不再固定代的划分;动态的分配代;使得内存使用更加灵活可用;同时整体类似标记整理;局部使用复制,这让空间排列规整,使用更加灵活;可以把所有标记的对象放一个没用过的region存放,随后直接清理region,非常的暴力可靠!
- 基于可预订的停顿时间,让使用者可以根据需求完善系统;
-
垃圾收集分类
毕竟各种各样的region总不能就使用一种收集方式吧
- YoungGc:类似其他收集器的minor Gc ;其他的eden区满了就会触发,但YoungGC 不会,毕竟他底层是可以预估回收时间的;我们又设定了一个回收时间;所以它会判断这个回收时间是不是远远小于设定的时间,如果是;那就继续增加空间!给老子继续放,这些垃圾还不够老子吃!
- MixedGc:非Full Gc;当老年代占用空间达到设定参数后就会触发;回收部分老年代和年轻代以及大对象区,使用复制算法将存活对象复制到空闲的region区;如果空闲的region不够用了,那就会触发FullGc(概率小)
- Full GC:暂停线程,使用单线程进行标记清理和压缩整理出一批空闲的region;提供给下一次MixedGC;
核心:
合理设置停顿时间,不要太长也不要太短,太长会导致年轻代越来越多才触发GC;这样S区放不下就会放到老年区;太小会导致回收频繁,
ZGC收集器
在Jdk11引入,在14已经转正了,功能非常强大;**它就是未来!!**算法:标记整理;
ZGC的目标:
那就来探究一下为什么他口气这么狂,踏马的他有什么本事?
- 因为初始版本,所以为了降低麻烦,选择不分代,当然这是暂时的,可能后面会分;
- 内存布局:分为小中大三个级别;
- 小型Region:2MB;存放小于256kb的对象;
- 中型region:32MB:存放256 - 4MB;
- 大型region:2的整数倍,动态分配;存放4Mb以上的大对象;虽然叫做大型region;但实际上他只放一个对象,所以容量一般比中型小;
NUMA-aware
介绍这个东西之前,我们先来思考一个问题,为什么前面的收集器会慢,慢的地方在哪?我们可以在什么地方提升速度?就拿CMS来说,单线程速度大致是不变得,所以只能从并发阶段入手,慢就慢在并发涉及到资源争强问题;这里能不能优化呢,也就是解决资源争抢问题,想ThreadLocal一样;各玩各的,当然有,这就是ZGC独有的一个特点
UMA(Uniform Memory Access Architecture,统一内存访问架构):表示访问内存的操作都在这一块内存上统一;这就是导致并发抢资源的原因;那NUMA不就是No UMA;不再是统一访问一块内存,而是各自访问各自的区域;这就减少了竞争
颜色指针;
不是三色标记;这是ZGC特性之一;
众所周知:64位系统JVM采用64位寻址,而64 位支持的内存高达一亿T;根本用不到那么多,所以ZGC采用42为做寻址,另外的位就可以添加一些操作;颜色指针就存在这其中的两位中:两位可以表示四种状态;