本帖最后由 逆风TO 于 2019-12-18 10:25 编辑
•写在前面
说起Java和C++,很容易想到让人疯狂的指针,Java使用了内存动态分配和垃圾回收技术,让我们从C++的各种指针问题中摆脱出来,更加专心于业务逻辑,不过如果我们需要深入了解java的JVM相关原理,我们必须要面对这些东西,深入了解JVM在内存动态分配和垃圾回收技术的原理知识,这篇文章就是来做一个先导,在jvm进行垃圾回收之前,它必须要知道回收的对象是否已“死”,这样才能保证程序的正常稳定。
•对象的创建
我们将回收对象前,先讲讲在虚拟机上,对象是怎么被创建的。在我们编写代码的角度(语言层面)来看,我们创建一个对象实例,只需要使用new关键词就完事儿了,很简单,不过你享受的简单是因为虚拟机帮你承受了所有繁琐的工作,那虚拟机是怎么工作创建一个对象的呢?
当虚拟机遇到new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用(没有类,创建个锤子的对象),并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须要执行相应的类加载过程。这是第一步,在类加载检查过后,接下来虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后便已经完全确定了(这里插一句,如何确定的?这就和对象的内存布局有关了,对象在内存中的布局可以分为3个区域,分别是对象头、实例数据和对齐填充,对象头里面存的是对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID等等之类的信息,也就是和储存数据无关的额外内存空间,按道理这一块空间应该是固定的,不过在设计上还是被弄成了非固定的数据结构,这样更具不同的类节省空间,不深入不然扯不完,想要可以看另外一篇文章。接下来实例数据就是对象真正储存的有效信息,也是程序代码中所定义的各种类型的字段内容。最后一个对齐填充,顾名思义就是填补空间,因为以HotSpotVM为例,对象的大小必须是8字节的整数倍,所以就靠这个补全),给对象分配空间的任务相当于把一块确定大小的内存从Java堆中划分出来(为啥可以看我另一篇文章,运行时数据区)。
划分的时候会出现两种情况,第一种就是java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间的一边移动对象大小相等的距离,这种分配方式就是“指针碰撞”。第二种情况就是空间不规整,也就是已使用的内存和空闲内存相互交错,这个时候指针碰撞起不来作用,那么这个时候虚拟机必须维护一个列表,记录哪些内存可用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新相关内存信息,这种方式叫做“空闲列表”。因为创建对象非常频繁,所以会涉及到并发的时候,会出现一个叫做“本地线程分配缓冲”的概念,我这里也不深入,自己去查,哈哈哈。空间分配完成之后,虚拟机需要分配到的内存空间都进行初始化为零值(注意不包括对象头),这样就保证对象的实例字段在java代码中可以不赋初始值就直接使用。最后虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码、对象的GC分代等信息。到此,对于虚拟机来说,对象创建完毕。
•引用计数算法
引用计数是一个很好理解的概念,就是给对象添加一个引用计数器,每当有一个地方引用这个对象时,计数器值加1,每当一个引用失效时,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。是不是很好理解,而且判定对象是否可用效率很高,在大部分时候它是一个很不错的算法,不过要注意,是大部分时候。在java虚拟机中,并没有使用这个算法来管理内存,其中最主要的原因就是它很难解决对象之间循环引用的问题。来,举个例子来理解,比如现在有两个对象objectA和objectB都有字段instance,赋值让objectA.instance = objectB, objectB.instance = objectA,除此之外没有任何其他引用,实际上这两个对象已经不可能再被访问了,但是因为它们两个互相引用这对方,导致它们的引用计数不为0,则算法不能通知GC收集器回收它们。所以这种算法不适合在虚拟机上使用,但是并不是说这个算法很垃圾,它可是在其他方面有很多著名的案例。
•可达性分析算法
JVM的主流实现是可达性分析,可达性分析在概念上其实也不难理解,它的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(图论里面专业一点来说,就是从GC Roots到这个对象不可达),则证明对象是不可用的,大致可以像下图理解。
那么哪些对象可以作为GC Roots对象呢?在java中大致有如下几种:
•虚拟机栈(栈帧中的本地变量表)中引用的对象;(不知道栈帧是啥的看我另一篇文章,运行时数据区)
•方法区中类静态属性引用的对象;
•方法区常量引用的对象;
•本地方法栈中JNI(即一般说的Native方法)引用的对象
•引用
引用是啥?搞过C++的我们第一反应就会回答,如果reference类型的数据中储存的数值代表的是另一个内存的起始地址,就称这块内存代表着一个引用。这种定义没有错,不过太狭隘了,一个对象在这种定义下只能被引用或者没有被引用两种状态,显然在回收中不足以应付碰到的情况。所以,java对引用概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度一次逐渐减弱。
•强引用,就是指在程序代码之中普遍存在的,类似A a = new A()这样的引用,只要强引用存在,垃圾回收器就不会回收掉被引用的对象;
•软引用,用来描述一些还有用但并非必须的对象,对于软引用的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会出现内存溢出异常;
•弱引用,也是用来描述非必需的对象,但是它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次回收发生之前,当垃圾回收器工作时,无论当前内存是否足够,都会回收掉;
•虚引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例、为一个对象设置引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
•不可达必须“死”?
其实在实际中,就算在可达性分析算法中不可达的对象,也并非一定会回收,这个时候不可达的对象暂时处于暂缓的阶段,一个对象要真正宣告死亡,至少要经历两次标记的过程,当对象进行可达性分析而不可达时,它会被第一次标记并且进行一次筛选,筛选条件是这个对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被调用过了,虚拟机将会把这两种情况视为没有必要执行finalize。当对象被判定有必要执行finalize时,对象将会被放置在一个叫做F-Queue的队列中,并在稍后的一个由虚拟机自动建立的、优先级低的一个Finalizer线程去出发这些对象的finalize(要注意的是,虚拟机并不承诺会等待这些对象finalize方法执行结束,这是因为如果一个对象的finalize方法执行缓慢、或者发生死循环,将导致F-Queue队列其他对象处于永久等待,甚至导致内存回收系统崩溃)。finalize是对象逃脱回收的最后一次机会,GC会将F-Queue中的对象进行第二次小规模的标记,如果对象在finalize中重新和引用链连上了,那么就被移出回收集合,没有逃脱则将会被回收(要记住哦,对象的finalize只能被执行一次,也就是说当对象通过finalize逃脱回收之后,下一次如果再被可达性分析标记,那么就逃不了了)。
•最后
其实很多时候我们谈论回收都在java堆上进行的,上面对象实例都是在java堆上进行的,很少谈及方法区的回收,因为方法区(一般被称为永久代)中的回收条件很苛刻,比如在java堆上进行回收可以达到70%-95%的空间,在方法区却低很低,但并不代表方法区不能有垃圾回收,Java虚拟机规范中,只是说可以不要求在方法区实现回收机制。
|
|