1.如何判断对象可以被回收:
1.引用计数法:
核心:为每个对象分配一个引用计数器,记录当前对象被引用的次数,如果为0,则该对象就是垃圾。
缺点:当两个对象互相引用时,就会导致循环引用,无法被回收,就会一直占内存。性能上面由于每次增减都会操作引用计数器,就会有性能开销。
2.可达性分析算法:
扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到表示可以回收。
2.1哪些对象可以作为GC Root对象:
1.虚拟机栈中的对象(包括本地虚拟机栈):方法内部的局部变量,参数,以及C/C++写的本地方法所使用的java对象。
2.静态变量引用的对象:比如:public static User user=new User();
3.常量引用的对象:比如:public static final User user=new User();
4.加锁的对象:如:synchronized(obj){.......} 中的obj对象。
5.JVM内部的核心对象:比如类加载器,运行时常量池,JVM自带的异常类对象(OOM,NULLPointerExeception等)
四种引用:
1.强引用,2.软引用,3.弱引用,4.虚引用,5.终结器引用,但是最后的终结器引用是最少见的,只谈论前面四个。
1.强引用:
最普通的引用(Object obj=new Object()),是程序默认的引用方式,只要强引用存在,垃圾回收器就绝对不会回收强引用对象,即使内存溢出,也不会被回收。
2.软引用:
通过SoftReference<Object> obj=new SoftReference<Object>()来引用,当内存充裕时,不会被回收,当内存空间不足时,垃圾回收器就会回收软引用的对象。
比如一些图片资源,缓存数据。
3.弱引用:
通过WeakReference<object> obj=new WeakReference<>()来引用,无论内存充裕与否,垃圾回收器都会回收弱引用对象。
比如ThreadLocal中的键引用。
4.虚引用:
通过PhantomReference类来使用,必须结合引用队列(ReferenceQueue)使用(ReferenceQueue<Object> queue=new ReferenceQueue<>(),
PhantomReference<Object> obj=new PhantomReference<>(new Object,queue)),这时GC无法通过虚引用找到对象,必须配合引用队列,当JVM发现了虚引用,将其入队(预处理信号,"这个对象即将被回收,赶紧执行清理逻辑")。当虚引用对象被回收后,此时就会有线程遍历队列,遍历到虚引用对象,就会执行最后的清理逻辑。避免堆外内存溢出。
注意:并不是只有虚引用有引用队列,软引用和弱引用也可以使用引用队列,但不是必须。
垃圾回收算法:
1.标记清除算法:

垃圾回收后,灰色区域就是可用区域,下面我们说一下这个算法的优缺点:
优点:无需移动对象,节省移动开销,而且不破坏指针地址引用
缺点:内存碎片化太多,导致大内存数据会无法分配内存,当堆内存太大,因为标记和清理都会遍历,会增大开销。
2.标记整理算法:

下面介绍一下标记整理方法的优缺点:
优点:彻底解决了内存碎片化问题,内存利用率高,无额外内存空间浪费。
缺点:移动对象和更改引用对象的开销较大。STW(世界停止)会更长,因为三个阶段会全程暂停用户线程
3.复制算法:

算法的优缺点:
优点:无内存碎片化,内存分配高效。低存活率下很快(存活数据少量,复制就会很快)。
缺点:占用双倍内存,存在固定空间浪费,高存活率下很慢(因为有大量的存活数据需要复制)
4.分代回收算法:

分代回收算法是JVM默认采用的垃圾回收算法,**基于对象存活的周期差异,将堆内存划分为不同的区域,为每个区域匹配最适合的垃圾回收算法。**本质是对"标记-清除法算法","标记-整理法算法","复制算法"的灵活运用,最终实现"高效回收,低停顿,高内存利用率"。
垃圾回收算法流程:
第一次Minor GC:初始状态 ,JVM启动后,新的对象分配在伊甸区,FROM和TO区都为空,首次执行Minor GC时。因为FROM为空,只会扫描伊甸区的存活对象,并将它们复制到TO区,同时这些对象年龄+1,最后角色互换(FROM和TO区互换角色,所有存活的对象都在FROM区)
经过第一次的GC后,FROM区会有存活对象,而伊甸区没有对象。
第二次执行Minor GC:如果伊甸区又满了,这时伊甸区和FROM区发生Minor GC,会将FROM区的幸存者和伊甸区的幸存者都会复制到TO区,年龄+1,然后FROM区和TO区角色互换。如果年龄达到阈值(默认15,可以通过-XX:MaxTenuringThreshold来设置),就会晋升到老年代。
特殊情况:
当伊甸区发生GC后,存活的对象太大,幸存区存不下,这时就会直接放入老年代。
超大对象:超过伊甸区的一半大小的对象,直接分配到老年代(避免频繁复制)。
老年代的垃圾回收机制:Major GC和Full GC
老年代GC触发的条件:
1.老年代内存不足:
老年代对象持续累积(长时间存活的对象,还有年轻代晋升的对象),当老年代内存不足时,就会触发Full GC。
2.年轻代分配内存失败:
当年Minor GC回收后,存活的对象在幸存区中存不下,就会给到老年代,如果老年代的空间不足的时候,也会触发Full GC。
3.元数据区满了
元数据区储存的类元数据,常量,当频繁加载类,就会导致元数据区内存溢出,会触发GC。
4.显式调用System.gc:
不推荐使用此方法,Java会触发Full GC
5.老年代内存碎片过多:
当老年代碎片过多,无法存下内存大的对象,就会触发GC
垃圾回收器:
1.串行:

