深入理解Java虚拟机2
2021-02-27 23:23:45 0 举报
AI智能生成
登录查看完整内容
bc
作者其他创作
大纲/内容
深入理解Java虚拟机
第一部分:走进Java
优点
摆脱硬件,一次编写,到处运行
提供相对安全内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界
实现了热点代码检测和运行时编译及优化
有一套完善的应用程序接口
Java技术体系
JDK
JRE
Java技术平台划分
Java内存区域与内存溢出异常
运行时数据区域
所有线程共享
方法区
堆
存放对象实例,几乎所有的对象实例都在这里分配内存
是垃圾收集的主要区域
根据分代算法理论思想,可分为新生代和老年代
细分为
Eden空间
From Survivor空间
To Survivor空间
常用配置堆参数
-Xms:堆的最小值;-Xmx:堆的最大值-Xmn:新生代大小-XX:NewSize;新生代最小值-XX:MaxNewSize:新生代最大值;
不需要连续的内存
线程私有
栈
虚拟机栈
1、生命周期与线程相同,是Java方法执行的内存模型
2、每个方法运行时都会创建一个栈帧
1、局部变量表
2、操作数栈
存放方法执行的操作数
3、动态链接
Java语言特性多态(需要类加载运行时才能确定具体的方法)
4、方法出口
调用程序计数器中的地址作为返回
本地方法栈
本地Native方法服务
异常
StackOverflowError:请求深度>虚拟机允许深度
OutOfMemoryError:栈动态扩展时无法申请到足够内存,抛出
程序计数器
1、是一小块内存区域,可以看做是当前线程所执行字节码的行号指示器
2、线程私有内存
3、唯一没有OutOfMemoryError的区域
直接内存
可以使用Native函数直接分配对外内存,通过DirectByteBuffer对象使用,避免了在Java堆和Native来回复制数据
异常:OutOfMemoryError
HotSpot虚拟机对象
对象的创建
对象的内存布局,对象在内存中存储布局分为三块区域
对象头
1、存储对象自身的运行时数据
哈希吗
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
2、类型指针
指向类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
3、如果是数组,还要有一块记录数据长度的数据
实例数据
存储有效信息,代码中各种类型的字段内容,包括从父类继承下来的
对齐填充
不是必然存在,也没有特别含义,仅仅起到占位符作用
jvm要求内存起始必须是8字节的整数倍,如果刚好一个9字节的数字,就需要16个字节来存储,后面7个字节是空的
对象的访问定位
Java程序通过栈上的reference数据来操作对象上的具体对象
句柄
句柄池
包含了对象实例数据域类型数据的地址
reference中存储的是稳定的句柄地址,当对象被移动时(比如GC时),只需更改句柄中的实例数据指针,而reference不需要改变。这样做的好处是如果一个对象被多个reference所引用,那么当对象地址被修改时,只需更改一个句柄地址即可,而不需要更改多个reference。提高效率。
缺点
定位对象时需要两次定位
直接指针
速度快,节省一次指针定位的时间开销,HotSpot采用此种方式
当对象被移动时,所有指向该对象的reference都需要被改变,耗时
Java虚拟机OutOfMemoryError
程序计数器不会发生OOM
程序计数器(Program Counter Register)也称PC寄存器。是运行时数据区里唯一一块没有Out of Memory的区域。只存下一个字节码指令的地址,消耗内存小且固定,无论方法多深,他只存一条。只针对一个线程,随着线程的结束而销毁
堆溢出
示例代码:不断创建对象,添加到list,因为有引用,所以不会被垃圾回收。
虚拟机参数配置
-Xms20m -Xmx20m
两参数相同,可避免堆自动扩展
-XX:+HeapDumpOnOutOfMemoryError
虚拟机出现OOM时Dump出当前内存快照,以便事后分析
虚拟机栈和本地方法栈溢出
出现异常两种情况
1、线程请求的栈深度大于虚拟机所允许的最大深度,抛出stackoverFlowError
2、虚拟机在扩展栈时无法申请到足够的内存空间,抛出OOM
在单个线程下,无论是由于线程栈帧太小还是虚拟机容量太小,当内存无法分配的时候,都是抛出stackoverflowError
线程数多的时候解决stackoverflowerror的特殊办法
在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
栈大小设置参数
-Xss128K
产生OOM:通过不断创建线程会产生OOM
方法区和运行时常量池
运行时常量池是方法区的一部分
String.intern()
是一个native方法,如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回这个字符串对象,否则将此String对象的字符串添加到常量池,并返回对象的引用
垃圾回收器与内存分配策略
判断对象死亡的方法
引用计数法
概念:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效的时候,计数器值减一;当计数器值=0的对象就是不可能再被使用的对象
问题:难以解决对象之间循环引用问题
HotSpot虚拟机不适用这个方法
可达性分析
概念:通过一些流的GC Roots的对象作为起点,从这些起点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Root没有任何引用链连接时,证明此对象不可用的。
GC Root包含的对象
虚拟机栈中引用的对象(栈帧中的本地变量表)
方法区中的静态属性引用的对象
方法区中常量的引用对象
本地方法寻址中的native引用的对象
再谈引用
改进的理由:有这样的对象,当内存空间还足够的时候,能够保留在内存中,如果内存空间在垃圾回收之后还是紧张,则可以抛弃这些对象
强引用
垃圾回收器不会回收
举例:Object obj = new Object();
软引用
概念:还有用但是是非必须对象,垃圾回收一次之后,内存还是不足,会对软引用对象二次回收
实现:使用SoftReference来实现软引用
弱引用
概念:弱引用对象只能生存到下一次垃圾收集器发生之前,即一定会被回收掉,不管内存是否足够
实现:WeakReference类来实现
虚引用
概念:也称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取一个对象的实例
为什么要使用虚引用:在这个对象被垃圾回收收到一个系统通知
实现:PhantomReference接口
生存还是死亡
可达性分析之后对象并不是绝对死亡,要经过两次标记
1、可达性分析标记一次,并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(Object类中的finalize()是个空方法)
GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
2、对象要执行finalize方法,会繁殖到F-Queue队列,虚拟机会触发这个方法,然后GC对F-Queue队列第二次标记,这是对象最后一次逃脱清理的机会。
回收方法区
常量池
1、常量池中的对象“abc”不存在任何引用时,会被回收
类的回收条件(可以回收,需要参数配合)
1、该类的所有实例都已经被回收,也就是Java堆中不再有该类的任何实例
2、加载该类的ClassLoader已经被回收
3、该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
参数控制
-Xnoclassgc
-verbose:class
-XX:+TraceClassUnLoading查看类加载
卸载信息
使用场景
大量使用反射、动态代理、CGLib,动态生成JSP以及OSGi这类频繁自定义CLassLoder需要虚引用功能
垃圾收集算法
分代收集算法理论
概述:根据对象存活的周期不同将内存分为几块:比如划分成新生代和老年代;新生代对象存活率低,使用复制算法;老年代对象存活率高,使用标记-清理或者标记-整理算法
标记-整理算法(老年代)
使用场景:老年代,适合对象存活率较高的
概述:先标记,再让存活的对象向一端移动,覆盖掉一部分标记对象,然后直接清理掉端边界以外的内存
复制算法(新生代)
概念:将内存按照容量划分为大小相等的两块,每次知识用其中的一块,当这一块内存用完了,就把还存活的对象复制到另一块上面,然后在把已经使用过的内存空间一次清理掉。
简单
效率高
适合对象存活率较低的
缺点:
内存缩小为原来的一半
特性:新生代中的98%对象是朝生夕死
使用(在新生代中):堆分为,回收的时候,将Eden和Surivivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和Survivor刚才使用过的空间。
from区(10%)
to区(10%)
Eden(80%)
标记-清理算法(老年代)
标记方法
引用计数器
概述:先标记回收对象,然后统一回收所有被标记的对象
不足:
效率低:标记和清除效率都低
空间问题:标记清除会产生大量不连续的内存碎片
HotSpot算法实现
枚举根节点
可达性分析-问题
仅方法区可能几百兆,检查可达性很耗时
GC停顿时
分析工作要保持一致性,让整个系统看起来冻结在某个时间点
主流虚拟机使用准确式GC,在HotSpot中,一组OOpMap数据结构来得知那些地方存储对象的引用,这样GC扫描时就能够知道这些信息
安全点
OOPMap内容变化的指令非常多,如果每一个对应的指令都生成OOPMap,那会需要大量额外的空间,提高GC成本
只在特定的位置记录OOPMap,被称为安全点,safePoint
安全点选定标准:是否具有让程序长时间执行的特征
最明显特征指令复用,循环、方法调用、异常跳转,这些地方才会产生安全点
如何在GC时让所有的线程都跑到安全点上?
抢先式中断
GC时,首先把线程全部中断,如果线程不在安全点,就让它继续执行到安全点
几乎没有虚拟机使用这种方法
主动式中断
GC时设置一个标志,线程轮询标志为真,线程挂起
轮询标志的地方和安全点是重合的+创建对象需要分配内存的地方
安全区域
问题?当线程处于sleep和blocked时,这时候线程无法满足中断请求,无法在安全点挂起,这时候就需要安全区域来解决。
概念:在一段代码片段中,引用关系不会发生变化,在这个区域任何地方GC都是安全的
垃圾收集器
云笔记-垃圾收集器
Serial收集器
特点
单线程收集器
垃圾收集时,必须暂停其他所有的线程,知道收集完成
Client模式下,默认的新生代收集器
简单、高效
单线程、没有线程交互,专心回收垃圾、效率高
Serial Old收集器
Serial收集器的老年代版本
使用标记-整理算法
主要给Client模式下的虚拟机使用
Server模式下用途
jdk1<=1.5,与Parallel Scavenge搭配使用
作为CMS收集器的备胎,在并发收集发生Concurrent Mode Failure时使用
回收过程
1、到达safePoint,用户线程暂停
2、新生代采用复制算法
3、老年代采用标记整理算法
ParNew收集器
Serial收集器多线程的版本
许多运行在Server模式下的虚拟机中首选的新生代收集器
在单CPU环境中不会比Serial收集器效果更好
只有ParNew能够和CMS收集器配合工作
参数
-XX:+UseParNewGC
强制指定ParNew收集器
-XX:+UserConcMarkSweepGC
默认使用ParNew收集器
-XX:ParallelGCThreads
限制垃圾收集线程数
Parallel Scavenge收集器(吞吐量有限收集器)
新生代收集器
使用复制算法
并行的多线程收集器
追求的是:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾邮寄时间)举例:虚拟机总运行100分钟,垃圾收集1分钟,吞吐量=99/100为99%
吞吐量越高,用户可能等待时间越少,用户体验越好
可以设置自适应调节策略
新生代选择了它,老年代只能是Serial Old
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
此参数越小,停顿时间的确会下降,但吞吐量也会降下来
-XX:GCTimeRatio 设置吞吐量大小
默认99
-XX:+UserAdaptiveSizePolicy
设置为true之后
不需要手工指定新生代大小、Eden和Survivor比例、晋升老年代对象大小等细节参数
虚拟机根据实际情况动态调整参数信息
调节方式:GC自适应的调节策略
Parallel Old收集器
Parallel Scavenge收集器老年代版本
使用多线程标记-整理算法
使用场合
在注重吞吐量和cpu资源敏感场合,优先考虑Parallel Scavenge和Parallel Old老年代收集器组合
CMS收集器(Concurrent Mark Sweep)又称:并发停顿收集器
以获取最短停顿时间为目标的收集器
标记-清除算法
标记过程
初始标记
仅仅只是标记下GC Root能关联到的对象,速度很快
执行过程会STW
并发标记
即进行GC Root Tracing过程
和用户线程并发执行
重新标记
修正并发标记期间用户程序继续运行导致比较变动的记录
停顿时间比初始标记长一点,远短于并发标记时间,STW
并发清除
垃圾回收标记对象
并发重置
重置本次GC过程中的标记数据
并发标记和清除都可以跟用户线程并发执行,所以停顿时间很短
并发收集
低停顿
1、对CPU资源非常敏感
cpu数>4,对用户线程影响较小,cpu<4,对用户线程影响可能就比较大
2、无法处理浮动垃圾
并发清理阶段,用户程序产生的垃圾只能下一次回收
并发清理时,要预留一部分空间给用户使用
3、标记清理之后会产生大量碎片空间
适合场景
Java能在Tomcat运行的服务程序或者B/S系统服务端上
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
G1收集器 Garbage First
面向服务端应用的垃圾收集器
1、并行与并发
2、分代收集
3、空间整合
从整理看基于:标记-整理
从局部看基于复制算法
4、可预测的停顿
能够建立可预测的停顿时间模型,在长度为M毫秒的时间片段内,消耗在垃圾收集器的时间不得超过N毫秒
收集范围不限于新生代和老年代
把整个堆内存划分成大小相等的多个独立区域(Region),来回收
跟踪各个Region里垃圾堆积价值大小,在后台维护一个优先列表,每次回收都是有限回收价值最大的Region。
防止全队扫描,处理不同Region之间的对象引用问题
每个Region都有一个与之对应的Remembered set,虚拟机在发现堆程序Reference类型的数据写操作时,产生一个Write Barrier屏障暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,并通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered set中。在GC根节点枚举范围中加入Remembered set即可保证部队全站扫描
回收步骤
仅仅只是标记下GC Root能直接关联到的对象,速度很快
修改TAMS(Next Top at mark Start)的值,让下一阶段用户程序并发运行时,能在争取可用的Region中创建新对象
会STW
耗时短
GCRoot可达性分析存活对象,耗时较长,可与用户程序并发执行,同CMS的并发标记
最终标记
修正并发标记期间用户程序继续运行而导致标记冰冻的那一部分的标记,同CMS的重新标记
需要把Remembered set Logs的数据合并到Remeed set中
此阶段需要STW,但可以并行执行。
筛选回收
先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划
追求低停顿,G1可以作为一个可尝试选择
G1不会为吞吐量带来什么特别的好处
参数配置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
ZGC收集器
有道笔记
支持TB级别内存
最大GC停顿时间不超10ms
奠定未来GC特性的基础
最糟糕的情况下吞吐量会降低15%
不分代
内存布局
基于Region, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的, 以低延迟为首要目标的一款垃圾收集器
小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象
中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。
运作过程
与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新颜色指针
并发预备重分配
这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
并发重分配font color=\"#1a1a1a\" face=\
重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障(见下面详解))所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
并发重映射
重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
理解GC日志
垃圾收集器参数总结
0 条评论
回复 删除
下一页