JVM基础篇1 - Class的加载
JVM基础篇2 - 指令集
JVM进阶篇1 - 内存模型
JVM进阶篇2 - GC垃圾回收
JVM总览- JVM架构
1. 引入问题: 什么是垃圾?
垃圾:没有任何引用指向它
C++ 对比 java
java
- GC处理垃圾
- 开发效率高,执行效率低
C++
- 手工处理垃圾
- 忘记回收(可能会导致内存泄漏)
- 回收多次(导致非法访问)
- 开发效率低,执行效率高
2. 怎样找到垃圾?
2.1 引用计数器
把计数器为0的清理,不采用,低效
缺点:需要为每一个对象分配一个计数器,计数器本身会有消耗,也不能解决循环引用的问题.
2.2 根可达算法:
解决引用计数器中不能循环引用的问题
当程序运行时,将根对象取出,由根对象出发往下查找,最终找不到的对象,都视为无法由根对象找到,也就是说找不到的对象就都视为垃圾 。
根对象:
- JVM stack(线程栈变量)
- native method stack(本地方法栈)
- static references in method area(方法区里的静态引用)
- run-time constant pool(运行常量池里的对象)
如图: 红色圈起来的相互引用的对象会被视为垃圾,因为无法从根对象找到他们。
3.找到垃圾,该怎么清除他们呢?
3.1 Mark-Sweep(标记清除)
两遍扫描 ,先扫描标记,再清除
能从GC Root 找到的都是不可回收的
- 优点: 算法相对较简单, 适用于对象比较多的情况下效率高(不适合伊甸园区)
- 缺点:两边扫描,效率低,容易产生碎片
3.2 Copying(拷贝)
内存一分为二,把上面的能从GC root找到的 copy到下面,把上面清空,存活的放到下面
- 优点 : 只扫描一次,没有碎片,效率高,适用于存活对象较少的情况(适合伊甸园区)
- 缺点: 浪费一半的空间,上面移动到下面,移动复制对象,需要调整对象的引用
3.3 Mark-Compact(标记压缩)
扫描两遍,先找到空位置,再把存活的对象移到该位置
- 优点: 不会产生内存碎片,方便对象分配,不会产生内存减半
- 缺点:扫描两次,需要移动对象,效率偏低(若多线程处理这个问题,还需涉及到多线程之间的同步问题)
总结:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度的问题)
内存整齐度:复制算法=标记压缩算法>标记清除算法
利用率:标记压缩算法=标记清除算法>复制算法
思考:难道没有更优的算法吗?答案:没有,没有最好的算法,只有最合适的算法
年轻代特点:
- 存活率低
- 复制算法!
老年代特点:
- 存活率高,区域大
- 标记清除(内存碎片不是太多的时候)+标记压缩混合实现
4.堆内存逻辑分区:
类加载器读取了类文件后,一般会把什么东西放到堆中?
类, 方法,常量,变量~,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域:
●新生区(eden(伊甸园区) + survivor (幸存区) ) Young/New
●养老区 old
●永久区Perm(JDK1.8中已经被移除,被方法区的 Metaspace(元空间)取代)
4.1survivor(幸存区):
本在Eden区的对象经过一次GC后没被清除就移动到 survivor区了,在两个survivor区(from区,to 区)经过多次copying算法还没被淘汰(达到年龄后,一般15次轻GC(对应GC age标志位共4位)),就会被转到Old区
- 若from区 -> old区 的对象超过 50% ,会把from区年龄大的对象放到Old区
4.2 GC概念:
- MinorGC/YGC :年轻代空间耗尽了触发
- MajorGC/FullGC :老年带无法继续分配空间,新生区和老年区一块进行GC
4.3 对象分配过程:
栈上分配,线程本地分配 TLAB
5.垃圾清除器:
5.1 Serial收集器
使用内存大小:
Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程收集器,但“单线程”并非指该收集器只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的。
收集器对于运行在Client模式下的虚拟机来说是一个很好的选择
适用于 :内存较小 几十MB
5.2 Parallel Scavenge 收集器
默认GC器 po(Parallel old) + ps ( Parallel Scavenge )
Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。
多线程清理垃圾
适用于: 上百兆 - 几个G
5.3 ParNew 收集器
ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器。
与 Parallel Scavenge的区别 :做了一些改动以便和CMS配合使用,CMS某个阶段的适合 PerNew会启动
5.4 CMS收集器
算法:三色标记 + Incremental Update
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。是基于“标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:
-
初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
-
并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
-
重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
-
并发清除(CMS concurrent sweep),会有浮动垃圾(在并发清理时产生的垃圾)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作(尽量不要有FGC,但还是会有)。
存在问题 :
- Memory Fragmentation 内存碎片
- Floating Garbage 浮动垃圾 (解决办法:降低FGC的阈值,预留一定空间给浮动垃圾 )
适用于: 20G左右
5.5 G1(garbage first)收集器
算法 : 三色标记 + SATB
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。
概念分区,物理上不存在
特点
- 并发收集:并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 容易预测GC的暂停时间:可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
适用于:上百 G 内存
card table:
card 类是于分页,card记录的是一个个card
- 由于做YGC时,需要扫描整个Old区,效率非常低,所以JVM设计了CardTable,如果一个Old区CardTable中有对象指向Y区,就将他设为Dirty,下次扫描时,只需要扫描Dirty Card Table, 在结构上,Card Table 使用bitMap实现
Cset (Collection Set)
一张表,里面放到是需要回收的card
Rset (RememberedSet)
不需要整个扫描整个表,记录了谁引用了我这个对象,查应引用时就不需要查别人的整个表,只需扫描自己的Rset,这也是G1 高效回收的关键
如果G1 产生FGC该怎么处理?
- 扩内存
- 提高CPU性能(加快回收速度)
- 降低MixedGC(类似于 CMS过程)的触发阈值,让MixedGC提早发生,早点发生垃圾回收(默认是45%)
G1正常垃圾回收: MixedGC , FullGC 使用 serial
6.三色标记法:
- 白色:没有检查
- 灰色:自身被检查了,成员没被检查完(可以认为访问到了,但是正在被检查,就是图的遍历里那些在队列中的节点)
- 黑色:自身和成员都被检查完了
Lost Object Problem (漏标)
在我们标记的时候,用户Application还在运行着呢,他可能会修改这个图的结构,引入一个新问题 漏标(本来是活着的对象,但是由于没有遍历到,被当成垃圾回收了):
解决办法:
- incremental update(增量更新),关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性(CMS使用)
- SATB( sanpshot at the beginning) 关注引用的删除,大部分B-> D消失时,要把这个引用推到GC的堆栈,保证D还能被GC扫描到(G1使用)
为什么G1要使用 SATB ?(面试题)
当灰色 - > 白色的引用消失时,如果没有黑色指向白色的引用,白色的将会被push到堆栈,下次扫描时拿到这个引用,由于有Rset的存在(就不需要扫描整个region,节省时间),不需要扫描整个堆区查找指向白色的引用,效率比较高,SATB配合Rset效率高