JVM垃圾收集器与内存分配策略
文章目录
GC需要完成的三件事情
- 哪些内存需要回收
- 什么时候回收
- 如何回收
一、哪些内存需要回收
堆中存放着Java中大量的对象实例,在对堆进行垃圾回收前,要确定哪些对象是可以被回收的
1、引用计数法
为对象添加一个引用计数器,每当有一个地方引用该对象时,计数器的值就加一;当引用失效时,计数器值就减一;当计数器的值为0的时候就代表该对象不可能再被使用。
优点:实现简单,判定效率高
缺点:会出现循环引用问题
class A {
B b = new B();
}
class B {
A a = new A();
}
此时class A 与 class B 均没有其他对象使用,但是却不能够被回收
2、根搜索算法
通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用连接,则证明该对象不可用。
在Java中,可作为GC Roots的对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈JNI(Native方法)引用的对象
3、什么是引用
无论是引用计数法还是根搜索算法都提到了对象引用,那么到底什么是对象的引用呢?
在JDK1.2之后引用被分为四种(强度依次降低):
- 强引用
- 软引用
- 弱引用
- 虚引用
强引用
在代码中普遍存在的,类似"Object obj = new Object()",只要强引用还存在,垃圾收集器就不会会受到被引用的对象
软引用
描述一些还有用,但非必须的对象。对于软引用对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收
弱引用
描述非必须的对象,但强度相比软引用更弱一些。被弱引用关联的对象只能受存在下一次垃圾收集发生之前,无论当前内存是否足够,都会回收
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例
4、对象的标记
在根搜索算法中不可达的对象,也并非一定被回收,此时它们处于“缓刑”阶段,要真正进行回收需要经历两次标记过程:
第一次:对象在进行根搜索后没有发现与GC Roos相连接的引用链,那么他会被第一次标记并且进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被调用过。
如果当前对象被判定为有必要执行finalize()方法,则该对象会被放入F-Queue队列中,并在由虚拟机自动建立的、低优先级的Finalizer线程执行,但是虚拟机不保证finalize()方法一定会被执行。
第二次:稍后GC会对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()方法中重新与引用链上的对象建立关联,则该对象被救活,不会回收,否则就死掉。
5、回收方法区
方法区中的垃圾回收一般不进行,回收性价比较低。
主要回收内容:
- 废弃常量
- 无用的类
废弃常量
回收常量与对象的回收非常类似,例:字符串"abc"进入常量池,但是没有任何一个String对其引用,进行垃圾回收时"abc"就会被回收
无用的类
无用的类判定条件:
- 该类所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有被引用,无法通过反射访问该类的方法
类满足条件可以被回收,但是不一定被回收,需要使用虚拟机参数进行控制
-Xnoclassgc
二、垃圾回收算法
- 标记清除算法
- 复制算法
- 标记整理算法
- 分代收集算法
1、标记清除算法
分为“标记”和“清除”两个阶段:首先标记处需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点:效率较低;标记清除后会产生大量内存碎片,当系统需要分配一个较大对象时,可能找不到足够的连续内存
2、复制算法
为解决效率问题,出现了复制算法,它将内存按照容量分为大小相等的两块,每次只使用其中的一块,当一块内存用完后将仍然活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
缺点:内存利用率较低
改进
有研究表明,新生代中的对象98%都是朝生夕死的,所以并不需要按照1:1划分内存空间。而是分成一块较大的Eden空间(80%)和两块较小的Survivor空间(各10%)(分别为From Survivor和To Survivor)。每次使用Eden和From Survivor。当回收时将Eden和From Survivor中存活的对象复制到To Survivor中,之后交换To Survivor和From Survivor
(在内存分配中会详细说明)
3、标记整理算法
对于标记清除算法增加了“整理”步骤,即垃圾回收完成后,将剩余存活对象移动,减少内存碎片
4、分代收集算法
根据对象的存活周期将内存划分为几块。一般把Java堆分为新生代和老年代。之后根据各个年代的特点采用最适合的垃圾收集算法。
三、垃圾收集器
1、Serial收集器
是一个单线程的收集器,在进行垃圾收集时需要暂停其他的工作线程(Stop the World事件),由虚拟机在后台自动发起和自动完成的。
优点:简单高效
缺点:在进行垃圾回收时,需要暂停用户的所有其他线程
2、ParNew收集器
Serial收集器的多线程版本,使用多条线程进行垃圾收集。是Server模式下的虚拟机中首选的新生代的首选收集器
可以使用-XX:ParallelGCThreads参数限制垃圾收集的线程数
3、Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,使用复制算法的收集器并且支持并行的多线程收集器
Parallel Scavenge目标是达到一个可控制的吞吐量,而不强调垃圾回收Stop The World的实时性
-XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间
-XX:MaxGCTimeRatio 设置吞吐量大小
4、Serial Old收集器
Serial收集器的老年代版本,使用标记整理算法
5、Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
6、CMS收集器
是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法
可以实现并发收集
整个过程分为四个步骤:
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快,需要(Stop The World)
- 并发标记:进行GC Roots Tracing
- 重新标记:修正并发标记期间因为用户程序继续运行而导致标记产生变动的一部分对象的标记记录,需要(Stop The World)
- 并发清除
缺点:
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾(浮动垃圾:GC过程中产生的新垃圾本次无法清除被称为浮动垃圾)
- 基于标记清除算法,产生大量空间碎片
7、G1收集器
-
G1收集器基于“标记-整理”算法实现,不会产生空间碎片。
-
可以精确控制停顿,让使用者指定必须在M毫秒的时间片内消耗垃圾收集的时间不超过N毫秒
-
可以回收新生代和老年代,将Java堆划分为多个大小固定的区域,并跟踪这些区域中的垃圾堆积程度,在后台维护一个优先列表,在允许垃圾回收的时间内,优先回收垃圾最多的区域
四、内存分配和回收策略
对象的内存分配基本在堆中分配(也有可能经过JIT编译器后被拆散为标量类型并间接在栈上分配)。主要分配在Eden区,少数情况分配在老年代中。
1、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机发起一次Minor GC
Minor GC过程:
- Eden区空间不足
- 进行垃圾回收,内存清理
- 将存活对象放入From Survivor,并将年龄+1
- 交换To Survivor成为From Survivor
2、大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象。例:超长字符串及数组
分配时忌讳大量朝生夕死的大对象,会导致频繁的Full GC,降低系统速度
设置大对象判定的阈值:-XX:PretenureSizeThreshold
3、长期存活的对象进入老年代
虚拟机为每个对象定义一个年龄计数器。在Eden出生的对象,并经过一次Minor GC后仍然存活,年龄+1,当年龄大于阈值之后会晋升到老年代中
阈值设置:-XX:MaxTenuringThreshold 默认15岁
动态年龄判断
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
4、空间分配担保
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为一次Full GC。
空间分配担保就是:当Survivor已经无法容下新对象之后可以直接进入老年代,也就是需要老年代的担保(前提是老年代还有容纳这些对象的剩余空间)但是在实际内存回收前是无法知道的,所以需要取每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间比较,决定是否进行Full GC腾出更多空间