开启-XX:UseSerialGC=Serial+SerialOld,就能开启串行垃圾回收器。
Serial:是新生代的垃圾回收,采取的是复制算法。
SerialOld:是老年代垃圾回收,采取标记-整理算法。
串行垃圾回收器是单线程进行垃圾回收,回收的时候,用户的工作线程会被阻塞。所以缺点是性能很低。
2.吞吐量优先:

吞吐量优先:当堆内存较大,多核CPU情况下,要求**单位时间内,STW的时间最短。**通俗一点就是"多做事,少休息"。
吞吐量优先的参数:
-XX:UseParallelGC -XX:UseParalleloled
前一个是新生代(采用复制算法),后一个是老年代(采用标记-整理算法),开启一个默认开了两个。
-XX:UseAdaptiveSizePolicy:这个参数,会自适应调整伊甸区和幸存区的大小,堆的大小等,前提是以"吞吐量优先"。
-XX:GCTimeRatio=ratio:这个参数用来设置吞吐量占比,吞吐量占比=100/(1+ratio),如果设置ratio为99,那么吞吐量占比(用户代码运行时间至少)99%,垃圾回收占比小于1%
-XX:MaxGCpauseMillis=ms:这个参数用来设置最大GC停顿时间。
吞吐量占比参数和吞吐量参数停顿时间:
不能保证吞吐量占比,就会自动调节堆的大小,扩大堆内存,可以让吞吐量占比更多,GC时间占比也会放大。但是堆内存扩大,那么GC就要回收更多的垃圾,可能就不能保证在最大垃圾停顿时间内。所以二者是矛盾的。
3.响应时间优先:

响应时间优先:核心是最小化单词的STW时间,通俗一点"优先保证快速响应,即使GC频繁"。通过频繁小额回收,因为CPU和内存资源宽带等资源,会牺牲一些吞吐量。
初始标记:"快速快照"只,暂停所有用户线程,把与GC Roots直接关联的对象,耗时极短,确保对应用响应影响最小。
并发标记:"后台标记",不暂停用户的线程。遍历初始标记对象,标记所有可达对象,避免长时间STW。
重新标记:最后一次短暂的STW,修正并发标记期间线程修改引用导致的偏差(如新增可达对象,新增失效对象),确保标记结果100%正确。
并发清理:GC与应用程序线程并行执行,回收已被标记的垃圾对象,保障应用持续响应。
响应时间优先参数:
1.-XX:UsecomMarkSweepGC=n -XX:UseparNewGC
前一个参数:是老年代垃圾回收(并发标记清除),注意:如果后期老年代因为内存碎片化太多,不能存储新对象,这时并发标记清除就会失效,然后变成Serialoled(单线程的标记整理,会阻塞用户线程)
后一个参数:是新生代的垃圾回收(复制算法)
2.-XX:ParalleLGCThreads=n -XX:ConGCThreads=threads
前一个参数是设置并行线程数的,后一个参数是设置垃圾回收并发线程数(一般是并行线程数*1/4)
3.-XX:CMSInitiatingOccupancyFraction=percent
清理浮动垃圾,控制内存占比,留一些区域来存放浮动垃圾**(因为在并发清理垃圾时,也会参数新的垃圾,那么就需要空一块区域来储存这些垃圾。即浮动垃圾)**
4.-XX:+CMSScavengeBeforeRemark
在重新标记的时候,先对新生代进行一次垃圾回收,可以减少重新标记的工作量,因为有些即将被回收的垃圾会被扫描一次(没必要),这样的话可以缩短STW的时间。