如何确定哪些东西是垃圾?
·引用计数法:Java中引用和对象是有关联的,如果我们想要操作一个对象,一定要只有这个对象
的引用,那么这个时候我们就可以根据对象被引用的次数来判断这个对象是否是可回收的对象。
·缺点:引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,
对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回
收,所以主流的虚拟机都没有采用这种算法。
·可达性分析法(引用链法):为了解决引用计数法的循环引用问题,Java 使用了可达性分析的
方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路
径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可
回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
·如果对象在可达性分析中没有不GC Root 的引用链,那么此时就会被第一次标记并且进行第
一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已
被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行 finalize()方法,那么这个对
象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程
是高优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者
发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于
F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。所
以我们尽量不要使用finalize,不然可能会导致OOM,而且finalize中的代码由另外一个守护线
程来执行,可能主程序执行结束了这个守护线程还没有执行结束,那也不会执行的。那么就会
出现问题了。所以这个finalize再Java9开始就被标记为已过时的了,我们尽量别用。
·哪些对象可以被当作GC roots?
·虚拟机栈中引用的对象
·方法区中类的静态属性引用的常量
·方法区常量池引用的对象
·本地方法栈JNI中引用的常量
确定了垃圾之后,如何进行垃圾清理?
·标记清除法:最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收
的对象,清除阶段回收被标记的对象所占用的空间。
·缺点:内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
·复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用
其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一
块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪
费一半的内存。
·虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一
半。且存活对象增多的话,Copying 算法的效率会大大降低。
·标记整理算法:是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决
了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移劢到一端,然后清
除掉端边界以外的对象,这样就不会产生内存碎片了。
·分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存
划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾
回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。在新生代中,由亍对象
生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较
高,没有额外的空间迚行分配担保,所以可以使用标记-整理 戒者 标记-清除。
java 内存分配与回收策率以及 Minor GC 和 Major GC
垃圾收集回收器的种类
1:Serial:最基本的垃圾收集器,采用复制算法,是jdk1.3之前唯一垃圾收集器,serial是一个单线程的垃圾收集器,并且在进行垃圾收集的同时必须暂停其他线程,知道垃圾回收结束。回收效率高,没有线程交互的开销,是Java虚拟机在Client模式下默认的新生代垃圾收集器
。
2:ParNew:实际上是Serial垃圾收集器的多线程版本,也是采用了复制算法,除了利用多线程进行垃圾回收之外,其他和Serial垃圾收集器一样。ParNew在垃圾收集时候也要暂停其他所有工作的线程。默认开启和CPU数目相同的线程数目,可以通过-XX:ParallelGCThreads参数来限定垃圾收集器的线程数,是Java虚拟机在Server模式下新生代默认的垃圾收集器
。
3:Parallel Scavenge:多线程复制算法,也是新生代的垃圾收集器,重点关注程序可以达到的一个可控制的吞吐量。包含自适应调节策略。
4:Serial Old:单线程标记整理算法,运行在Client模式下Java虚拟机默认的老年代垃圾收集器
。
5:Parallel Old:多线程标记整理算法
6:CMS:多线程标记清除算法,一种老年代垃圾收集器
7:G1:标记整理算法,不产生内存碎片。
CMS垃圾收集器
这是一个老年代的垃圾收集器,采用的是多线程标记清除算法,标记过程分为四个阶段,
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
优点:处理速度快
缺点:会产生内存碎片
CMS在并发标记的过程中,会产生对象丢失的问题,产生对象丢失的条件有两个
(参考文章,这个写的很清楚。其他文章)
解决的方式有两种,分别对应的是破坏上面两个条件
G1垃圾收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收
集器两个最突出的改进是:
ZGC(参考文章)
ZGC是JDK111中推出的一款低停顿的垃圾收集器,它的设计目标如下:
染色指针可以说就是指的是对象内存地址的44-47位的颜色标志,这样就可以快速地判断这个对象是否是标记或者转移阶段,或者是否是只有finalize方法才能调用。
垃圾收集的安全点
GC并不是在任何时候都会发生的,因为我们知道垃圾回收的时候判断是否可达,不可达的对象才会被定义成垃圾,这样在标记的时候就要stop the world
,防止在标记过程中产生的新的垃圾对象。HotSpot虚拟机维护了一个映射列表oopmap
用来记录哪些位置存放了对象的引用,以便于快速判断是否是垃圾,但是由于对于每一个操作都维护一个oopmap
未免有点过于占用空间,得不偿失。因此只有在特点的地方生成oopmap
,这样的位置就是安全点。
不过安全点的设置也不是越多越好,多了反而导致垃圾收集效率很低,太少导致一些线程等待其他线程到达安全点的时间很久,导致性能变差。一般安全点会设置在下面几个地方:
强烈推荐,推荐文章
这些位置有助于使线程不会长时间运行导致无法到达safePoint,避免其他线程都停顿等待本线程。
线程中断的方式
经典的停顿导致主线程长时间等待代码:
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
//i定义成int类型会被当成可数循环导致jvm不会在这里设置安全点safepoint
for (int i = 0; i < 1000000000; i++) {
num.getAndAdd(1);
}
System.out.println(Thread.currentThread().getName()+"执行结束!");
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
//由于sleep方法是底层用到了native,回来的时候就会进入安全点等待其他线程也进入安全点进行
//gc,但是由于int的循环没有添加安全点导致了主线程只能等待其他线程的循环体执行完才能进入
//安全点,所以下面的打印会很慢,如果改成long类型的i就可以解决问题,或者添加一个
//sleep(0)的代码也可以有native方法执行完之后进入安全点达到解决长时间停顿的问题。
Thread.sleep(1000);
System.out.println("num = " + num);
}
安全区域
思考一下前面的安全点会不会有一些问题?
比如线程阻塞获取不到时间片,或者sleep了,无法到达安全点怎么办?总不能一直不进行垃圾回收吧。
安全区域的出现就是为了解决上面的问题,所谓的安全区域就是在某一部分代码执行的时候对象的引用状态不会发生变化,这样GCROOT就不会变化,那么在这一块代码执行的时候是可以直接进行垃圾回收的而不必非要等到线程到达安全点。
那么这里又会出现一个问题,假如我在垃圾回收的时候,线程从安全点跑出去了导致对象的引用状态发生了变化那不就完蛋了???所以这里在线程执行完安全区的代码的时候也不会直接跑出安全区,而是先看这个垃圾回收是否进行完,回收结束了才会让你跑出去。不然就等着吧你。
java8默认是垃圾回收算法是什么?
使用命令查看
java -XX:+PrintCommandLineFlags -version
JDK8中默认的选择是”-XX:+UseParallelGC",是 Parallel Scavenge + Parallel Old组合。