Fork me on GitHub

深入理解JVM虚拟机 第3章 垃圾回收器和内存分配策略

垃圾回收和内存分配策略

垃圾收集(Garbage Collection, GC)主要关注的是java堆。因为虚拟机栈,本地方法栈,程序计数器都是随着线程的产生和消失。所以这部分的内存分配是在编译器可知。对它的内存分配是确定的。对于java堆的对象实例,只有在运行期间才知道创建那些对象实例,则这部分的内存分配和回收是动态的。

引用计数算法

给对象添加引用计数器,有引用就值加1,失效就减1.计数器为0就不可能再被使用。效率高,但是不能解决对象相互引用的问题。
GC Rootsde 的可达性分析来判断对象是否存活。比如

1
2
3
4
5
6
7
8
9
10
public class A{
public Object instance = null;
}
A a1 = new A();
A a2 = new A();
a1.instance = a2;
a2.instance = a1;
a1 = null;
a2 = null;
System.gc();

则a1和a2应该要被回收的,但是他们有引用,不可能被回收的问题。
可达性分析算法:通过一系列的称为”GC Roots”的对象作为起始点。当对象到GC Roots之间有可达的引用连认为对象可用。否则就认为对象不可达,可以回收。当一个对象到GC Roots 没有任何链相连就认为对象是不可用的。
作为GC Roots的对象包括:

  1. 虚拟机栈(栈帧的本地变量表)中引用的对象。
  2. 本地方法栈中的JNI(native 方法)引用的对象。
  3. 方法区中的常量或者类静态属性引用的对象。 // static的属性引用的对象 因为static是类加载就生成对象了

引用类型

1
2
3
4
1. 强引用 new出来的对象,只要引用存在,该对象就不会被回收 
2. 软引用 SoftReference 引用一些有用但并非必须的对象,当系统出现内存溢出异常之前会对软引用的对象进行回收,回收后还没有足够的内存才会内存溢出
3. 弱引用 被弱引用关联的对象只能生存到下一次的垃圾回收之前。即对象被引用了还是会被回收。WeakReferemce
4. 虚引用 无法通过虚引用来获得对象的实例,设置虚引用的目的是对象被垃圾回收时收到一个系统通知。PhantomReference

回收方法区

回收方法区方法区(永久代)回收两个部分:废弃常量和无用的类。回收废弃常量比较简单。只要没有对象引用这个常量就可以将该常量移出常量池。在常量池中有“abc”这个字符串,只要没有任何的String对象引用这个对象,该对象就会被回收。
回收无用的类:

  1. 对象的实例被全部回收。
  2. 加载该类的classloader被回收。
  3. class对象没有被引用,无法通过反射调用该类的方法。
  4. 该类可以被回收。
    可达性分析:
1
2
3
4
5
6
7
1. Object aobj = new Object ( ) ;
2. Object bobj = new Object ( ) ;
3. Object cobj = new Object ( ) ;
4. aobj = bobj;
5. aobj = cobj;
6. cobj = null;
7. aobj = null;

第4行和第7行都会导致有对象被回收。因为aobj、bobj、cobj都是虚拟机栈的局部变量表中的reference所指的对象。而其中new的3个object是存放在java堆中的。那么如果aobj=bobj后,那么aobj所指向的object就没有gc roots的引用链可以引用,就会被回收。而cobj也会被回收。

1
2
3
1. String str = new String("hello");
2. SoftReference<String> sr = new SoftReference<String>(new String("java"));//软引用
3. WeakReference<String> wr = new WeakReference<String>(new String("world")); //弱引用

第二个在内存不足会被回收,第三个一定会被回收。

总结下常用的会被回收的情况

1.显示的将引用赋值为null或者将已经指向某个对象的引用指向新的对象。

1
2
3
4
5
1. Object obj = new Object();
2. obj = null;
3. Object obj1 = new Object();
4. Object obj2 = new Object();
5. obj1 = obj2;

2.局部引用指向的对象

1
2
3
4
5
6
7
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}

循环每执行一次,生成的object对象都会被回收。
3.只有若引用与其关联的对象

1
WeakReference<String> wr = new WeakReference<String>(new String("world"));

垃圾回收算法

Mark-Sweep(标记清除算法)

标记出所有需要被回收的对象,清除就是回收所有的标记对象的占用的内存。
5EA3262E-6138-41AB-B06E-C7C9A21F4863
图中看出容易产生碎片。后续无法为大的对象分配内存。

复制算法

将内存分为相等的2快,每次垃圾回收将存活的对象全复制到另外一般的干净的内存中,然后对整半个内存清理。 不需要考虑内存碎片等情况。缺点:内存缩小到原来的一半。
37B31B22-A277-464D-B561-CA1B188A2808

