第3章-垃圾收集器和内存分配策略
1. 垃圾回收(GC)的任务主要解决以下三个问题:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
1.1 哪些内存需要回收
程序计数器只是作为虚拟机在执行字节码指令时的一个数值标记,不会占用内存,或者只会占用一点点内存。java虚拟机栈和本地方法栈作为栈的特性,方法的调用到执行完毕,其实都是一个栈帧入栈出栈的过程,所以肯定也不需要过多的内存,不需要GC。因此只有Java堆和方法区需要GC。
2,3两个问题涉及这一章中的很多内容,需要一一展开。
1.2 对象已死?
在jvm进行垃圾回收之前,首先要确定那些对象仍然存活,哪些对象已经死去(不可能再被任何途径使用的对象)
判断对象是否可用(死)的算法:
-
引用计数法
- 算法简述:
- 给每个对象一个引用计数器,如果当前对象被引用,则计数器加一,当引用失效,计数器减一,计数器为0时,表示当前对象已死,不可使用。
- 缺点:
- 难以解决对象之间的循环引用问题,即
objA.instance = objB; objB.instance = objA;
,如果ObjA和ObjB都不再被访问,说明此时GC可以进行回收,但是由于二者相互引用,导致他们各自的引用计数器都不为0,所以无法回收。
- 难以解决对象之间的循环引用问题,即
- 算法简述:
-
可达性分析算法(主流)
- 算法简述:
- 将GC root对象作为起始点,从这些节点向下搜索,所走过的路径成为引用链
- 如果在向下搜索的过程中,发现某个对象节点不可达,也就是GC root和他之间不存在引用路径,说明此节点不可达
- 但是,节点不可达,并非宣告当前对象立即死亡,后续还有两次标记的机会
- java中什么对象可作为GC Roots
- java虚拟机栈中的局部变量表中引用的对象
- 本地方法栈 native方法引用的对象
- 方法区类静态属性引用的对象
- 方法区常量引用的对象
- 算法简述:
-
JDK1.2之后,引用扩充
- 强引用
Object obj = new Object()
此时便是强引用- 只要强引用存在,则一定不可能被GC
- 软引用
- 描述还有用但是非必须的对象
- 当JVM将要发生OOM之前,将会把软引用标记的对象列入可回收范围,进行第二次回收,如果第二次回收还是没有足够的内存,才回抛出OOM
- 弱引用
- 描述非必须对象
- 被弱引用关联的对象,只能生存到下一次垃圾回收之前,也就是说,只要发生垃圾回收,无论内存是否充足,都会回收掉这些对象。
- 虚引用
- 也被称为幻影引用、幽灵引用
- 这种引用对于对象没有任何关系,也不会对对象产生任何影响。
- 设置虚引用的唯一目的就是当虚引用对象被回收时,发送一个系统通知。
- 强引用
-
两次标记法判断对象是否死亡
我们说,主流jvm使用的判断对象是否不可用算法中,也就是可达性分析算法,如果该算法检测到某个对象节点不可达,但并不能直接将当前对象节点判处死刑,而是判处死缓,还需要后续的判断才能得出是否对象节点已死,可以回收。
流程简述
- 如果可达性分析算法判断当前对象节点不可达,那么当前对象节点会被标记,记住这是第一次标记,同时还需要进行一次筛选,筛选的条件如下:
- 当前对象有没有必要执行finalize方法。
- 如果当前对象没有覆盖finalize方法,或者虚拟机已经调用过finalize方法,则认为没有必要执行finalize方法,那么这类对象节点直接被回收
- 否则有必要执行finalize方法。
- 当前对象有没有必要执行finalize方法。
- 如果当前对象节点,被筛选之后,有必要执行finalize方法,那么这个对象将会被放入F-Queue队列中。并由虚拟机的低优先级的线程去执行finalize方法。
- 此时是对象逃离死亡的最后一次机会,在finalize方法执行时,GC会对对象进行第二次标记。如果成功标记,则被回收,否则逃离GC。
- 如何逃离呢?
- 重新与引用链上的其他对象关联即可,具体可看书籍p66-67页代码。
- 如何逃离呢?
- 如果可达性分析算法判断当前对象节点不可达,那么当前对象节点会被标记,记住这是第一次标记,同时还需要进行一次筛选,筛选的条件如下:
-
任何对象的finalize方法都只会被系统调用一次
1.3 回收方法区
方法区的垃圾回收一般效率较低,方法区的垃圾回收主要在永久代。
永久代垃圾回收主要两个方面:
- 废弃常量
- 例如一个字符串已经进入常量池,但是当前没有引用指向它,则他是废弃常量
- 无用的类
- 该类所有的实例都被收回,java堆中不存在该类的任何实例。
- 加载该类的classloader已经被回收。
- 无法通过反射访问该类的方法
2.垃圾回收算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9EaJFWRD-1599120408713)
垃圾回收算法主要包括以下三种。
2.1基础:标记-清除算法
-
算法简述
- 标记:首先标记需要回收的对象
- 清除:待标记完成后统一回收已标记对象
-
缺点:
- 效率问题:标记和清除效率都不算高
- 空间问题:标记清除之后会产生大量的内存碎片,不利于大对象的存储。
2.2 解决效率问题:复制算法
-
算法简述:
- 首先将内存划分为大小相等的两块,每次给对象分配内存的时候只使用其中的一块,当着一块内存用完,则直接将所有存活着的对象全部复制到另一半区。
- 将已使用过的内存空间一次清理掉。
-
优缺点:
- 每次都在内存的半区进行分配,不需要考虑内存碎片的问题。
- 但是每次也只利用半个内存区域,有点浪费。
-
知识迁移·商业虚拟机如何进行垃圾回收:
- 我们说上述的复制算法,每次都将内存区域划分为1:1的两块,但却会带来内存的浪费。商业上的使用并不如此。
- 98%的对象都是朝生夕死的
- 将内存划分为1个较大的Eden空间和2个较小的Survivor空间。每次使用Eden和其中的一个Survivor。Eden:Survivor1:Survivor2 内存比例为8:1:1,也就是说新生代中可用的内存占新生代总内存的90%。
- 当进行垃圾回收时,将Eden和Survivor中存活的对象一次性复制到另外一个Survivor区域,然后清理掉Eden和Survivor。这样只浪费掉10%的内存空间。
- 当Survivor内存空间不够用的时候,需要依赖其他内存,也就是老年代进行分配担保。
- 我们说上述的复制算法,每次都将内存区域划分为1:1的两块,但却会带来内存的浪费。商业上的使用并不如此。
2.3 解决碎片化问题:标记-整理算法
算法简述
- 标记需要回收的对象
- 让所有存活着的对象向一端移动,然后直接清理掉边界之外的内存。
2.4 分代收集算法
根据对象的存活周期,将内存分块,每一块各自处理。
- 新生代:
- 每次垃圾回收都会有大量的对象死亡,只有少量的对象存活下来,使用复制算法。
- 老年代:
- 老年代中对象存活率高,同时没有额外空间进行分配担保。使用标记整理算法。
3.内存分配与回收策略
对象的内存分配,就是在java堆上分配。
3.1 先看一个问题
Minor GC 和 Full GC是什么,有什么不同?
- Minor GC是发生在新生代GC中的,新生代中的对象绝大多数都是朝生夕死的,所以需要大量频繁的Minor GC,回收速度较快。
- Full GC也成为了Major GC,是发生在老年代上的GC,每次发生Major GC的时候,经常会伴有至少一次得Minor GC,Major GC速度一般会比Minor GC慢10倍以上。
三条分配策略
- 对象优先在Eden分配
- 大多数情况下,对象在新生代的Eden区分配
- 当Eden区中没有空间,则进行一次Minor GC
- 大对象直接进入老年代
- 大对象指的是:需要大量连续内存空间的java对象,例如和很长的字符串和数组。
- 长期存活的对象将进入老年代
- 固定年龄绑定:
- 虚拟机采用分代的思想来管理内存,那么内存回收的时候,就必须能够识别那些对象应该放在新生代哪些应该放在老年代。
- 虚拟机给每个对象一个年龄计数器
- 当对象在Eden中出生,并且经过第一次Minor GC之后仍然存活,并且Suvivor容量足够大,对象成功移至Suvivor中,此时,age计数器为1.
- 当对象在Suvivor中每熬过一次Minor GC,Age就加一。当age达到阈值,默认15,则进入老年代。
- 动态年龄绑定:
- Survivor 中有相同年龄的对象的空间总和大于 Survivor 空间的一半,那么,年龄大于或等于该年龄的对象直接晋升到老年代。
- 固定年龄绑定:
- 空间分配担保
- JVM的对象创建之后,大多数将存储在新生代的Eden区域,由于新生代的特点,其采用的是复制算法作为垃圾回收机制算法,当发生Minor GC时,会将Eden和Survivor1中所有存活的对象,复制到Survivor2中,但当Survivor2中的空间不足,此时对象将直接进入老年代,进行分配担保。
- 进入老年代也需要个前提条件,就是老年代的空间要大于Eden和Survivor中的所有存活对象的大小。
- 在发生Minor GC之前,JVM将会先检查老年代中的最大连续可用空间大小是否大于新生代中所有存活的对象
- 如果大于,则Minor GC一定是安全的。因为老年代的空间可以兜底。
- 如果小于就不会进行MinorGC,而是先进行老年代的Full GC又称Major GC。
永久代被元空间取代
之前我们讨论过方法区和永久代的关系,在JDK8中,永久代被移除,取而代之的是元空间。
移除永久代的原因
- 永久代经常内存溢出
java.lang.OutOfMemoryError: PermGen
,不方便管理。 - 促进JVM和JRockit VM的融合。
移除之后,方法区和字符串常量的位置
- 方法区:移动至元空间
- 字符串常量:移动至Java Heap
元空间的位置
- 本地堆内存
元空间的有点
- 永久代中的OOM问题不复存在,因为默认的类信息数据分内存分配只受本地内存大小的限制。本地内存可用多少,理论上元空间就会有多大。
便管理。
- 促进JVM和JRockit VM的融合。
移除之后,方法区和字符串常量的位置
- 方法区:移动至元空间
- 字符串常量:移动至Java Heap
元空间的位置
- 本地堆内存
元空间的有点
- 永久代中的OOM问题不复存在,因为默认的类信息数据分内存分配只受本地内存大小的限制。本地内存可用多少,理论上元空间就会有多大。