1.引用计数法
引用计数法(Reference Counting)比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象的引用计数器的值为0,即表示对象A不能在被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:(1)他需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
(2)每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
(3)引用计数器还有一个严重的问题,即无法处理循环引用的问题,这是一条致命的缺陷,导致在Java回收的垃圾回收器中没有使用这类算法(java没有采用此类方法)。
public class xx {
xxx xxx1;
}
public class xxx {
xx xx1;
}
2.可达性分析算法
下图中object5对象则会被垃圾回收
那些可以当作GCRoot(黑马JVMp51):
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象(new ArrayList()),譬如各个线程被调用的方法 堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。 ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象 (比如 NullPointExcepiton、OutOfMemoryError)等。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
- 必须考虑到内存区域是虚 拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭 的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需 要将这些关联区域的对象也一并加入 GC Roots 集合中去
五种引用:
强引用:
上图实心线表示强引用:比如,new
一个对象M,将对象M通过=
(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。
强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当GC Root都不引用该对象时,才会回收强引用对象。
- 如上图B、C对象都不引用A1对象时,A1对象才会被回收。
软引用:上图宽虚线为软引用
软引用的特点:当发生了一次垃圾回收且系统内存不足够时这个时候软引用会被当做垃圾回收回收掉,
弱引用特点:当系统发生垃圾回收的时候不管内存充不充足都会回收弱引用对象
这里解释一下引用队列。当软引用 引用的对象被回收的时候,软引用这回被放入引用队列。(弱引用同理)
为什么要做这样的处理:因为软引用和弱引用自身也会占用一定的内存,如果你想释放他们自己的内存就需要使用引用队列(并不强制要求使用要和虚引用和终结器引用区别),依次遍历引用队列然后删除。(弱引用同理)
虚引用:虚引用 引用的对象被垃圾回收时,虚引用就会被放入引用队列,然后用一个线程调用虚引用的方法 ,如之前在直接内存那篇中虚引用Cleaner关联的对象被垃圾回收的时候就会调用Cleaner的clean
方法来释放直接内存
终结器引用:
- 所有的类都继承自Object类,Object类有一个
finalize()
方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()
方法。调用以后,该对象就可以被垃圾回收了。 - 如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的
finalize()
方法。调用以后,该对象就可以被垃圾回收了。
总结五种引用:
虚引用案例:
先设置JVM配置:-Xmx20m -XX:+PrintGCDetails
前一个时堆内存最大大小20m,后面时打印GC垃圾回收信息。
public static void main(String[] args) {
List<byte[]> list=new ArrayList<>();
for (int i=0;i<5;i++){
list.add(new byte[_4Mb]);
}
// soft();
上图可知使用强引用会报堆内存不足
改进使用软引用:
public static void main(String[] args) {
List<byte[]> list=new ArrayList<>();
// for (int i=0;i<5;i++){
// list.add(new byte[_4Mb]);
// }
soft();
}
public static void soft (){
List<SoftReference<byte[]>> list=new ArrayList<>();
for (int i=0;i<5;i++){
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4Mb]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
发现打印并没有报错但是只有最后一个保留下来了, 这就是触发软引用机制清除了部分软引用。
改进:上面例子发现有4个null对象,虽然时空对象但是依然占用一定的内存现在用引用队列清除null对象。
public static void main(String[] args) {
List<byte[]> list=new ArrayList<>();
// for (int i=0;i<5;i++){
// list.add(new byte[_4Mb]);
// }
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
soft(queue);
}
public static void soft (ReferenceQueue<byte[]> queue){
List<SoftReference<byte[]>> list=new ArrayList<>();
for (int i=0;i<5;i++){
//关联引用队列,当软引用对象被垃圾回收的时候,软引用自己会加入到queue队列中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4Mb],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
Reference<? extends byte[]> poll = queue.poll();
while(poll!=null){
list.remove(poll);
poll=queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
打印发现确实没有null对象了。
弱引用案例:
public class main {
static final int _4Mb=4*1024*1024;
public static void main(String[] args) {
List<WeakReference<byte[]>> list=new ArrayList<>();
for (int i=0;i<10;i++){
//关联引用队列,当软引用对象被垃圾回收的时候,软引用自己会加入到queue队列中去
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4Mb]);
list.add(ref);
for (WeakReference<byte[]> reft : list) {
System.out.print(reft.get()+" ");
}
System.out.println();
}
}
}
发现弱引用一般在垃圾回收时候就会清除弱引用,同时弱引用自己也会占用一定的内存空间。
优化:也可以用引用队列优化。案例同上软引用。
垃圾回收算法:
1.标记-清除
定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。
这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。
缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。
2.标记-整理
标记-整理:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。
复制
复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
总结:
分代垃圾回收
长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):
回收流程
新创建的对象都被放在了新生代的伊甸园中:
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC:
Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象先复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO。
伊甸园中不需要存活的对象清除:
交换FROM和TO:
同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:
流程相同,仍需要存活的对象寿命+1
:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里不好展示用文字描述了)
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!
如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:
如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:
小结:
- 新创建的对象首先会被分配在伊甸园区域。
- 新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。
- Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
- 当对象寿命超过阈值15时,会晋升至老年代。
- 如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。
相关JVM参数设置:
参考文章:JVM常用内存参数配置
GC 分析
大对象处理策略:
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代。
线程内存溢出:
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。
垃圾回收器:
相关概念
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%。
下面来了解一下垃圾回收器的分类:
串行
单线程
适用场景:内存较小,个人电脑(CPU核数较少)。
串行垃圾回收器开启语句:-XX:+UseSerialGC = Serial + SerialOld
Serial:表示新生代,采用复制算法;SerialOld:表示老年代,采用的是标记整理算法。
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。
Serial 收集器
Serial(新生代)收集器是最基本的、发展历史最悠久的收集器:
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本:
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题
Serial Old 收集器
Serial Old是Serial收集器的老年代版本:
特点:同样是单线程收集器,采用标记-整理算法。
吞吐量优先
- 多线程
- 适用场景:堆内存较大,多核CPU
- 单位时间内,让STW(stop the world,停掉其他所有工作线程)时间最短
- JDK1.8默认使用的垃圾回收器
// 1.吞吐量优先垃圾回收器开关:(默认开启) -XX:+UseParallelGC~-XX:+UseParallelOldGC // 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小 -XX:+UseAdaptiveSizePolicy // 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间 -XX:GCTimeRatio=ratio // 4.垃圾收集最大停顿毫秒数:默认值是200ms -XX:MaxGCPaiseMillis=ms // 5.控制ParallelGC运行时的线程数 -XX:ParallelGCThreads=n
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器:
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)。
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间。
XX:GCRatio 直接设置吞吐量的大小。
Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本:
特点:多线程,采用标记-整理算法(老年代没有幸存区)。
响应时间优先
多线程
适用场景:堆内存较大,多核CPU
尽可能让单次STW时间变短(尽量不影响其他线程运行)(与吞吐量优先区别的是响应时间优先是单次的STW最短所以总共的垃圾回收时间之和可能比吞吐量优先更长)
// 开关:
-XX:+UseConMarkSweepGC~-XX:+UseParNewGC~SerialOld
// ParallelGCThreads=n并发线程数
// ConcGCThreads=threads并行线程数(一般为并发线程数的1/4)
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
// 执行CMS垃圾回收的内存占比:预留一些空间保存浮动垃圾(指的是下图最后面程序一边清理一边运行时产生的浮动垃圾)
-XX:CMSInitiatingOccupancyFraction=percent(一般为65%)
// 重新标记之前,对新生代进行垃圾回收(这样可以减少无用的查找标记工作)
-XX:+CMSScavengeBeforeRemark
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:
特点:基于标记-清除算法实现。**并发**收集、低停顿,但是会产生内存碎片。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
CMS收集器的内存回收过程是与用户线程一起并发执行的。
注意:这里采取的时标记加清除算法,所以可能产生较多的垃圾碎片,当新生代碎片过多不足老年代碎片过多也不足时候,可能造成依次并发失败,这个时候垃圾回收器就会退化为SerialOld 做一次单线程串行的回收,当发生并发失败时候,垃圾回收时间将会变得很长,这也是一个缺点。
虚拟机参数:
G1
定义:
Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器:
适用场景:
- 同时注重吞吐量和低延迟(响应时间)。
- 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域。
- 整体上是标记-整理算法,两个区域之间是复制算法。
相关参数:JDK8 并不是默认开启的,需要参数开启:
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
垃圾回收阶段:
垃圾回收过程:
- 先新生代搜集
- 然后当老年代空间不足时候,会进入下一个阶段新生代搜集加并发的标记
- 混合搜集会对新生代和老年代都进行一次大规模的收集
- 循环上三个过程
Young Collection:
将幸存对象复制到幸存区 (E->S)
当幸存区的对象存活时间超过一定次数后会晋升到老年代(S->O),同时不够年龄的会被拷贝到另一个幸存区域(S->S),同时新生代幸存的一些对象也会复制到这个区域。
Young Collection + CM(CM==并发标记)
- 在 Young GC 时会对 GC Root 进行初始标记。
- 在老年代占用堆内存的比例达到阈值时,进行并发标记(不会STW),阈值可以根据用户来进行设定:-XX:InitiatingHeapOccupancyPercent=percent(默认值45%)
Mixed Collection
会对E、S 、O 进行全面的回收。
- 最终标记
- 拷贝存活
// 用于指定GC最长的停顿时间
-XX:MaxGCPauseMillis=ms
为什么这里不是所有的老年区被拷贝了?
答:因为C1会根据-XX:MaxGCPauseMillis=ms(用于指定GC最长的停顿时间)来挑选出最具有价值的区域(回收释放空间最多的区域)进行拷贝。
G1在老年代内存不足时(老年代所占内存超过阈值):
- 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理。
- 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC(现在版本是)。
Young Collection 跨代引用
卡表与Remembered Set
Remembered Set 存在于E中,用于保存新生代对象对应的脏卡:
脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡。
在引用变更时通过post-write barried + dirty card queue。(这里会将脏卡标记操作加入队列然后异步执行)
concurrent refinement threads 更新 Remembered Set。
Remark
-
Remark重新标记阶段
-
在垃圾回收时,收集器处理对象的过程中:
- 黑色:已被处理,需要保留的
- 灰色:正在处理中的
- 白色:还未处理的
观察下图并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark。
过程如下:
之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态。
在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它。
JDK 8u20 字符串去重
优点与缺点:
- 优点:节省了大量内存。
- 缺点:新生代回收时间增加,导致略微多占用CPU。
案例分析:
String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
- 将所有新分配的字符串(底层是char[])放入一个队列。
- 当新生代回收时,G1并发检查是否有重复的字符串。
- 如果字符串的值一样,就让他们引用同一个字符串对象。
- 注意,其与String.intern()的区别:
- intern关注的是字符串对象。
- 字符串去重关注的是char[]数组。
- 在JVM内部,使用了不同的字符串标。
字符串去重开启指令 -XX:+UseStringDeduplication
:
底层优化后:
JDK 8u40 并发标记类卸载
在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用时,则卸载它所加载的所有类。
并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark
默认启用。
JDK 8u60 回收巨型对象
H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉
JDK 9 并发标记起始时间的动态调整
- 并发标记必须在堆空间占满前完成,否则就退化为 Full GC。
- JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 设置阈值,默认是 45%。
- JDK 9 可以动态调整:(目的是尽可能的避免并发标记退化成 Full GC)
- -XX:InitiatingHeapOccupancyPercent:用来设置初始阈值。
- 在进行垃圾回收时,会进行数据采样并动态调整阈值。
- 总会添加一个安全的空挡空间,用来容纳那些浮动的垃圾
4.4.12 JDK 9 更高效的回收
- JDK 9 对垃圾回收进行了 250+ 项的增强,180+ 项的bug修复。
- 参考文章:Oracle 官方的虚拟机调优指南