3.垃圾回收
1.对象什么时候可以被垃圾器回收
1.垃圾回收的概念
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以, 在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自 动识别完成。
2.对象什么时候被回收
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
3.引用计数法
说明:一开始执行String demo = new String("123"); 时,栈中会创建一个demo变量,由于是new,会在堆中开辟一个空间存储当前对象。此时ref = 1。后面执行 String demo = null; demo变量就不再指向堆中的对象了,ref = 0;此时说明个对象可以被垃圾回收,释放内存。
但是,这种方法也有问题:
说明:一开始 a,b 指向堆中对象时 ref = 2;后面指向空时,由于堆中a,b对象之间出现了循环引用,就导致了:没有变量引用这两个对象,但 ref = 1,无法垃圾回收!这就可能导致内存泄漏。
4.可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象
局部变量,静态方法,静态变量,类信息
核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用, 则可以当做垃圾回收
说明:
X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。
finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。
四种GC Root:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
/**
* demo是栈帧中的本地变量,当 demo = null 时,由于此时 demo 充当了 GC
Root 的作用,demo与原来指向的实例 new Demo() 断开了连接,对象被回收。
*/
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
2.方法区中类静态属性引用的对象
/**
* 当栈帧中的本地变量 b = null 时,由于 b 原来指向的对象与 GC Root (变量
* b) 断开了连接,所以 b 原来指向的对象会被回收,而由于我们给 a 赋值了变量的
* 引用,a在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存
* 活!
*/
public class Demo {
public static Demo a;
public static void main(String[] args) {
Demo b = new Demo();
b.a = new Demo();
b = null;
}
}
3.方法区中常量引用的对象
/**
* 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收
*/
public class Demo {
public static final Demo a = new Demo();
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
4.本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
2.垃圾回收的算法有哪些
通过可达性分析算法,我们已经可以找到需要回收的对象。现在需要通过垃圾回收算法,把垃圾回收,释放内存。
1.标记清除算法(使用较少)
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root 节点引用的对象都会被回收。
同样,标记清除算法也是有缺点的:
1.效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要 停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
2.(重要)通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
导致的问题:我们知道数组也会存储到堆中,并且数组存储需要的是连续的内存空间。如果碎片化严重,就可能无法存储一个较大的数组!
2.复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块, 在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
1)将内存区域分成两部分,每次操作其中一个。
2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。
3)周而复始。
优点: 在垃圾对象多的情况下,效率较高清理后,内存无碎片
缺点: 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
一般年轻代的垃圾回收器使用
3.标记整理算法
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。
1)标记垃圾。
2)需要清除向右边走,不需要清除的向左边走。
3)清除边界以外的垃圾。
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
与复制算法对比:复制算法标记完就复制,但标记整理算法得等把所有存活对象 都标记完毕,再进行整理。
一般老年代的垃圾回收器使用
3.分代回收
分代收集算法
回收机制
1.新创建的对象,都会先分配到eden区
2.当伊甸园内存不足,标记eden区与 from区(现阶段没有)的存活对象
将存活对象(假设这里只有A存活)采用复制算法复制到 to 中,复制完毕后,eden和 from 内存都得到释放
3.经过一段时间后伊甸园的内存又出现不足,标记eden区域和to区存活的对象(假设这里A和1都存活), 将存活的对象复制到from区
4.循环往复,每次eden满了以后就标记eden和from/to区域存活对象,将存活对象复制到to/from区。每次清理完,就只有from/to中的一个区域可能有存活对象!
直到:当幸存区(from和to)对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
这里A就是移动次数超过15次,而被放入老年区的对象,因为可以看作很长一段时间都会使用该对象,所以放入老年区。(也有可能是A占用内存过大,或者幸存者区内存不足而移到老年区)
MinorGC、 Mixed GC 、 FullGC的区别是什么
MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免。一般发生在新生代和老年代内存严重不足时。
名词解释: STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
小结
4.Jvm中的垃圾回收器
在jvm中,实现了多种垃圾收集器,
包括: 1.串行垃圾收集器 2.并行垃圾收集器 3.CMS(并发)垃圾收集器 4.G1垃圾收集器
1.串行垃圾回收器
效率低,使用较少
2.并行垃圾回收器
3.并发垃圾回收器
主要针对老年代进行垃圾回收
说明:
1.初始标记时,就是采用可达性分析算法进行标记,此时会阻塞其他线程(阻塞时间很短,不会影响用户体验)此时,只标记了GC Roots和GC Roots直接关联的对象A
2.并发标记,此时,不会阻塞线程,会从A开始找到所有存活的对象:A、B、C、D、GC Roots。
3.重新标记,为什么这里还要重新标记一遍呢?这是因为当时并发标记的时候,其他线程还在执行,如果这期间,比如说:之前没有被引用的X,在此期间被A引用了,这里重新标记的时候,就会把X标记为存活对象
注:重新标记的时候,所有线程共同参与,就不会出现新的引用!
4.并发清理,其他线程照常执行。
小结
5.G1垃圾回收器
概述
1.Young Collection(年轻代垃圾回收)
说明:下图中 e代表eden区(伊甸园),s代表幸存者区,o代表老年代
初始时,所有区域都处于空闲状态
创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
G1会对eden区占比进行限制,比如说当eden区占百分之五的时候,触发垃圾回收。当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象到幸存者区,其他区域的空间就会被释放,标记和释放的时候都需要暂停用户线程(stw)。
随着时间流逝,伊甸园的内存又有不足
将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
2.Concurrent Mark (并发标记)
当老年代占用内存超过阈值(默认是45%)后,触发并发标记,标记老年代中存活的对象,这时无需暂停用户线程
并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程(由于之前已经标记过一次,因此此时就算要暂停所有线程,时间也非常短,不会影响用户体验)。
这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,因为如果一次回收所有的老年代垃圾,会导致暂停时间太长。这里我们可以设置一个预期暂停时间,比如暂停时间不超过多少毫秒。因此为了不超过预期暂停时间,不是一次把所有老年代垃圾回收。而是根据暂停时间目标优先回收价值高 (存活对象比例较少)的区域(这也是 Gabage First 名称的由来)。
3.Mixed Collection (混合垃圾回收)
由于并发标记阶段已经把要回收的老年代垃圾标记出来了。在此阶段,某些回收价值高的区域会和eden区、幸存者区的垃圾一起被回收。
eden区和幸存者区还是存活对象还是会被复制到新的幸存者区,幸存者区中存活较久的对象、某些回收价值高的老年区中存活的对象,会被复制到新的老年区。
注意:混合垃圾回收可能要执行多次,才能把内存释放到阈值以下。
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
注:其中H叫做巨型对象,如果对象非常大,会开辟一块连续的空间存储巨型对象
如果我们垃圾回收的速度小于分配新对象的速度,就会出现并发失败,会触发一次Full GC,暂停时间较久(出现概率很低)。
小结
6.Java中的引用类型
1.强引用
一个对象A被局部变量、静态变量引用了就产生了强引用。因为局部变量、静态变量都是被GC Root对象关联上的,所以被引用的对象A,就在GC Root的引用链上了。只要这一层关系存在,对象A就不会被垃圾回收器回收。所以只要方法不退出,局部变量就会保存当前对象A的地址,该对象就不会被回收。
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
while (true) {
//以下两行代码产生了两个引用关系,1.bytes变量 到 new byte[1024 * 1024]; 这个字节数组的引用关系,这也是一个强引用
byte[] bytes = new byte[1024 * 1024];
//2. objects集合对象 到 字节数组的关系,也是强引用
objects.add(bytes);
//每一轮while循环结束后,第一个引用关系就断开了。
//但第二个引用关系没有断开!所以 bytes数组1mb大小的内存空间是不会被回收的。
//这样就会导致每轮循环内存中增加1mb的数据,而且不能被回收
}
}
最后一次垃圾回收:回收前7873k,回收后7831k。回收前后内存差不多。说明有大量强引用在,而强引用不能被回收。最终内存不够用而报错。
2.软引用
由于软引用允许垃圾回收器在内存不足时回收对象。所以软引用放的对象一般都不是很重要的对象,否则内存不足时,这个对象一被回收,某些核心数据就没了。主要应用场景在缓存里面。
public static void main(String[] args) {
ArrayList<SoftReference> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[1024 * 1024];
//软引用
SoftReference<byte[]> softReference = new SoftReference<>(bytes);
//将软引用对象放入集合中
objects.add(softReference);
System.out.println(i);
}
for (SoftReference softReference : objects) {//打印一下没有被回收的软引用
System.out.println(softReference.get());
}
}
注:这里设置了堆内存10mb(由于还要其他强引用占用空间,所以存不下10个1mb的字节数组),且打印垃圾回收日志
说明:可以看到前七次没有进行垃圾回收,在到下标为6时,进行垃圾回收,这个时候就会把所有软引用对象全部回收,前七个对象没有了。最后再放入7-9三个字节数组对象。
最终打印集合:前七个为null(被回收了),只有后三个了。
注:这种机制很适合缓存,因为就算缓存中软引用的数据因为内存不足被回收了,也可以去数据库中查询数据。
3.弱引用
弱引用的生命周期比软引用更短,无论内存是否充足,只要垃圾回收器开始工作,弱引用关联的对象都会被回收。弱引用适用于实现可以随时被回收的缓存数据,因为它不受内存状况的影响。
null
null
null
null
null
null
null
null
null
[B@4eec7777
进程已结束,退出代码0
执行结果:前九个为空,最后一个由于强引用存在,没有被回收。
4.虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被JVM当作垃圾进行GC。
7.ThreadLocal中为什么使用弱引用
补个概念:
ThreadLocalMap中的key就是Entry,Entry是一个弱引用,关联了当前ThreadLocal对象。需要存储的数据为值。调用set方法要传入两个参数ThreadLocal对象和要存入ThreadLocal对象的数据。
如下图:threadLocal.set(new User(1,"main线程对象")); 这行代码(User类是一个自定义类,threadLocal是当前线程)。等于说通过Entry弱引用,既可以找到ThreadLocal对象,又可以找到存放的value值
同理:get方法可以获取数据
为什么ThreadLocal要使用弱引用?
现在我们调用 threadlocal = null; 使静态变量和ThreadLocal对象之间的强引用关系去掉。现在这个ThreadLocal对象就可以被回收。根据弱引用的特点:假设某个对象只有弱引用关联,该对象是可以被回收的!(假设Entry和ThreadLocal之间使用强引用,没人指向这个ThreadLocal对象,但它还是不能被回收,就出现内存泄漏!)
总结一下使用弱引用的好处:如果ThreadLocal对象不再使用了(不再被引用了)。尽管它还被ThreadLocalMap引用着,它依然可以被回收。
还有一个问题:由于Entry和value之间是强引用,那么这个value对象如何被回收呢?
由于之前ThreadLocal对象已经没有强引用了,它就会被回收。现在调用set/get/remove方法时,发现当前Entry所关联的ThreadLocal已经没有了。这个Entry所关联的value对象也会被回收。
但如果一直没有调用set、get、remove方法,那么这个Entry和value就一直不会被回收,从而可能导致内存泄漏。如何解决呢?
因此,当我们某个ThreadLocal不再使用了,就要手动调用remove方法将ThreadLocalMap和Entry之间的引用断掉。这样垃圾GC Root找不到这两个对象,就会被垃圾回收了!
总结
4.JVM实战
1.JVM中的常见参数
-Xms:设置堆的初始化大小
-Xmx:设置堆的最大大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小 之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
注:如果使用的是G1垃圾回收器,年轻代大小就不用设置了。G1垃圾回收器会帮我们动态调整年轻代大小,以适配垃圾回收时间。
参数模板
2.内存泄漏问题
内存泄漏的概念
解决思路
1.发现问题
例:如下面这段代码
由于objects对bytes的强引用,所以每次while循环结束了,bytes数组也没办法被垃圾回收器回收。从而达到内存一直上涨的情况。
通过visualvm工具也能看出来,堆内存的使用量一直在增加。即使手动fullgc也没办法清理内存。
现在把objects对bytes的强引用去掉。现在每次while执行结束以后,bytes就没有被引用,就可以被垃圾回收了!
现在就能看出来,内存占用在一个稳定的范围之内,而且离最大堆内存有很大距离。
2.诊断
生成的内存快照文件,可以通过检查工具(如MAT)打开,这些工具可以判断出可能出现的原因,及代码位置。
3.修复问题
3.cpu飙高问题
ps H -eo pid,tid,%cpu | grep 40940
命令说明:
-
ps
: 这是一个用于显示当前运行的进程信息的命令。 -
H
: 这个选项的作用是显示每个进程的线程信息,包括它们的进程ID(PID)和线程ID(TID)。 -
-eo
: 这个选项用于指定输出的字段,其中pid
代表进程ID,tid
代表线程ID,%cpu
代表CPU使用率。 -
grep
: 这个命令用于过滤输出,只显示包含特定文本的结果。在这种情况下,它会过滤出所有包含进程ID40940
的行。