天天看点

JVM内存分区与垃圾回收算法

1.概述

对于从事c/c++程序开发的开发人员来说,他们拥有内存管理的直接权利,所有内存空间的管理都交给程序员手动实现。

对于java程序员来说不再需要直接参与内存的管理,这些工作都由jvm帮我们实现。这样不容易出现“内存泄漏”等问题。这一切看起来很美好,如果不了解虚拟机怎么使用内存的,一旦遇到内存泄漏和溢出方面的问题将无从下手。

2.JVM运行时内存分区

java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干不同的区域,这些区域有不同的用途与特性。根据java虚拟机规范,java所管理的内存包括以下几个运行时数据区。

JVM内存分区与垃圾回收算法

1.程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 程序计数器处于线程独占区。
  • 如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined。
  • 此区域是唯一没有规定OutOfMemoryError情况的区域。

2.虚拟机栈

  • java虚拟机栈是线程私有的,它的生命周期与线程相同。
  • 虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候会创建一个栈帧用于存储局部变量表,操作数,动态链接,方法出口等信息。每一个方法从调用到执行完毕就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError错误。
  • 如果虚拟机栈不能申请到足够的内存,就会抛出OutOfMemoryError错误。

3.本地方法栈

  • 本地方法栈为虚拟机执行native方法服务。
  • 本地方法栈的工作机制基本与java虚拟机栈一致。

4.堆

  • 堆是java虚拟机管理的最大一块内存区域,它被所有的线程共享。
  • 此区域的作用用来存储对象实例,同时也是垃圾收集的工作区域。
  • 如果在堆中没有完成内存分配,并且堆无法扩展的时候将会抛出OutOfMemoryError错误。

5.方法区

  • 与堆一样也被各个线程共享。
  • 用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError错误。

6.运行时常量池

  • 运行时常量池是方法区的一部分,Class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请内存时会抛出OutOfMemoryError错误。

7.直接内存

  • 直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。
  • 直接内存实质是jvm通过native方法向操作系统申请的内存区域,其大小会受到本机物理内存大小以及操作系统的限制。
  • 同样存在OutOfMemoryError错误。

java 中对象的回收是由jvm自动完成的,其工作区域是堆。一个通用的垃圾回收器应该需要考虑以下两个问题:

  1. 对象存活的判定,即什么样的对象需要被回收。
  2. 回收算法,即怎么回收。
2.对象存活判定算法

a.引用计数法

  • 引用计数算法基本原理:

    给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数值减1;任何时刻计数器为0的对象是不可能再被使用的。

    JVM内存分区与垃圾回收算法
  • 引用计数计数算法的缺陷
class A {
    public Object b;
}

class B{
    public Object a;
}


public class Reference {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();

        a.b = b;
        b.a = a;
        
        a = null;
        b = null;
    }
}
           

虽然a和b都被赋值为null,但是由于a和b存在循环引用,对象a和对象b的引用计数为1,这样a和b永远都不会被回收。

JVM内存分区与垃圾回收算法

b.可达性分析

  • 可达性分析算法基本原理:

    通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象不可用。

    JVM内存分区与垃圾回收算法
  • java语言中的GC Roots:

    1.在虚拟机栈中的引用对象。

    2.在方法区中的类静态属性引用的对象。

    3.在方法区中的常量引用的对象。

    4.在本地方法栈中JNI(native方法)的引用的对象。

3.常见垃圾回收算法

a.标记清除(Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间,过程如下图所示:

JVM内存分区与垃圾回收算法

优缺点:

  • 标记和清除的效率不够高。
  • 由于垃圾对象的不连续性容易产生内存碎片。

b.标记整理(Mark-Compact)

标记整理与标记清除算法步骤一样,只是后续不是直接清除垃圾对象,而是将所有存活的对象向一端移动,然后直接清理边界以外的内存。过程如下图所示:

JVM内存分区与垃圾回收算法

优缺点:

  • 解决了内存碎片的问题。
  • 由于需要将存活对象进行整理,因此效率较低。

c.复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。过程如下图所示:

JVM内存分区与垃圾回收算法

优缺点:

  • 回收垃圾时简单高效。
  • 对内存空间要求高,实际有效内存只使用了一半。

d.分代算法

分代算法将堆空间划分为几块,一般分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。新生代中只有少量对象存活因此适合复制算法,老年代中对象存活率高,适合使用标记清除或标记整理算法。

JVM内存分区与垃圾回收算法

优缺点:

  • 通过对堆内存进行分代划分,每个分代区域使用与之特性匹配的回收算法,很好的解决了单一算法很难满足所有场景的问题。
  • 对大内存场景支持不够好。

e.分区算法

一般来说,堆空间越大,一次GC所需要的时间就越长。分区算法将整个堆空间划分为连续的不同小区域,每个小区独立使用,独立回收。

JVM内存分区与垃圾回收算法

优缺点:

  • 适用于大内存,多CPU的物理环境。
4.JVM常见引用类型

a.强引用

特点:

1.强引用可以直接访问目标对象。

2.强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会强行回收引用所指向的对象。

public class StrongRef {
    public static void main(String[] args) {
        String str1 = new String("hello world");
        String str2 = str1;
        str1 = null;
        
        System.out.println("GC前: " + str2);
        System.gc();
        System.out.println("GC后: " + str2);
    }
}
           

运行结果:

GC前: hello world
GC后: hello world
           

b.软应用

特点:

当内存资源不足时,软引用对象会被回收,因此软引用不会导致OOM。

import java.lang.ref.SoftReference;

class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class SoftRef {
    public static void main(String[] args) {
        
        StringBuilder builder = new StringBuilder();
        for (int i = 0 ;i < 1024 * 10; ++i) {
            builder.append("hello");
        }
        User user = new User(builder.toString(), 1);
        SoftReference<User> stringSoftReference = new SoftReference<>(user);
        user = null;

        System.out.println(stringSoftReference.get());
        System.gc();

        System.out.println("First GC:");
        System.out.println(stringSoftReference.get());

        byte[] b = new byte[1024  * 400];
        System.gc();
        System.out.println("Second GC:");
        System.out.println(stringSoftReference.get());
    }
}
           

设置运行参数-Xmx1M

运行结果:

reference.[email protected]
First GC:
[email protected]
Second GC:
null
           

c.弱引用

特点:只要发生GC,无论内存资源怎样,弱引用对象都会被回收。

public class WeakRef {
    public static void main(String[] args) {
        String str = new String("hello world");
        java.lang.ref.WeakReference<String> ref = new java.lang.ref.WeakReference<String>(str);
        str = null;
        System.out.println("GC前: " + ref.get());
        System.gc();
        System.out.println("GC后: " + ref.get());
    }
}
           

运行结果:

GC前: hello world
GC后: null
           

d.幽灵引用

特点:一个持有幽灵引用的对象和没有引用几乎一样,随时都可能被垃圾回收器回收。

继续阅读