Java和C++之间有一堵由内存自动分配和垃圾收集技术围成的高墙
1.了解垃圾收集、内存自动分配的意义
2.JAVA虚拟机各个区域的垃圾回收简介
3.判断对象是否存活
3.1引用计数算法
3.2可达性分析算法
3.2.1简介
3.2.2GC Roots说明
3.3引用简介
3.4真正的死亡回收
4.方法区垃圾回收
4.1废弃的常量
4.2无用的类型
1.了解垃圾收集、内存自动分配的意义
当需要排查各种内存溢出、内存泄漏问题时,当垃圾回收成为高并发的瓶颈时,就需要我们对这些自动化的技术进行监控和调节
2.JAVA虚拟机各个区域的垃圾回收简介
对于程序计数器、虚拟机栈、本地方法栈,他们的生命周期和线程同步,栈中的栈帧随着方法的执行进栈和出栈。它们的回收相对简单,随着线程或者方法的结束就可以回收。而JAVA堆和方法区(永久代/元空间)的回收相对复杂。
3.判断对象是否存活
3.1引用计数算法
当一个对象被引用时,计数器值加1,当引用失效时,计数值减1,当对象没有被引用时,认为对象已死。在主流的JAVA虚拟机里面都没有使用这个算法。因为这个算法看起来很简单,但是却需要考虑很多特殊的情况,需要配合大量的额外处理才能够确保工作的额正确进行。比如两个对象之间的相互循环引用。
3.2可达性分析算法
3.2.1简介
主流的商用程序语言,都是采用的这个算法
这个算法的基本思路就是通过一系列被称为"GC Roots"的根对象作为起始节点集,从这些节点开始,通过运用关系向下搜索,搜索过程的路径成为"引用链"。如果某个对象到"GC Roots"之间没有引用链,则证明该对象不可能再被使用。
如下图,5/6/7之间有引用,但是没有连接到GC Roots,所以判定它们是可回收对象
3.2.2 GC Roots说明
在Java语言中,可作为GC Roots的对象包含以下几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
2)方法区中静态属性引用的对象,比如JAVA类的引用类型静态变量
3)方法区中常量引用的对象
4)本地方法栈中(Native方法)引用的对象
5)所有被同步锁(synchronized)持有的对象
6)反反映了虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
7)除了上面这些固定的GC Roots集合外,根据用户选用的垃圾回收器以及当前回收的内存区域不同,还可以有其它对象临时性的加入。
(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,它就会作为GC Root。
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
(4)最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
3.3 引用简介
在JDK1.2以前,Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。
我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft
Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
1)强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A
a = new A()这个意思。
2)软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,会把这些对象列入回收范围进行第二次回收。软引用可用来实现内存敏感的高速缓存。
3)弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
4)虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
3.4真正的死亡回收
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
1)判断对象是否在GC Roots引用链上,如果不在,进行第一次标记
2)判断该对象是否有必要执行的finalize()方法。当对象没有覆盖(重写)finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收
3) 如果这个对象被判定为有必要执行finalize()方法,执行finalize()方法,再判断是否在GC Roots引用链上,不在,对象被回收
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
示例:下面示例演示垃圾回收时finalize方法被系统自动调用
package com.jenkin.wx.Test; /** * @Classname GcClass2 * @Description TODO * @Date 2021/11/1 0001 下午 3:33 * @Created by jcc */ public class GcClass2 { public static GcClass2 c = null; @Override protected void finalize() throws Throwable { //重写finalize()方法 super.finalize(); System.out.println("finalize方法执行"); c = this; } public static void main(String[] args) throws InterruptedException { c = new GcClass2(); //创建个对象
c = null; //对象不再被引用 System.gc(); //垃圾回收 Thread.sleep(200); //Thread.sleep(200),等待垃圾回收、finalize()方法执行 if(c == null){ System.out.println("被回收了"); }else{ System.out.println("活着"); }
//这里的代码上面的代码一模一样,同样的代码执行两遍 c = null; System.gc(); Thread.sleep(200); if(c == null){ System.out.println("被回收了"); }else{ System.out.println("活着"); } } }
执行
finalize方法执行
活着
被回收了
发现,第一个判断中,c不为null,因为垃圾回收时,执行了finalize方法,这个对象被赋值给变量c了
第二个片段中,c为null,说明垃圾回收时,finalize方法没有再次执行。
因为任何一个对象的finalize方法都只会被系统自动调用一次。
注意:finalize方法被官方申明为不推荐使用的语法,不推荐使用。
4.方法区垃圾回收
很多人以为方法区(或者HotSopt VM中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且性价比一般较低,在对的新生代生一般能回收70%~95%的空间,而永久代远低于此。
方法区垃圾回收主要包含两块内容。废弃的常量和不在使用的类型。
4.1废弃的常量
回收废弃常量与回收Java堆中的对象非常相似。以常量池中字面量的回收为例,若字符串“abc”已经进入常量池中,但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该“abc”就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。
4.2无用的类型
需要满足3个条件:
(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而不是必然回收