JVMGC浅析

为什么在C++避之莫及的new在java中却趋之若鹜,为什么JAVA的类不用写析构函数

垃圾回收机制的意义

JVM中的垃圾回收机制(GC)是帮助程序员自动释放内存的。任何程序的运行都会在内存上进行,内存是有大小的,当一个程序只使用而不释放内存的话,致使内存逐渐占用完,导致程序崩溃,这样一来就容易出现_内存泄漏_。

在Java中,引入GC来解决上述问题,来减少内存泄漏出现的概率。

JVM内存区域划分

JVM其实也是一个进程,它是一个Java进程。运行Java进程,Java进程就会从内存中申请一片内存区域,以供Java使用。

内存区域进一步划分(核心区域)以及用途

  1. 堆:用来存放实例化对象(即new出来的对象)和成员变量
  2. 栈:维护方法之间的调用关系,以及存放一些局部变量
  3. 元数据区(也叫方法区):用来存放类加载之后的类对象以及静态变量。
    注:类对象:就是类的实例化

例:给一段代码,试说出其中某一个变量在内存的哪个区域。
此类题目要看这个变量的形态,即是什么变量(局部,成员,静态),再结合上面的区域划分,以此来判断变量在内存中的区域。

void Test(){
	Dog a = new Dog();
}
123

其中,void Test() 作为一个方法,是在方法区 ,a是一个局部变量,在栈上,new Dog() 是一个实例化对象,对象本体是在堆上。

在JVM进程中,堆和元数据区只有一份
栈和程序计数器(JVM内存区域中的一部分)存在多份,每个线程中都有一份

主要回收的内存区域

对于内存的释放,存在下列问题:

  • 什么时候申请内存?对于内存的申请,当然是使用了才申请
  • 什么时候释放内存?彻底不使用了才能释放内存。

使用程序,内存自然就申请了,那么,"彻底不使用程序"该怎么判断,就是接下来GC所要做的事。

GC主要释放的是堆区 (即实例化对象和成员变量)。程序计数器(一个用来存地址的整数)以及栈,都是随线程一起销毁;方法调用完后,方法的局部变量自然就随着出栈操作一起销毁了;元数据区(方法区),存的类对象,很少会释放。
GC是以 对象 为单位进行内存释放的。

GC主要的两个阶段

找垃圾(进行标记)

前面提到,当一个程序(在GC中就是对象)彻底不使用了,就认为是垃圾。即当一个对象彻底不使用了,就是垃圾。
Java中对于对象的使用,只能通过引用,那么就可以通过这个对象有没有引用来判断是否为垃圾,如果一个对象没有引用指向它,那么这个对象就是垃圾,反之就不是垃圾了。

引用计数法(Python ,PHP采用)

设定:在每个对象里面有一个额外的空间,用来保存一个整数,表示该对象有几个引用


随着引用的增加,引用计数器就增加,引用的销毁计数器就减少,当引用计数器为0了,就证明该对象无指向它的引用,就认为是垃圾。

缺点:

  • 浪费内存空间。一个引用计数器至少是int类型,4个字节,若对象很小,则4个字节对于对象来说是"巨额开销"。
  • 存在循环引用,导致引用计数器逻辑判断出错。

可达性分析法(Java采用)

设定:把对象之间的引用关系转换成一个树形结构,从起点出发进行遍历,把能遍历到的对象就称为"可达",不可达的就认为是垃圾。

如上图,从根结点a出发进行遍历,这样一来每个结点都是可达的。
若当 root.right.right = null,f 结点就不可达了,这样就认为 f 结点为垃圾,那么对应的对象就会被释放。
若当 root.lift = null, 那么b,d,e,g结点都会不可达,对应的各个对象也就会被认为是垃圾而被释放掉。
通过上述 root 这个引用,就能引用到每个结点,访问到树的任意点。

那么root该如何选取,在Java中,可以作为GC Roots的如下:

  • 栈中引用的对象,即栈上的局部变量
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

总的来说,可达性分析法就是从所有的GC Roots起点出发,通过root对象里面的引用来访问别的对象(即哪个对象引用root对象就访问该对象),把所有可以访问的对象都逐一遍历一遍,并在遍历的同时把对象标记为“可达”,没被标记的就自然是不可达了。

可达性分析法虽然克服了引用计数法的缺点,但它也存在一定的问题:

  • 消耗更多时间,垃圾不能及时被标记释放。在进行逐一遍历对象的时候是比较慢的。这样致使一旦某个对象成为垃圾,也不能很快就发现。
  • 在逐一遍历过程中,若某个对象的引用关系发生了改变,就会出错,使得标记更加麻烦。

释放垃圾(内存回收)

若对象被标记,则该对象就是垃圾。
垃圾回收算法(释放垃圾的典型实现):

标记清除算法

将标记为垃圾对象的内存直接释放,未被标记的对象就还会在内存中,这样未被标记对象之间还有一块释放后的内存,产生内存碎片。而对于内存空间的申请都是申请一片连续的空间,即使上述内存空闲空间很大(超过申请的内存空间),但也有可能无法申请。

  • 产生内存碎片。
  • 标记和清除两个过程效率不高

复制算法

把整个内存分成两半,一次只使用一半

上面内存中有5个对象,当2,3为垃圾时,就将1,4,5对象复制到另外一半中,然后将左边这一半整体释放掉。

简言之,就是将不是垃圾的对象复制到另一半内存中,然后再将有垃圾的这半内存整体释放掉

  • 解决了内存碎片问题,但内存利用率较低
  • 若当前对象大部分都是未被标记的,即垃圾较少,则复制成本就很高,效率变低

标记整理算法


图中,2,4为垃圾,将3移动到2处,5移动到3处,6移动到4处,如下图:

然后再将后面的内存释放。

  • 解决内存碎片问题,移动开销大

分代算法(算法更佳)

定义:分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略。
一般是把Java堆分为新生代老年代在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,采用_标记清理_或者_标记整理法_

可以理解为给每个对象都设定了一个“年龄”,年龄用来表示这个对象存活了多久,每经过一次可达性分析,没有被标记成垃圾,这给对象的“年龄”就会增加一岁

新生代:一般创建的对象都会进入新生代
老年代:一些大的对象和一些经历过很多次(一般默认情况是15次)垃圾回收依然存活下来的对象,该对象就会从新生代移动到老年代。

  • Minor GC:又称为新生代GC ,指的是发生在新生代的垃圾收集。Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  • Full GC :又称为老年代GC或者Major GC ,指发生在老年代的垃圾收集。 出现了Major GC,经常会伴随至少一次的Minor GC(不是绝对的),Major GC的速度Minor GC慢。

图文:

  • 伊甸区:其中伊甸区用来存放刚创建的对象。在第一轮垃圾回收中会释放掉绝大部分对象。
  • 幸存区:为空间大小相同的两个部分,一次只使用一半。通过第一轮的垃圾回收会将存活的对象通过复制算法拷贝到幸存区中,若当GC再次进行扫描时,垃圾对象就会直接被释放,而不是垃圾对象的就会通过复制算法拷贝到另一半中。
  • 老年代:当一个对象在生存区中经过多轮垃圾回收还存在的,就会通过复制算法拷贝到老年代中;一些大的对象会直接被放到老年代中;若老年代中有对象被标记为垃圾,会通过标记整理法或者标记清除法释放。
  • 新生代会比老年代经历垃圾回收机制次数多
posted @ 2024-01-02 22:11  加固文明幻景  阅读(20)  评论(4编辑  收藏  举报