JVM规范说了并不需要必须回收方法区,不具有普遍性,永久代使用的是JVM之外的内存
引用计数:效率要比可达性分析要强,随时发现,随时回收,实现简单,但是可能存在内存泄漏
局部变量表,静态引用变量,通过引用链关联的引用链是不会被回收,局部变量表天然作为GCROOTS
1,虚拟机栈中引用的对象(栈帧中的本地方法表)。 2,方法区中(1.8称为元空间)的类静态属性引用的对象,一般指被static修饰的对象,加载类的时候就加载到内存中。 3,方法区中的常量引用的对象。 4,本地方法栈中的JNI(native方法)引用的对象。 注意:即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下 要真正宣告对象死亡需经过两个过程。 1.可达性分析后没有发现引用链 2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。
就是只是进行新生代回收的时候老年代的引用也可以作为GCROOTS
public class Test { public static Test obj;//这是一个类变量 // @Override // protected void finalize() throws Throwable { // System.out.println("调用当前链上的finalize方法"); // obj=this;//当前带回收的对象在finalize方法上和一个引用链上面的对象建立了联系 // } public static void main(String[] args) throws InterruptedException { obj=new Test(); //对象第一次拯救自己 obj=null; System.gc();//调用垃圾回收器 System.out.println("第一次GC"); //因为finalizer线程优先级很低,暂停2s来等待他 Thread.sleep(3000); if(obj==null){ System.out.println("对象已经死了"); }else{ System.out.println("对象还活着"); } obj=null; System.gc();//调用垃圾回收器 System.out.println("第二次GC"); //因为finalizer线程优先级很低,暂停2s来等待他 Thread.sleep(3000); if(obj==null){ System.out.println("对象已经死了"); }else{ System.out.println("对象还活着"); } } }
垃圾回收算法:
任何时候都可能,当系统觉得你内存不足了就会开始回收常见的比如分配对象内存不足时这里的内存不足有可能 不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象,当前堆内存占用超过阈值时,手动 调用 System.gc() 建议开始GC时,系统整体内存不足时等
标记是非垃圾的对象就是可达的对象,然后清除清楚的是垃圾对象,要先递归进行遍历所有可达对象,然后清除的时候需要再开始遍历一遍,还需要进行维护空闲列表
就比如说我们的硬盘,只要你不小心点击了格式化,此时也不是真正的进行格式化,只是标记性删除,但是千万不要再向里面存放数据,因为数据会覆盖,就不好恢复了
复制算法:内存利用率贼低
首先经过可达性分析在A区找到可达的对象,一旦找到了可达的对象就不需要进行标记,直接将可达的对象进行复制算法放到另一块区域B,这是另一块空间的所有区域B的对象都是连续的
缺点:维护引用和对象的地址映射
回收的对象比较少,存活的对象比较多,那么移动的对象比较多,但是还要大量维护指针和对象的关系,老年代不适合使用复制算法,因为很多对象都不死,老年代复制对象开销太大
标记整理算法:
还要移动位置,还要修改引用对象关系很麻烦,这个算法比标记清除算法效率还低
分代回收:
新生代:老年代=1:2,edin区:幸存者1区:幸存者2区:
标记的开销和存活的对象成正比,因为标记只能标记存活的对象
清除阶段要进行全堆空间线性的遍历
压缩或者是整理和存活对象的大小成正比
Stop The World:
先确定GCROOTS,枚举根节点,此时要进行Stop The World,确保数据的一致性
stop the world停止的是用户线程,就是为保证一致性
可达性分析算法中枚举根节点会导致所有Java执行线程停顿
衡量一个垃圾回收器的标准就是吞吐量和低延迟
增量收集算法:
比如说我现在有一个房子,我一直不进行清理,一直制造垃圾,直到三个月之后才清理一次,此时清理的时间就比较长,阻隔用户线程的时间就比较长,但是如果说隔一会清理一会效果就会比较好,用户线程和回收线程协调交替执行,看样子就是并发的执行从而到达一种低延迟的行为,就是为了让用户感觉好一点;
被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样
CMS自称低延迟,开发中不要显示的进行GC,导致STW
分区算法:降低停顿时间,主要是保证低延迟而不是吞吐量
有的分区存放大对象,有的区域存放小对象,回收区域的个数取决于时间的长短,可以控制可控时间
System.gc():提醒JVM垃圾回收器去执行GC,但是不确实马上执行GC,底层是调用RunTime().getTime().gc(),进而也不能确定finlizle方法一定会被调用
Full GC 就是收集整个堆,包括新生代,老年代和方法区,但是调用System.gc系统做了免责声明,GC具体干不干,不怪这个方法,仅仅是提醒JAVA虚拟机进行垃圾回收,但是实际上是否进行垃圾回收System.GC()不保证
做性能测试之前先进行GC,局部变量表第一个位置存放的是this
不会触发垃圾回收
buffer作用域已经过了,所以buffer肯定用不上了,系统会判定buffer占用的slot为可覆盖的slot,一但value覆盖buffer所在的槽,buffer引用被覆盖,此时没有任何引用指向字节对象数组,所以此时触发System.gc就会回收垃圾
内存溢出和内存泄漏:
空间不够GC之后空间还不够才会报OOM
如果动态加载很多类,intern方法调用太多,都会造成OOM,本地内存报OOM的情况相对来说比较少一些
1)宽泛意义上的内存泄漏:其实可以把对象定在方法内部作为局部变量,当方法执行完成以后,对象就要被回收了,但是如果将这个变量成员变量,那么这个对象的存活周期就变得很长,但是如果是这个变量静态变量还是大对象,类变量,随着类的加载而加载随着类的消亡而消亡,也会理解成宽泛的内存泄露,还比如说Session会话,不使用就没有必要存放,内存泄漏可能会导致内存溢出,如果出现很多生命周期很长的对象再加上很多没有办法回收的数据的存在就很有可能造成内存泄露;
2)存在很多生命周期很长的对象,而本身生命周期没有这么长的对象而又生命很长可以称之为是内存泄漏;
严格意义+宽泛意义上的内存泄漏,有些对象不使用,但是还存着引用链,有可能忘记断开引用,如右图,对象没有用处,但是还存在引用链,可能造成内存回收,ThreadLocal
1)单例模式中的对象是static,单例对象的生命周期和应用程序是一样长的,一个进程只有这一个实例,就比如说RunTime实例,声明的是静态的,每一个进程只有一个实例会随着程序的执行而产生,随着进程的结束而销毁,如果此时单例对象关联了一个外部的很大的对象,这个外部对象用一会就不用了,单例对象的生命周期非常长,但是这个引用关系又断不掉,所以此时连带着外部对象的生命周期也很长,本身又不用,但是无法释放内存,此时这个对象还无法回收,引用链条得不到释放;
2)当内部资源外部资源需要交互的时候,是需要进行手动的关闭资源链接,没有关闭资源,GC就无法回收这些对象,只有当程序结束的时候才能回收这些链接的对象,此时可能就会发生内存泄漏;
程序中的并行和并发(一个CPU快速切换CPU,不是真正意义上的同时执行):
并行就是在具体某一个时刻的时候有三个线程同时的进行执行,主要是取决于多核CPU,而并发在某一个时间点上面只能有一个进程在执行,在时间段内是可能有多个线程
垃圾回收器:
下面的绿线表示用户线程,红色表示垃圾回收器,串行垃圾回收器很慢
安全点:
点比较少,这个时候GC等待时间太长,用户线程执行时间过长,还有可能会导致OOM
点太多,STW时间也会变长,这个时候切换线程开销很大
最好是在跳转的时候或者调用新方法的时候,执行时间比较长,比如说在进行方法调用的时候,要将方法压入虚拟机栈,把它们作为savepoint,最好不要在程序指令执行很快的时候设置saveponint,sleep和block引用关系也不会发生变化;
线程处于睡眠或者是阻塞状态,这个线程无法响应JVM中断请求,此时你在让去走到中断安全点挂起是不可能的,不可能唤醒吧;
安全区的对象引用关系不会发生变化,因此安全区域发生GC,程序依然会继续执行, 要出安全区域了但GC没有结束,程序会等待,安全区域应该是并发的,但不能走出安全区域;
枚举出整个“GC Roots”是非常麻烦的,首先运行时数据本身就是动态的,在这个枚举的过程中必须保证其原子性,并且在今天 Java应用越来越大的情况下,单单一个方法区就有可能数百上千兆,里边的类、常量等等更是多,如果要逐个找出这些 根节点实在是一个非常非常耗时的事情。
那 JVM 是如何解决的呢?
首先在进行根节点选举时,必须暂停全部的用户线程,把这个过程称为“Stop The Word”(下面简称STW,但必须要说明,STW不一定是全局的,也可以是局部的,这和安全点的类型有关。此时说的必须暂停全部用户线程只是因为GC时必须使全部线程进入安全点
在HotSpot的解决方案中,是使用一组称为OopMap的数据结构来存放这些对象的引用OopMap在类加载动作完成时生成,也就是说当用户线程暂停下来之后,其实并不需要一个不漏的检查完所有的执行上下文和全局的引用位置,而是直接通过OopMap来获取栈上或寄存器里哪里有GC管理的指针引用指针
安全点
OopMap解决了一部分问题,但没有解决所有的问题,试想一下,对象中的引用关系并非一成不变,如果每次执行一条字节码指令都去生成一个OopMap,那就必须消耗大量额外的存储空间,为了解决这个问题HotSpot并没有让每条指令都生成OopMap,而是只在特定的位置生成OopMap,这个位置就被称为安全点,放置安全点的位置一般是以“是否具有让程序长时间执行特征”为标准进行选定
引用:前提是引用关系在的情况下,都是可达的,没有引用关系啥引用都会被回收
强引用:如果发生OOM,也不会回收
软引用:内存不足回收,内存充足就不会回收,主要用于缓存,当我们第一次进行深度优先遍历之后把那些不可达的对象垃圾回收之后,发现空间还是不够,于是就会尝试把可达的软引用所指向的对象进行回收,也叫做二次回收,如果回收了软引用空间还不够,就会报OOM
但是强引用才会导致内存溢出,软引用是不会导致内存溢出的
弱引用:只要进行GC,就会进行垃圾回收,主要用于缓存
虚引用:唯一目的就是对象回收的跟踪
软引用:内存足够不会回收可触及的也就是软引用可达的对象,当堆内存不够的时候,才会回收,软引用是不会报内存溢出的,Mybatis内部缓存就是用到软引用
此时如果说刚好能容得下大数组,大对象,还刚好容不下这个弱引用,不一定说回收软引用就会发生OOM,不一定报OOM之前才会回收
public static void main(String[] args) throws InterruptedException { //声明一个强引用,将强引用的对象放入到软引用的形参里面 Object s1=new Object(); SoftReference
import java.lang.ref.SoftReference; class User{ public String username; public String password; public User(String username,String password){ this.username=username; this.password=password; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + '}'; } } public class Test { public static void main(String[] args) throws InterruptedException { //声明一个强引用,将强引用的对象放入到软引用的形参里面 User user=new User("李四","12503487"); SoftReference
弱引用:非必需的对象
软引用不如弱引用回收的快,因为软引用要使用算法判断内存是否不足
虚引用:一旦将弱引用回收,就会将虚引用存放到引用队列中,可以追踪垃圾回收过程,虚引用的作用主要是将回收的对象放在队列中 进行GC对象追踪
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Test{ public static Test test;//对当前对象的声明 public static ReferenceQueue
queue=null;//声明引用队列 public static class WorkThread extends Thread{ @Override public void run() { while (true) { if (queue != null) { PhantomReference reference = null; try { //如果这个对象被回收了,那么虚引用会被放到等待队列里面 reference = (PhantomReference ) queue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if (reference != null) { System.out.println("追踪垃圾回收过程:当前Test实例被GC了"); } } } } public static void main(String[] args) { Thread t=new WorkThread(); t.setDaemon(true);//设置为守护线程:当程序中没有非守护线程时,守护线程也就执行结束 t.start(); //1.创建虚引用队列 queue=new ReferenceQueue<>(); Test test=new Test(); //2.创建Test 对象的虚引用 PhantomReference ref=new PhantomReference<>(test,queue); test=null; //3.尝试获取到虚引用中的对象获取失败,因为虚引用的对象不可以被获取到 System.out.println(ref.get()); System.gc(); //4.执行GC之后,虚引用引用的对象会被回收,此时会把虚引用放入到引用队列里面 } } } 不同的引用类型主要是取决于不同对象的可达性状态和对象垃圾收集的影响,强引用就是普通对象的一个引用,只要有一个强引用指向一个对象就表示这个对象还活着,垃圾回收器就永远无法回收这一类的对象,只有没有其他引用关系或者是超过了引用的作用域或者是显示将引用设置为null,才会进行垃圾回收;
软引用:只有当JVM认为内存不足的时候才会进行试图回收引用所指向的对象,软引用主要适用于实现内存敏感的缓存,如果还有空闲内存,就可以暂时去保留缓存,当内存不足的时候会清理掉,这样就可以保证使用缓存的同时,不会耗尽内存
弱引用是相对于强引用关联的,不管内存是否足够都会回收弱引用
虚引用不会决定对象的生命周期,它提供了一种确保对象被finlize之后,去做某些事情的一种机制,当垃圾回收器准备去回收一个对象的时候,如果发现她还存在虚引用,就会在回收对象的内存之前,就会把这个虚引用加入到与之关联的引用的队列里面,那么程序可以通过判断引用队列是否已经加入了虚引用来去了解被引用的对象是否要进行垃圾回收,然后就可以在引用对象被回收之前来采取必要的一个行动;
终结器引用:finalReference
吞吐量=a/a+b这个数值越大越好,垃圾收集开销=b/a+b这个值越小越好
暂停时间=STW时间
收集频率:回收的频率低,不代表一次GC的时间短,大学洗衣服
一次赞一快洗(时间比较长)VS经常洗(一天一洗,时间比较短),频率越高,STW时间短一点
1)吞吐量:吞吐量越大越好,就是用户线程所消耗的时间在整个JVM生命周期中所占用的时间越长,那么垃圾频率就越低,但是每一次执行GC,那么用户线程停止,STW时间就越长(类比于洗衣服),一次性的暂停时间就很长,单位时间内用户线程做的事情更多
2)低延迟:注重每一次的暂停时间变短,用户线程暂停时间短,那么垃圾回收GC的频率就越高,因为暂停时间短,每一次GC都收集不了多少垃圾,线程频繁切换也需要时间,每一次本来就注重低延迟,要求GC垃圾回收短,况且线程上下文切换还消耗时间,每一次GC垃圾又回收不了多少,那么最终一共的STW时间肯定会比吞吐量的STW时间长(类比于洗衣服);
3)高吞吐量和低延迟是矛盾的
这就类似于洗衣服,从宿舍去水房的时间和从水房回到宿舍的时间就类似于线程切换
和用户交互的程序,延迟要短一些,争取在垃圾回收的过程中多线程回收
有的是服务器端,吞吐量要高一些
G1垃圾回收器就是可以保证在给定停顿时间的基础上,尽量的提高吞吐量
JDK7之前,实线,Serial OLD GC是CMS的后备方案
在JDK9中取消了红线组合
在JDK14中绿线会被删除
CMS和PSGC框架不同,不可以一起使用,PNGC和PSGC性能差不多
CMS:不能是在老年代空间满的时候进行使用,需要提前进行回收,因为CMS是并发的,他在回收的时候用户线程还在执行,用户线程还有可能制造新的垃圾,所以需要提前进行回收,那如果说回收的比较晚,垃圾制造的速度比回收的速度还要快,可能CMS回收失败一旦失败,所以要使用SOGC作为备用方案,赶紧把用户线程停下来进行全部GC
单核CPU是单线程垃圾收集器比多线程垃圾收集器要高,因为防止进行大量的线程切换
Serial(新生代)+Serial Old(老年代):单线程垃圾回收:
当新生代使用serial的时候老年代默认使用Serial Old,他们在执行的时候必须停止所有的用户线程
-XX:PrintCommandLineFlags -XX:+UseSerialGC,表明新生代使用Serial GC,老年代使用Serial Old GC
然后可以通过jps验证一下,jinfo -flag UseSerialGC +进程的ID
缺点:串行垃圾回收器会导致验证的STW
parNew新生代并行垃圾回收器+和Serial Old(单线程穿行垃圾回收器)或者是CMS(老年代并行垃圾回收器)一起使用
在服务器端模式下是多核CPU的场景,这个时候就不和客户端一样是一个单线程的垃圾回收器了,服务器端硬件更多一些
在老年代可以使用CMS或者是Serial Old,在JDK9中Serial Old不能再和ParewNew使用了,在JDK14CMS也被移除了,这个时候ParNew就比较尴尬了
对于新生代,使用多线程垃圾回收器,使得GC的时间更短,垃圾回收更高效STW时间更短
但是在老年代,标记整理算法效率比较差,涉及到内存碎片整理
单CPU:同一时刻只能由一个线程执行
设置线程数量不要超过CPU核数,放置多个线程抢夺CPU,和CPU核数相同越好
-XX:PrintCommandLineFlags -XX:+UseParNewSerialGC -XX:+UseConcMarkSweepGC