引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!
可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。
在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:
可以作为GC Root的对象:
上图实心线表示强引用:比如,new
一个对象M,将对象M通过=
(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。
强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当GC Root都不引用该对象时,才会回收强引用对象。
上图中宽虚线所表示的就是软引用:
软引用的特点:当GC Root指向软引用对象时,若内存不足,则会回收软引用所引用的对象。
软引用的使用:
public class Demo1 {
public static void main(String[] args) {
final int _4M = 4*1024*1024;
// 软引用对象内部包装new byte[_4M]对象
SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
// 这时List 跟 SoftReference之间是强引用,SoftReference 跟 byte[] 之间是软引用
List 跟 <SoftReference<byte[]>> list = new ArrayList<>();
}
}
如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理。
如果想要清理软引用,需要使用引用队列:
public class Demo04 {
final static int _4M = 4 * 1024 * 1024;
public static void main(String[] args) {
// List和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列,用于移除引用为空的软引用对象
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联引用队列,当软引用所关联的 byte[]被回收时,软引用自己会假如到queue中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4M], 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();
}
System.out.println("==========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
**大概思路为:**查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)。
只有当弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference。
当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中:
然后调用Cleaner的clean
方法(Unsafe.freeMemory()
)来释放直接内存:
所有的类都继承自Object类,Object类有一个finalize()
方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()
方法。调用以后,该对象就可以被垃圾回收了。
finalize()
方法。调用以后,该对象就可以被垃圾回收了。强引用:无论内存是否足够,不会回收。
软引用:内存不足时,回收该引用关联的对象。(可以选择配合引用队列使用,也可以不配合)。
弱引用:垃圾回收时,无论内存是否足够,都会回收。(可以选择配合引用队列使用,也可以不配合)。
虚引用:任何时候都可能被垃圾回收器回收。(必须配合引用队列使用)。
定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。
缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。
标记-整理:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。
当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中:
然后清除FROM中的需要回收的对象:
最后 交换 FROM 和 TO 的位置:(FROM换成TO,TO换成FROM)
复制算法:将内存分为等大小的两个区域,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,扫描新生代和老年代中所有不再使用的对象并回收:
小结:
大对象处理策略:
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代。
线程内存溢出:
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。
相关概念
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%。
下面来了解一下垃圾回收器的分类:
串行垃圾回收器开启语句:-XX:+UseSerialGC = Serial + SerialOld
Serial
:表示新生代,采用复制算法;SerialOld
:表示老年代,采用的是标记整理算法。
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。
Serial(新生代)收集器是最基本的、发展历史最悠久的收集器:
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
ParNew收集器其实就是Serial收集器的多线程版本:
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题
Serial Old是Serial收集器的老年代版本:
特点:同样是单线程收集器,采用标记-整理算法。
// 1.吞吐量优先垃圾回收器开关:(默认开启)
-XX:+UseParallelGC~-XX:+UseParallelOldGC
// 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小
-XX:+UseAdaptiveSizePolicy
// 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间
-XX:GCTimeRatio=ratio
// 4.垃圾收集最大停顿毫秒数:默认值是200ms
-XX:MaxGCPaiseMillis=ms
// 5.控制ParallelGC运行时的线程数
-XX:ParallelGCThreads=n
与吞吐量关系密切,故也称为吞吐量优先收集器:
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)。
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
是Parallel Scavenge收集器的老年代版本:
特点:多线程,采用标记-整理算法(老年代没有幸存区)。
虚拟机参数:
// 开关:
-XX:+UseConMarkSweepGC~-XX:+UseParNewGC~SerialOld
// ParallelGCThreads=n并发线程数
// ConcGCThreads=threads并行线程数
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
// 执行CMS垃圾回收的内存占比:预留一些空间保存浮动垃圾
-XX:CMSInitiatingOccupancyFraction=percent
// 重新标记之前,对新生代进行垃圾回收
-XX:+CMSScavengeBeforeRemark
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
CMS收集器的内存回收过程是与用户线程一起并发执行的。
Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器:
适用场景:
相关参数:JDK8 并不是默认开启的,需要参数开启:
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)。
分区算法region
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间。
E:伊甸园 S:幸存区 O:老年代
垃圾回收时,会把伊甸园(E)的幸存对象复制到幸存区(S):
当幸存区(s)中的对象也比较多触发垃圾回收,且幸存对象寿命超过阈值时,幸存区(S)中的一部分对象(寿命达到阈值)会晋升到老年代(O),寿命未达到阈值的会被再次复制到另一个幸存区(S):
CM:并发标记!
-XX:InitiatingHeapOccupancyPercent=percent // 默认值45%
会对E、S 、O 进行全面的回收。
// 用于指定GC最长的停顿时间
-XX:MaxGCPauseMillis=ms
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的最大停顿时间,会根据最大停顿时间,有选择的回收最有价值的老年代(回收后,能够得到更多内存)。
G1在老年代内存不足时(老年代所占内存超过阈值):
卡表与Remembered Set
在引用变更时通过post-write barried + dirty card queue。
concurrent refinement threads 更新 Remembered Set。
但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark。
过程如下:
优点与缺点:
字符串去重开启指令 -XX:+UseStringDeduplication
:
案例分析:
String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用时,则卸载它所加载的所有类。
并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark 默认启用。
H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~
并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark 默认启用。
H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
[外链图片转存中…(img-2775SAwR-1639636370837)]
巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~