回顾JVM的内存分配包括程序计数器,虚拟机栈,本地方法栈,JAVA堆和方法区5部分,其中前三是属于线程私有的,内存分配和回收有确定性,不多考虑回收的问题,因为线程或者方法结束时,内存自然回收。相对的,在JAVA堆和方法区里,需要注意内存回收的问题。
-
对象存活判断方法
1.1 引用计数法,非Java
给对象添加一个引用计数器,当有引用时,给之加一,引用减少,给之减一,当计数器为0代表不再存在引用。
️无法解决循环引用
1.2 可达性分析
通过定义GC Roots作为起始点,从这些节点往下搜索,路径称为引用链,当一个GC roots没有任何引用链相连,则证明对象不可用。如下o5,o6和GC roots是不可达的,如图:
可以作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象,即Native方法
java引用类型
2.1 强引用
普遍存在的类似Object o = new Object()的引用
2.2 软引用
描述一些还有用但是非必需的对象,系统在发生内存溢出之前,会把这些对象列入回收范围做第二次回收,如果还是没有足够内存,才会抛出异常。提供SoftReference类实现软引用。
2.3 弱引用
只能生存到下一次垃圾回收之前,即处在回收边缘。当垃圾回收时,无论当前内存是否足够,都会回收只被弱引用关联的对象。提供WeakReference类实现弱引用。
2.4 虚引用
对象的虚引用关系不会对其生存时间构成影响,也无法通过虚引用取得一个实例,设置虚引用的唯一目的是能在这个对象被回收内存时收到一个系统通知。提供一个PhantomReference来实现虚引用。方法区回收
3.1 回收废弃常量
常量没有被引用,或不存在对应字面值
3.2 无用的类
️是否回收通过参数控制,可以进行回收的条件如下:
- 类所有实例已经被回收,java堆中不存在该类的任何实例。
- 加载类的ClassLoader被回收
- 类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
4.1 标记-清除
基于之前的可达性分析,标记出所有需要回收的对象,在标记完成后统一回收。存在两个问题:1,效率,2,空间,会产生很多内存碎片。如果之后再分配大对象时,无法找到足够内存,而触发另一次新的垃圾收集动作。
4.2 复制-算法
将内存按容量分为大小相等的两块,每次只使用其中一块,当用完以后将还存活的对象复制到另一块上面,再把已经使用过的内存空间一次性清掉。每次只堆半个区进行回收,分配时也不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配内存即可。缺点是将内存分为两块,缩小了一半。
细节是将堆分为eden区和两个survivor区,一般比例是8:1,在survivor空间之间进行复制清理,也就只有10%的内存浪费。
️如果另一个survivor区没有足够空间存放上一次新生代收集下来的存活对象,这些对象通过分配担保机制进入老年代。
4.3 标记-整理
复制算法在对象存活率较高时,需要很多的复制操作,效率较低。如果不想类似分配survivor区,需要额外的内存担保,所以老年代不能直接选用复制算法。根据其特性,使用标记-整理方法。先进行内存标记,标记可回收对象,和标记清除不同,标记-整理让这些所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
4.4 分代收集
分为新生代和老年代。新生代每次垃圾回收都有大批对象死去,选用复制算法。老年代对象存活率高,没有额外空间进行分配担保,必需使用标记-清除或者标记-整理进行回收。垃圾收集器
5.1 Serial串行收集器
单独一个线程,新生代复制算法,老年代标记整理,都暂停所有用户线程,有很高的但线程效率,常用在client模式的服务上。
5.2 ParNew收集器
多个GC线程,并行多线程版本的Serial收集器,server模式下,首选的新生代收集器,与老年代收集器CMS配合。
5.3 Parallel Scavenge收集器
新生代收集器,采用复制算法,并行多线程,与ParNew不同在于通过参数设置吞吐量,GCTimeRatio-收集时间占比和MaxGCPauseMillis-最大GC时间。补充:通过UseAdaptiveSizePolicy设置根据监控数据自适应提供最大吞吐量。
5.4 Serial Old收集器
Serial收集器的老年代版本,单线程,主要用在client模式下。server模式主要和Parallel Scavenge配合,以及作为CMS的后备方案。
5.5 Parallel Old收集器
Parallel Scavenge的老年代版本,多线程,与Parallel Scavenge配合,保证吞吐量。
5.6 CMS收集器
低停顿,并发,最常用的收集器。收集过程有:初始标记,并发标记,重新标记,并发清除。缺点有以下3个:
- 并发的缘故,CPU资源敏感
- 无法清理浮动垃圾,垃圾不断新产生。如果导致CMS预留的内存无法满足需要,出现一次“Concurrent Model Failure” , 临时会启动Serial Old 收集器,关联参数,设置触发阈值,CMSInitiatingOccupancyFraction
- 标记-清除,导致内存碎片,分配大对象没有连续空间,导致提前FGC。关联参数:
UseCMSCompactAtFullCollection 在CMSfull GC时开启内存碎片整理过程。
CMSFullGCsBeforeCompaction 执行参数设置的次数FGC后,进行压缩的碎片整理。
5.7 G1
以下4个特性: - 并行和并发:利用多CPU,多核优势,缩短停顿时间。
- 分代收集:保留分代收集,但不再隔离。
- 空间整合:分成region区域,整体看类似标记-整理,region间类似复制,不会产生内存碎片,收集后有规整的可用内存。
- 可预测的停顿:有可预测的时间模型,指定停顿时间。因为可以有计划的进行全区域的垃圾收集。跟踪Region里的垃圾堆积的价值大小(回收后的空间和垃圾收集所需的时间经验值),后台维护一个优先列表,优先回收价值最大的Region,也是其名称G1(Garbage First)的由来。
️可能的问题:region区域之间对象的引用,是否需要扫描整个堆?
引申阅读:remember Set 在创建引用时,先检查引用关系,写入对应的region的remember set中,之后GC过程方便取到,避免全堆扫描。(个人理解)