现在的虚拟机都是采用这个方法来收集新生代:新生成的对象98%是要死的。那么存活的对象很少。可以把内存分为1个较大的Edge区域和2个Survicor空间。比例为8:1。那么每次回收就把Survivior的对象和Eden对象的存活对象复制到另一个Survivor区间。然后清理刚刚所有的Edge和Survivor区间的对象。当一个survivor中不能存放所有的存活对象时候,这些对象会直接通过担保机制进入到老年代当一个新生对象经过15次的垃圾回收后还存活,就将对象移动到老年代。

标记-整理算法

标记复制算法在对象存活率较高时候要进行过多的复制操作,效率比较低。不想浪费50%的空间,还需要额外的空间担保分配以应对内存中100%的对象都存活的极端条件。所以在老年代中不使用这个方法,而是用标记整理算法。因为老年代的对象存活率较高
4911FB05-3FCD-456B-A9DE-1B871B2B1BE5
将对象标记,完成标记后将所有的存活对象都往一端移动,然后清理边界以外的内存。有点:充分利用内存。

分代收集算法

将对象分为新生代和老年代。对新生代采用复制算法。对老年代就采用标记-清理或者标记-整理。
==枚举根节点:==
可作为GC Roots的节点主要是全局性引用((常量池中)常量和(方法区中)类的静态属性变量)和 执行上下文(帧栈中的本地变量表)。但是有些方法区就有数百兆。那么如何快速定位常量、静态变量或者本地变量表。 需要使用OopMap数据结构。记录下栈和寄存器那些位置是引用。这样GC扫描直接可以得到GC Roots 。
安全点:导致OopMap变化的指令很多。那么就要就要设定在特定的地方记录OopMap。“让程序长时间执行的特征”设立安全点。比如方法调用、异常跳转、循环跳转等。产生safepoint。
线程安全: 在gc调用时,要暂停全部线程(除了虚拟机调用的线程)。那么有两种方案。

  1. 抢先式中断 如果发生gc,就把所有的线程都中断。
  2. 主动式中断 如果gc需要中断,就设定标志,各线程去轮训这个标志.

线程休眠:当线程休眠,无法响应JVM的中断请求。就进入”安全区域”。对安全区域的内容不用线程中断。直接清理。当线程开始后,要等清理完毕后才能开始工作。

典型的垃圾收集器:

  1. serial/serial old 收集器
    单线程收集器进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
  2. ParNew收集器
    多线程版本。
  3. CMS收集器
    一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是标记-清除算法。分为以下4个步骤1.初始标记 //暂停所有线程. 2.并发标记 3.重新标记//暂停所有线程 4.并发清除
    产生大量的内存碎片,需要内存紧缩操作,这个过程不能并行。在并发清除时候要和用户线程一起操作,会降低效率。
  4. G1收集器
    G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
    对新生代支持标记复制算法,对老年代支持标记整理算法。同时维护一个可预测时间大小的region域,根据时间要求去清除最优价值的区域。

内存分配和担保策略

java的内存分配就是如何在堆上分配对象,对象主要分配在新生代的Eden区域,如果启动了本地内存分配缓冲,则将按线程有限在TLAB分配,在少数情况下也会分配在老年代。

  1. 对象优先在Eden分配
    小对象在Eden区分配,当Eden区没有足够的空间区域分配就会发起一次Minor GC。
  2. 大对象直接进入老年代
    大对象指需要连续内存空间的JAVA对象,一般是大String或者大数组,比如一个new Integer[1024]的对象就会分陪在老年代。
  3. 长期存活的对象会直接进入老年代
    虚拟机为每个对象分配一个age计数器,对象在Eden区出生并且经过一次minor gc后存活就被移动到survivor区域,并且age+1,每次经过minor gc就会年龄+1,当age增加到一定值(默认15),会被移动到老年代。
  4. 动态对象年龄判断
    在survivor区域的相同年龄的所有对象的内存总和大于survivor空间的一半,年龄大于或者等于这个对象的直接进入老年代,无需默认的15的要求。
  5. 空间担保策略
    在发生Minor GC时,虚拟机都会先检查老年代的最大连续可用空间是否大于新生代的所有对象空间。如果成立,则Minor GC是安全的。如果小于的话,就会先进行一次Full GC。新生代采用复制收集算法,使用一个Survivor空间来做备份,当Minor GC后还出现大量的存活对象,survivor无法存储的对象将直接进入到老年代。前提是老年代有足够的空间容纳这个对象。反正老年代的空间不足就会full gc(我是这么理解的)
0%