【Java】JVM
2020-12-09 16:55:59 0 举报
AI智能生成
JVM整理
作者其他创作
大纲/内容
运行时结构
程序计数器
唯一一个没有规定任何OutOfMemoryError情况
程序计数器是线程私有的内存区域
虚拟机栈
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
本地方法栈
堆
方法区
运行时常量池
直接内存
垃圾对象的判定
判断对象是否存活
引用计数法
可达性分析法
GCRoot对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用对象
正确理解引用
强引用 strong reference
Object obj = new Object()
软引用 soft reference
SoftReference
弱引用 weak reference
WeakReference
虚引用 phantom reference
PhantomReference
对象死亡的标记
两次标记过程
如果没有GCRoot可达,则标记该对象需要执行finalize(),放入F-Queue的队列中等待第二次标记
对F-Queue的队列进行第二次标记,并调用finalize()进行回收
自救
对象可以在被GC时自我拯救。
这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
回收方法区
废弃常量和无用的类
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
GC算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
效率问题,标记和清除两个过程的效率都不高;
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”
标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记—清理"或者"标记—整理"算法来进行回收。
垃圾收集器
概念理解
并行parallel和并发concurrent
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
MinorGC和FullGC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
收集器
Serial收集器
新生代收集,复制算法
单线程,STW
ParNew收集器
新生代收集,复制算法
Serial收集器的多线程版本
多线程,并行parallel,STW
Parallel Scavenge收集器
新生代收集,复制算法
吞吐量优先
具有自适应调节策略
-XX:+UseAdaptiveSizePolicy
当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
Serial Old收集器
老年代收集,标记整理
单线程,STW
应用模式:client端
Parallel Old收集器
老年代收集,标记整理
多线程,并行Parallel,STW
注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器
CMS收集器
老年代收集,标记清除
Concurrent Mark Sweep 是一个响应时间优先的收集器
GC过程
初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”
并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程
重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”
并发清除(CMS concurrent sweep)
并发清除阶段会清除对象
并发收集、低停顿
缺点
CMS收集器对CPU资源非常敏感
CMS收集器无法处理浮动垃圾
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了
CMS收集器会产生大量空间碎片
Remark阶段扫描全堆,可通过增加CMSScavengeBeforeRemark参数,用来保证Remark前强制进行一次Minor GC,来减少Remark阶段的耗时
MinorGC在标记回收新生代对象时,如何判断老年代对象是否引用了新生代的对象? JVM使用卡表,具体策略是
将老年代的空间分为大小为512B的若干张卡,卡本身是单字节数组,数组中每个元素对应这一张卡,当发生老年代引用新生代对象时,虚拟机将该卡对应的卡表元设置为适当的值。MinorGC时通过扫描卡表就可以很快识别哪些卡中存在老年代指向新生代的引用,避免了全堆扫描。
将老年代的空间分为大小为512B的若干张卡,卡本身是单字节数组,数组中每个元素对应这一张卡,当发生老年代引用新生代对象时,虚拟机将该卡对应的卡表元设置为适当的值。MinorGC时通过扫描卡表就可以很快识别哪些卡中存在老年代指向新生代的引用,避免了全堆扫描。
G1收集器
设计目标
与应用线程同时工作,几乎不需要stop the world
整理剩余空间,不产生内存碎片(CMS只能在Full GC时,使用Serial Old垃圾收集用stop the world 整理内存碎片)
GC停顿更加可控
不牺牲系统的吞吐量
GC不要求额外的内存空间(CMS需要预留空间存储浮动垃圾)
优先清除垃圾较多的region区域,也是命名的由来:Garbage first
全部收集,低停顿,复制算法
整理剩余空间,不产生内存碎片(CMS只能在Full GC时,使用Serial Old垃圾收集用stop the world 整理内存碎片)
GC停顿更加可控
不牺牲系统的吞吐量
GC不要求额外的内存空间(CMS需要预留空间存储浮动垃圾)
优先清除垃圾较多的region区域,也是命名的由来:Garbage first
全部收集,低停顿,复制算法
重要概念
Collection Set
将要被回收的区块集合。GC 时,在这些区块中的对象会被复制到其他区块中,总体上 Collection Sets 消耗的内存小于 1%。
Remembered Set:Point into
每个区块都有一个 RSet,用于记录进入该区块的对象引用(如区块 A 中的对象引用了区块 B,区块 B 的 Rset 需要记录这个信息),它用于实现收集过程的并行化以及使得区块能进行独立收集。总体上 Remembered Sets 消耗的内存小于 5%。记录老年代该Region的即可(即old-young、old-old)
RSet究竟是怎么辅助GC的呢?
在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
SATB:Snapshot-At-The-Beginging
三色标记法:
白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
灰:对象被标记了,但是它的field还没有被标记或标记完。
黑:对象被标记了,且它的所有field也被标记完了。
白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
灰:对象被标记了,但是它的field还没有被标记或标记完。
黑:对象被标记了,且它的所有field也被标记完了。
1. 在开始标记的时候生成一个快照图,标记存活对象
2. 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的,即对于gray对象移除的目标引用对象标记为gray,对于black引用的新产生的对象标记为black)
3. 可能存在浮动垃圾,将在下次被收集
2. 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的,即对于gray对象移除的目标引用对象标记为gray,对于black引用的新产生的对象标记为black)
3. 可能存在浮动垃圾,将在下次被收集
如何解决GC过程中新创建的对象?
每个region记录者两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的
如何解决GC过程中引用对象发生变化?
通过write barrier。
GC模式
Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。注意:依然是在新生代满了的时候,对整个新生代进行回收--整个新生代中的对象,要么被回收,要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小
Mixed GC
选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。注意:Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。
1. global concurrent marking
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节
初始标记(initial mark,STW):它标记了从GC Root开始直接可达的对象。
并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
重新标记(Remark,STW):标记那些在并发标记阶段发生变化的对象,将被回收。
清除垃圾(Cleanup):清除空Region(没有存活对象的),加入到free list。
第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。
2. 拷贝存活对象
FullGC
子主题
当Mixed GC跟不上创建对象的速度,将导致Serial Old进行Full GC
G1和CMS比较
G1在压缩空间方面有优势
G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
Eden、Survivor、Old区不再固定,在内存使用效率上来说更灵活
G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间,避免应用雪崩现象
G1 在回收内存后会马上同时做合并空闲内存的工作,而CMS默认是在STW的时候做
G1会在YoungGC中使用,而CMS只能在Old区使用
G1的适合场景
服务端多核CPU、JVM内存占用较大的应用
应用在运行过程中会产生大量内存碎片、需要经常压缩空间
想要更可控、可预期的GC停顿周期;防止高并发下应用的雪崩现象
内存分配和回收策略
对象优先在Eden分配
大对象直接进入老年代
-XX:PretenureSizeThreshold=size
长期存活的对象将进入老年代
-XX:MaxTenuringThreshold=15
CMS默认值为6,G1中默认为15(在JVM中,该数值是由4个bit来表示,所以最大值1111,即15)
动态对象年龄判定
-XX:TargetSurvivorRatio=50
计算每个对象年龄的总大小,如果发现某个年龄的总大小已经大于了Survivor空间的50%,那么就会动态调整晋升阈值为该对象的年龄大小
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。当大量对象在Minor GC后任然存活,就需要老年代进行空间分配担保,把Survivor无法容纳的对象直接进入老年代。如果老年代判断到剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为经验值),则进行一次Full GC
JDK命令
jps
jps -mlv
jstat
jstat -gcutil <pid> <interval> <count>
-gc
-gccause
jinfo
jinfo -flag SurvivorRatio <PID>
jmap
jmap -dump:live,format=b,file=<filename> <PID>
jmap -heap <PID>
jmap -histo:live <PID> | more
jhat
jstack
jstack -l <PID> > txt
虚拟机执行子系统
类文件结构
类加载机制
加载
查找并加载类的二进制数据,将其放入方法区中,之后在内存中创建一个Class对象,
用来封装类在方法区中的数据结构
类加载器 双亲委托机制
保证类在内存中的唯一性
保证类在内存中的唯一性
根类加载器
扩展类加载器
系统类加载器
用户自定义的类加载器
获取类加载器
获取当前类的classLoader clazz.getClassLoader()
获取线程上下文的ClassLoader Thread.currentThread().getContextClassLoader()
获取系统的ClassLoader ClassLoader.getSystemClassLoader()
获取调用者的ClassLoader DriverManage.getCallerClassLoader()
线程上下文类加载器
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是由Java核心库所提供的,
而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他
来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类的加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他
来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类的加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
连接
验证
确保被加载类的正确性
准备
为类的静态变量分配内存,并将其初始化为默认值
解析
把类中的符号引用转换为直接引用
初始化
为类的静态变量赋予正确的初始值
初始化一个类时,并不会先初始化它的父接口
在初始化一个接口时,并不会先初始化它的父接口
结论:一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化
使用
主动使用
创建类的实例
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射
初始化一个类的子类
Java虚拟机启动时被标明为启动类的类
JDK1.7后提供动态语言支持
被动使用
除了主动使用的7种情况,都看作对类的被动使用,都不会导致类的初始化
类的实例化
为新的对象分配内存
为实例变量赋默认值
为实例变量赋正确的初始值
垃圾回收和对象终结
卸载
GC调优
堆外内存

收藏
0 条评论
下一页