JVM问题式学习
2020-09-14 15:23:41 0 举报
AI智能生成
JVM问题式学习,连贯性非常好,专门针对基础不好的人
作者其他创作
大纲/内容
java vs jvm
1. Java怎么到JVM的?
java文件编译成class文件
class文件通过ClassLoader加载到JVM
java文件怎么编译成class文件?
1. 编译原理
1) 语法树
2)……
1) 语法树
2)……
Class文件怎么通过ClassLoader加载到JVM
双亲委托机制
破坏双亲委托机制
概念
1. 什么是jvm
Java Virtual Machine,Java虚拟机
2. 为什么要创建jvm
各个操作系统的底层调用都不一样,实现一样东西的跨平台,没有虚拟机的概念就需要各个系统都实现一遍。浪费人力
后面java因为其生态大行其道。广泛运用在商业生产上。
3. 垃圾回收器
c/c++语言直接对内存,需要显式释放内存。
jvm内部有一个东西称为垃圾回收器,同一回收无用的内存
垃圾回收器
对应c/c++一对一释放内存,jvm由垃圾回收器统一回收
统一回收怎么回收呢?
先分两大步
1. 判断哪些内存是垃圾
2. 回收
统一回收第一个问题:怎么判断什么内存是垃圾
这个问题非常复杂
业界有两种做法
1. 引用计数法
2. 对象(一个抽象的模型)可达不可达
java一开始采用第一种做法,后面都采用第二种做法了
想知道那些内存分配是垃圾,得先知道jvm的内存分布
统一回收第二个问题:怎么清除内存
这个问题还算简单
内存清0
基于以上的两个问题,怎么实现内存回收——内存回收的算法
1. 标记清除法
1. 标记
2. 清除/回收
2. 标记整理法/标记压缩法
1. 标记
2. 整理/复制
3. 清除/回收
3. 复制算法
1. 标记
2. 整理/复制
3. 清除/回收
这个复制算法和标记整理法其实步骤是一模一样的
区别在于,复制算法会将自己可控的内存部分按比例划分
一般这样划分:Eden:Survivor1:Survivor2 = 8:1:1
它从Eden复制到Survivor1,Survivor2,或者Survivor互相复制等等
标记整理法复制的是自己内存内部的复制
区别在于,复制算法会将自己可控的内存部分按比例划分
一般这样划分:Eden:Survivor1:Survivor2 = 8:1:1
它从Eden复制到Survivor1,Survivor2,或者Survivor互相复制等等
标记整理法复制的是自己内存内部的复制
4. 分代算法
分为两代:年轻代+老生代
YGC
MixedGC
FullGC
目前主要采用这种方式。
年轻代采用复制算法,称为YGC
老生代采用 标记清除法/标记整理法
年轻代采用复制算法,称为YGC
老生代采用 标记清除法/标记整理法
具体实现
内存组织方式
需要参考本人jdk内存模型图
怎么使用内存
1. 内存分配
2. 内存使用
3. 内存释放
执行引擎
C/C++的执行是基于操作系统的,所以针对不同的操作系统,需要开发多次
C/C++编译之后,直接生成操作系统的可执行文件
它们是基于寄存器运行的。寄存器是在CPU的高速缓存的。
C/C++编译之后,直接生成操作系统的可执行文件
它们是基于寄存器运行的。寄存器是在CPU的高速缓存的。
首先JVM是基于栈运行的。栈是在内存中的。
大白话讲就是把class文件翻译成汇编语言执行的。所以Java运行比C/C++慢。
大白话讲就是把class文件翻译成汇编语言执行的。所以Java运行比C/C++慢。
垃圾回收器具体实现
1. 怎么判断对象是否是垃圾?
扫描每个对象和没个对象的field的引用。判断哪些有用,哪些没有。
2. 怎么扫描每个对象?
扫描栈帧(虚拟机栈,本地方法栈),元空间(方法区,包括元数据+常量)
3. 扫描的时候,是多线程操作,如果对象引用变化非常频繁怎么办?
STW+三色标记法
4. 什么是STW
在这里有个STW的概念
STW:Stop The World,就是暂停整个jvm。
包括用户线程/内存开辟,计算,引用等等,
所有的线程进入一个安全点,然后等其它线程扫描哪些对象是垃圾
STW:Stop The World,就是暂停整个jvm。
包括用户线程/内存开辟,计算,引用等等,
所有的线程进入一个安全点,然后等其它线程扫描哪些对象是垃圾
5. 什么是安全点/安全区域?
jvm让所有的用户线程/本地方法线程 进入到一个 点,称为安全点,然后不执行其它任务。等待GC线程收集垃圾。
如果一些线程很久不进入安全点,jvm就会放宽范围,只要进入到安全区域就行。
6. 基于上面的理论,怎么实现一个最简单的垃圾收集器?
我们先看我们简单内存的使用场景:
1. 开辟内存
2. 使用内存
3. 释放内存
c/c++的使用也是像上面那样,只是Java帮我们简化了这部分概念,特别是第三步释放内存。
这里Java与C/C++的区别就是我们可以Java中批量开辟内存,然后使用内存,不考虑释放,最后JVM会STW然后帮我们释放。C/C++中如果这样做出现内存泄漏的可能性非常大。
1. 开辟内存
2. 使用内存
3. 释放内存
c/c++的使用也是像上面那样,只是Java帮我们简化了这部分概念,特别是第三步释放内存。
这里Java与C/C++的区别就是我们可以Java中批量开辟内存,然后使用内存,不考虑释放,最后JVM会STW然后帮我们释放。C/C++中如果这样做出现内存泄漏的可能性非常大。
针对垃圾收集器这个专题。我简化步骤如下:
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
......
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
1. 开辟内存+使用内存
2. STW
3. 扫描GC Roots,并且标记
4. 释放内存
5. 结束STW
......
JVM之中的古老的Serial垃圾收集器采用这个方案
Serial垃圾收集器,年轻代使用标记清除法,老年代使用标记整理法
7. 后面的垃圾收集器都是针对上面的👆优化
1. ParNew
2. Parallel GC
3. CMS
4. G1
目前商用是这个比较多,jdk7引入,jdk9作为默认回收器
具体可以参考我的G1调优总结
官网介绍:
https://www.oracle.com/java/technologies/javase/hotspot-garbage-collection.html
https://www.oracle.com/java/technologies/javase/hotspot-garbage-collection.html
G1的推荐用例
G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。
如果当前具有CMS或ParallelOld垃圾收集器运行的应用程序具有以下一个或多个特征,则将其切换到G1将非常有益。
超过50%的Java堆被实时数据占用。
对象分配率或提升率差异很大。
不必要的长时间垃圾收集或压缩暂停(长于0.5到1秒)
G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。
如果当前具有CMS或ParallelOld垃圾收集器运行的应用程序具有以下一个或多个特征,则将其切换到G1将非常有益。
超过50%的Java堆被实时数据占用。
对象分配率或提升率差异很大。
不必要的长时间垃圾收集或压缩暂停(长于0.5到1秒)
5. ZGC
最有潜力的GC
各个垃圾回收器优化的情况都是针对减少停顿时间的,具体情况可以看到我的图
8. G1 的GC分类
1. YGC,也称为 Minor GC
YGC是采用复制算法
具体实现原理探索
复制算法是把内存划为3部分:Eden:Survivor1:Survivor2
它的产生来源是假设对象都是朝生夕死,用完就对。据统计一般我们的应用中这种情况非常多,所以我们Eden区用来创建对象,Survivor区用来存放过了Eden用了一次之后,还存在的对象。
YGC每次都检查Eden和一个Survivor,扫描之后。把Eden存活的还有扫描的Survivor存活的对象搬到另外个Survivor中,或者已经超过一定存活次数的,搬到老生代中。
它的产生来源是假设对象都是朝生夕死,用完就对。据统计一般我们的应用中这种情况非常多,所以我们Eden区用来创建对象,Survivor区用来存放过了Eden用了一次之后,还存在的对象。
YGC每次都检查Eden和一个Survivor,扫描之后。把Eden存活的还有扫描的Survivor存活的对象搬到另外个Survivor中,或者已经超过一定存活次数的,搬到老生代中。
从上面需要解决一些问题:
1. 在YGC中扫描GC Roots时怎么判断哪些GC Roots是YGC的,难道需要全部根都扫描一次?
2. 怎么知道存活的对象?
1. 在YGC中扫描GC Roots时怎么判断哪些GC Roots是YGC的,难道需要全部根都扫描一次?
2. 怎么知道存活的对象?
1. 在YGC中扫描GC Roots时怎么判断哪些GC Roots是YGC的,难道需要全部根都扫描一次?
针对这个问题,JVM设计了CSet这种概念。
CSet: Collection Set
CSet我理解会存放所有的GC Roots,并且GC Roots是有办法区分开的,比如有标志区分是年轻代/老生代。
如果性能高一点就会有不同的容器区分开年轻代/老生代。
如果性能高一点就会有不同的容器区分开年轻代/老生代。
YGC会选择对应的 CSet,即年轻代的 CSet
2. 如果有老生代的对象指向新生代的对象,改怎么处理?
跨代的处理,G1引入了RSet
RSet记录这对象跨HR之间的引用。这里的跨代,只有老年代指向年轻代,还有就是老年代指向老年代。
3. RSet怎么存放引用?RSet是全局吗?
1. RSet采用Point In的方式存放引用
1. 比如ObjA.field = ObjB. 那么在ObjB的RSet就会存放ObjA
这样的好处是扫描RSet只用扫描一层,扫描到ObjA就可以认为这是Root。否则就需要继续扫描到更深的地步。
2. RSet并不是全局的,RSet是每一HR一个。
4. 有了上面的扫描理论的基础,处理了根的引用还有跨HR之间的引用,差不多全了,那么我们可以猜测一下YGC的过程
1. STW
2. 扫描栈获取Eden的栈直接指向的对象。将对象从Eden 复制到Survivor
这里有个问题,我对象里面的field,这些引用怎么办?继续深层次的扫描再搬还是放入到队列之中第二次再搬?
G1采用的是第二种做法,将field放入一个queue之中(PSS队列)过后再来处理
G1采用的是第二种做法,将field放入一个queue之中(PSS队列)过后再来处理
3. 变更栈/老生代指向,指向到Suvivor。
4. 扫描RSet,同样可以认为根扫描到的对象是Root,然后复制到新的Survivor/老生代之中。
这里有个很大的问题?RSet什么时候写入的,这里去扫描,总得有地方写入吧。
上面的栈在对象分配的时候写入,这是毫无疑问的。
RSet的维护是一个大 工程,G1专门设计了一组线程Refine线程来管理RSet。Mutator线程也有可能直接更新RSet。
上面的栈在对象分配的时候写入,这是毫无疑问的。
RSet的维护是一个大 工程,G1专门设计了一组线程Refine线程来管理RSet。Mutator线程也有可能直接更新RSet。
5. 变更栈/老生代对象的指向。
这里有个问题,为什么第3步和第5步没有合并?
没找到相关的资料,个人理解是代码复杂度的增加,都是性能没增加多少。
因为第4,5步有很多代码是复用第2,3步的代码
因为第4,5步有很多代码是复用第2,3步的代码
6. 扫描PSS队列,在PSS队列(G1ParScanThreadState)之中的对象都是活跃对象。每个对象直接复制到Survivor去。然后继续针对它的field,判断是否是在YGC的CSet中,在则把对象加入到PSS队列,待处理。
然后一直循环到PSS队列为空位置。
然后一直循环到PSS队列为空位置。
2. MixedGC
3. Full GC
0 条评论
下一页