如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。
某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。
引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!
可达性分析算法:就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。
在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:
JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。
可以作为GC Root的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象。
可以理解成一个葡萄,从根出发能到达各个葡萄的位置。
案例演示:
这里在list1有无值两方面抓取快照。
然后通过eclipse提供的MAT工具来 GCRoot
,哪些可以作为GCRoot
这些都可以作为gcroot
GC Root
都不在引用它,才会释放。
当引用的对于ByteBuffer被回收以后,直接内存还没有被回收,虚引用对象(Cleaner)就会被放入到引用队列中,
然后就会由线程ReferencHandler
定时去引用队列寻找是否有Cleaner
对象,如果有,就会调用Cleaner对象的clean方法,而clean方法就会根据前面记录的直接内存地址,调用Unsafe.freeMemory()方法,来释放直接内存。
总之虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存。
根据上面的图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用 队列中,然后调用它的clean方法来释放直接内存。
可以回顾一下之前学的直接内存,和Cleaner底层原理。
所有的类都继承自Object类,Object类有一个finalize()方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。
当一个对象重写了finalize方法,肯定是希望通过终结器应用释放。
例如上图,当A4对象没有强引用的时候,这时候会进行垃圾回收,虚拟机会对这个对象创建终结器引用,终结器应用会进入到引用队列,但这时候A4对象还没有被释放!!
只有当优先级很低的FinallizeHandler线程来应用队列寻找是否有终结器应用,然后调用A4对象的finallize()方法,在下一次垃圾回收的时候A4对象才会得到释放。
所以也就有一个弊端,因为FinallizeHandler线程优先级很低,而且第一次内存回收的时候并不能完全释放。所以有时候对象迟迟得不到释放,内存就会一直被占用,所以这个终结器应用在日常用的并不多。
让我们细看一下细节。打印gc详情
所以在处理一些不重要的对象的时候,就可以通过软引用来降低内存压力。
弱引用一般会发生在垃圾回收的时候,当内存紧张时,就会回收弱引用对象,同时也会回收弱引用自身。
首先有两步
缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。
会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。
复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),在内存不足的时候也可能回收,用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):
新生代、老年代;新生代又划分为伊甸园、幸存区from、幸存区to。
长时间使用的对象放在老年代中,用完了就丢弃的对象放在新生代中。根据对象生命周期的不同特点,采用不同的垃圾回收算法,老年代发生次数少,新生代比较频繁。不同区域采用不同算法,更有效的对垃圾回收进行管理
java虚拟机本身就是一个小的操作系统吧?
**含义 ** 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC
通过如下代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,什么时候 幸存区放不下,直接晋升老年代,使用前需要设置 jvm 参数。
public class Code_10_GCTest {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
}
}
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行
这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。
在谈论垃圾收集器的上下文语境中, 它们可以理解为:
并行(Parallel) :
并行描述的是多条垃圾收集器线程之间的关系, 说明同一时间有多条这样的线程在协同工作, 通常默认此时用户线程是处于等待状态。
并发(Concurrent) :
并发描述的是垃圾收集器线程与用户线程之间的关系, 说明同一时间垃圾收集器线程与用户线程都在运行(不一定是并行的可能会交替执行)。 由于用户线程并未被冻结, 所以程序仍然能响应服务请求, 但由于垃圾收集器线程占用了一部分系统资源, 此时应用程序的处理的吞吐量将受到一定影响。
即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
单线程
适用于堆内存较小,如个人电脑(CPU核数较少也可以,因为单线程)
-XX:+UseSerialGC=serial + serialOld
让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
在服务端模式下,它也可能有两种用途: 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用, 另外一种就是作为CMS收集器发生失败时的后备预案, 在并发收集发生Concurrent Mode Failure时使用
parallel:并行的 Pause:停顿
//第一个是新生代的垃圾回收器,复制算法;第二个是老年代的垃圾回收器,标记整理算法
//都是多线程的,只要开启一个,另外一个就会开启
//工作开启的回收线程数目,与cpu核数有关。回收时,cpu占有率100%
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy //GC自适应调节策略,伊甸园和幸存区占比
-XX:GCTimeRatio=ratio // 垃圾回收和总时间占比 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 垃圾回收暂停200ms
-XX:ParallelGCThreads=n //回收线程数 控制
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同, CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput) 。 所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 。
与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms):
回收器一般会让堆变小,减少每次停顿的时间
XX:GCTimeRatio=radio 垃圾收集时间占总时间的比率,相当于吞吐量的倒数
回收器一般会让堆变大,去减少垃圾回收次数,从而减少时间
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法
//第一个CMS是并发标记清除算法,用户线程与垃圾回收线程并发进行,老年代浮动垃圾过多,退化为 SerialOld
//第二个是parnew新生代多线程回收器
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
//第一个是并行的垃圾回收线程数,一般跟cpu核数一样;第二个是并发的垃圾回收线程数,一般设置为并行线程数的四分之一
//比如:核数为4,那就占用一个cpu核进行垃圾回收。其他用于用户线程
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
//同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
//设置得太高将会很容易导致大量的并发失败产生,性能反而降低;
//设置太低将导致内存回收频率增加,性能降低
-XX:CMSInitiatingOccupancyFraction=percent
//重新标记前对新生代 先做一次垃圾回收(UseParNewGC),新生代存活对象少了,减轻重新标记的压力
-XX:+CMSScavengeBeforeRemark
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行 。
为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题,这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短。
并发清除阶段, 清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的
在CMS的并发标记和并发清理阶段, 用户线程是还在继续运行的, 程序在运行自然就还会伴随有新的垃圾对象不断产生, 但这一部分垃圾对象是出现在标记过程结束以后, CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。 这一部分垃圾就称为“浮动垃圾”。
如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,很耗费时间,本来是响应优先的垃圾回收器,响应时间变得更长了,这也是CMS存在的问题。
CMS是一款基于“标记-清除”算法实现的收集器, 这意味着收集结束时会有大量空间碎片产生。 空间碎片过多时, 将会给大对象分配带来很大麻烦, 往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。 退化为SerialOld,进行标记整理,很耗费时间,本来是响应优先的垃圾回收器,响应时间变得更长了,这也是CMS存在的问题。
由于在整个过程中耗时最长的并发标记和并发清除阶段中, 垃圾收集器线程都可以与用户线程一起工作, 所以从总体上来说, CMS收集器的内存回收过程是与用户线程一起并发执行的。
// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)。
学习视频:【黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓】 https://www.bilibili.com/video/BV1yE411Z7AP/?share_source=copy_web&vd_source=fcae3ca58a4c2446a58b5aaacbaa4bbe
这个老师的JUC也很赞!!