Java JVM 内存垃圾回收机制

一、垃圾回收原理

内存结构:

  • 方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字
  • 节码)等数据
  • 堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要
  • 在堆上分配
  • 栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操
  • 作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所
  • 以还是一个指向地址的指针
  • 本地方法栈:主要为 Native 方法服务
  • 程序计数器:记录当前线程执行的行号

分类:

  • 新生代 GC(Minor GC):其中包括Eden、Survival from to
    • Eden区:Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。
    • Survival from to:Survival区与Eden区相同都在Java堆的年轻代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。
  • 老年代 GC(Full GC):年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。
    • System.gc()强制执行的GC为Full GC

minorGC过程详解

  1. 在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。
  2. 当Eden区满了的时候,minor garbage 被触发 。
  3. 经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁
  4. 在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。当Eden 和 s0区空间满了,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象。
  5. 再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。
  6. 再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold默认是15),就会被从年轻代Promotion到老年代。
  7. 随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。

引用类型:

  • 强引用:发生 gc 的时候不会被回收。
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

java 类加载过程:

加载

查找并加载类的二进制数据(把 class 文件里面的信息加载到内存里面)

验证

把内存中类的二进制数据合并到虚拟机的运行时环境中

验证的目的是为了确保 Class 文件的字节流中的信息不会危害到虚拟机. 在该阶段主要完成,

以下四钟验证:

  • 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
  • 元数据验证: 对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
  • 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
  • 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

public static int value=123;// 在准备阶段 value 初始值为 0 。在初始化阶段才会变为 123 。

解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM 把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)

初始化

为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用 static 代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用 static 初始化。

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。

类加载器

虚拟机将加载动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为“类加载器”。

系统提供的3种类加载器

  1. 启动类加载器(Bootstrap ClassLoader):负责将存放在\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)
  2. 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器。

双亲委派

双亲委派原则的用处:

  1. 避免重复加载同一个类;
  2. 防止用户任意修改java中的类;

双亲委派:

如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一层次的类加载器都是这样,因此所有的加载请求最终都应该传送到底层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试去加载。


二、判断对象是否可以被回收

引用计数器法:

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。

可达性分析算法(主流):

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

GC Roots

  • JavaStack中的引用的对象。
  • 方法区中静态引用指向的对象。
  • 方法区中常量引用指向的对象。
  • Native方法中JNI引用的对象。

三、垃圾回收算法

1.标记-清除算法:

说明:标记无用对象,然后进行清除回收。
该算法分为两个阶段,标记和清除。

  • 标记阶段标记所有需要回收的对象
  • 清除阶段回收被标记的对象所占用的空间。

缺点:效率不高,无法清除垃圾碎片。
问题:该算法最大的问题就是内存碎片严重化,后续可能发生对象不能找到利用空间的问题。

2.复制算法:

说明:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。

缺点:内存使用率不高,只有原来的一半。

3.标记-整理算法:

说明:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

4.分代算法:

说明:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代。

四、垃圾收集器


Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器


回收新生代收集器

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • PraNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代默认 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

回收老年代收集器

  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法):老年代默认, 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

五、垃圾收集器实践

1、虚拟机默认参数

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1


-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断

2、基础调参

  • 串行(Serial)收集器
    • -XX:+UseSerialGC(新生代和老年代都使用串行回收器)
    • -XX:+UseSerialOldGC
    • 说明:只能有一个垃圾回收线程执行,用户线程暂停。 适用于内存比较小的嵌入式设备(100M 左右)。
  • 并行(Parallel)收集器[吞吐量优先]
    • -XX:+UseParallelGC(新生代使用并行回收收集器,老年代使用串行收集器)
    • -XX:+UseParallelOldGC(新生代,老年代都使用并行回收收集器)
    • -XX: +UseParNewGC(新生代使用并行收集器,老年代使用串行回收收集器)
    • -XX: +UseConcMarkSweepGC(新生代使用并行收集器,老年代使用CMS)
    • -XX: +UseG1GC(G1收集器)
    • 说明:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 适用于科学计算、后台处理等弱交互场景 。
    • 其他:
      • 使用-XX:ParallelGCThreads= 设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
      • 最大垃圾回收暂停: 指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis= 指定。为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。
      • 吞吐量: 吞吐量为垃圾回收时间与非垃圾回收时间的比值 ,通过-XX:GCTimeRatio= 来设定,公式为1/(1+N) 。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。
  • 并发(Concurrent)收集器[停顿时间优先]
    • -XX:+UseConcMarkSweepGC(新生代使用并行收集器,老年代使用CMS)
    • -XX:+UseG1GC(G1收集器)
    • 说明:用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。 适用于相对时间有要求的场景,比如Web 。

你可能感兴趣的:(架构,jvm,java,算法)