JVM总结之垃圾回收

文章目录

  • 垃圾回收
    • 一、java堆内存的细分
      • 1、分代收集算法
      • 2、新生代
        • 2.1、Eden 区
        • 2.2、Survivor From 区
        • 2.2、Survivor To 区
      • 3、老年代
      • 4、元数据
      • 5、对象分配
      • 6、对象晋级
    • 二、垃圾回收
      • 1、怎么定位垃圾
        • 1.1、引用计数法
        • 1.2、可达性分析
      • 2、怎么回收垃圾
        • 2.1、标记清除算法(Mark-Sweep)
        • 2.2、复制算法(copying)
        • 2.3、标记整理算法(Mark-Compact)
    • 三、垃圾回收器
      • 垃圾回收默认配置及互联网后台推荐配置
    • 四、java四种引用类型
      • 1、强引用
      • 2、软引用
      • 3、弱引用
      • 4、虚引用
  • 常见内存溢出溢出问题
    • 一、堆溢出
      • 1、产生原因
      • 2、解决方法
    • 二、栈溢出
      • 1、产生原因
      • 2、解决办法
    • 三、方法区或元数据区溢出
      • 1、产生原因
      • 2、解决办法
    • 四、本机直接内存溢出
      • 1、产生原因
      • 2、解决办法

垃圾回收

一、java堆内存的细分

1、分代收集算法

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为 新生代(Eden 区、Survivor From 区和 Survivor To 区)和老年代 。。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

2、新生代

新生代是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

2.1、Eden 区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

2.2、Survivor From 区

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

2.2、Survivor To 区

保留了一次 MinorGC 过程中的幸存者。

3、老年代

老年代主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

4、元数据

在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代 。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存 。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中 ,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

5、对象分配

  • 优先在Eden区分配。当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域。如Minor GC时survivor空间不够,对象提前进入老年代,老年代空间不够时进行Full GC;
  • 大对象直接进入老年代,避免在Eden区和Survivor区之间产生大量的内存复制, 此外大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.

6、对象晋级

  • 年龄阈值: VM为每个对象定义了一个对象年龄(Age)计数器, 经第一次Minor GC后仍然存活, 被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.
  • 提前晋升: 动态年龄判定;如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.

二、垃圾回收

1、怎么定位垃圾

在垃圾回收之前,肯定得先确认哪些是垃圾,这里有两种方法: 引用计数法可达性分析 ;其中java虚拟机用的是可达性分析。

1.1、引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

1.2、可达性分析

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

在Java, 可作为GC Roots的对象包括:

  1. 方法区: 类静态属性引用的对象;
  2. 方法区: 常量引用的对象;
  3. 虚拟机栈(本地变量表)中引用的对象.
  4. 本地方法栈JNI(Native方法)中引用的对象。

要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

2、怎么回收垃圾

2.1、标记清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:

缺点:
效率问题: 标记和清除过程的效率都不高。
空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

2.2、复制算法(copying)

为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

优点:

  • 由于是每次都对整个半区进行内存回收,内存分配时不必考虑内存碎片问题。
  • 垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可,实现简单,内存效率高;
  • 特别适合java朝生夕死的对象特点;

缺点:

  • 内存减少为原来的一半,太浪费了;
  • 对象存活率较高的时候就要执行较多的复制操作,效率变低;
  • 如果不使用50%的对分策略,老年代需要考虑的空间担保策略

目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

2.3、标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

优点:

  • 不会损失50%的空间;
  • 垃圾回收后空间连续,只要移动堆顶指针,按顺序分配内存即可;
  • 比较适合有大量存活对象的垃圾回收;

缺点:

  • 标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

老年代因为每次只回收少量对象,因而采用 标记整理算法(Mark-Compact)。

  1. JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况(对象较大)会直接分配到老生代。
  3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
  4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
  5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
  6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中。

三、垃圾回收器

由于新生代和老年代的特点不同,所以会用不同的垃圾回收器去回收,而不同的垃圾回收器用的算法不尽相同,且一般会用一个新生代垃圾回收器配合一个老年代垃圾回收器进行垃圾回收。但是G1回收器比较牛逼,他可以直接回收新生代和老年代,算法也和其他回收器不太一样,会是以后主流的垃圾回收器。不同垃圾回收器可以搭配使用的关系如下:

新生代垃圾回收器:

收集器 收集对象和算法 收集器类型 说明 使用场景
Serial 新生代,复制算法 单线程 进行垃圾收集时,必须暂停所有工作线程,直到完成;(stop the world) 简单高效;适合内存不大的情况;
ParNew 新生代,复制算法 并行的多线程收集器 ParNew垃圾收集器是Serial收集器的多线程版本 搭配CMS垃圾回收器的首选
Parallel Scavenge吞吐量优先收集器 新生代,复制算法 并行的多线程收集器 类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量; 本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不需要太多交互的任务;

注:
吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)
垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

老年代垃圾回收器:

收集器 收集对象和算法 收集器类型 说明 使用场景
Serial Old 老年代,标记整理算法 单线程 jdk7/8默认的老生代垃圾回收器 Client模式下虚拟机使用
Parallel Old 老年代,标记整理算法 并行的多线程收集器 Parallel Scavenge收集器的老年代版本,为了配合Parallel Scavenge的面向吞吐量的特性而开发的对应组合; 在注重吞吐量以及CPU资源敏感的场合采用
CMS 老年代,标记清除算法 并行的多线程收集器 尽可能的缩短垃圾收集时用户线程停止时间;缺点在于:1.内存碎片 2.需要更多cpu资源 3.浮动垃圾问题,需要更大的堆空间 重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。互联网后端目前cms是主流的垃圾回收器;
G1 跨新生代和老年代;标记整理 + 化整为零 并行的多线程收集器 JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势; 面向服务端应用的垃圾回收器,目标为取代CMS

垃圾回收默认配置及互联网后台推荐配置

  • 在JVM的客户端模式(Client)下,JVM默认垃圾收集器是串行垃圾收集器(Serial GC + Serial Old,-XX:+USeSerialGC);
  • 在JVM服务器模式(Server)下默认垃圾收集器是并行垃圾收集器(Parallel Scavaenge +Serial Old,-XX:+UseParallelGC)
  • 而适用于Server模式下
    1. ParNew + CMS + SerialOld(失败担保),-XX:UseConcMarkSweepGC;
    2. Parallel scavenge + Parallel,-XX:UseParallelOldGC

四、java四种引用类型

1、强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

2、软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

3、弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

4、虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

常见内存溢出溢出问题

java内存溢出异常主要有两个:

  1. OutOfMemeoryError:当堆、栈(多线程情况)、方法区、元数据区、直接内存中数据达到最大容量时产生;
  2. StackOverFlowError:如果线程请求的栈深度大于虚拟机锁允许的最大深度,将抛出StackOverFlowError,其本质还是数据达到最大容量;

一、堆溢出

1、产生原因

堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收实例对象,就会在对象数量达到堆最大容量时产生OutOfMemoryError异常。 java.lang.OutOfMemoryError: Java heap space

2、解决方法

使用-XX:+HeapDumpOnOutOfMemoryError可以让java虚拟机在出现内存溢出时产生当前堆内存快照以便进行异常分析,主要分析那些对象占用了内存;也可使用jmap将内存快照导出;一般检查哪些对象占用空间比较大,由此判断代码问题,没有问题的考虑调整堆参数;

二、栈溢出

1、产生原因

  1. 如果线程请求的栈深度大于虚拟机锁允许的最大深度,将抛出StackOverFlowError;
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemeoryError;

2、解决办法

  • StackOverFlowError 一般是函数调用层级过多导致,比如死递归、死循环,避免这种情况的发生;
  • OutOfMemeoryError一般是在多线程环境才会产生,一般用“减少内存的方法”,既减少最大堆和减少栈容量来换取更多的线程支持;

三、方法区或元数据区溢出

1、产生原因

  1. jdk 1.6以前,运行时常量池还是方法区一部分,当常量池满了以后(主要是字符串变量),会抛出OOM异常;
  2. 方法区和元数据区还会用于存放class的相关信息,如:类名、访问修饰符、常量池、方法、静态变量等;当工程中类比较多,而方法区或者元数据区太小,在启动的时候,也容易抛出OOM异常;

2、解决办法

  • jdk 1.7之前,通过-XX:PermSize,-XX:MaxPerSize,调整方法区的大小;
  • jdk 1.8以后,通过-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,调整元数据区的大小;

四、本机直接内存溢出

1、产生原因

jdk本身很少操作直接内存,而直接内存(DirectMemory)导致溢出最大的特征是,Heap Dump文件不会看到明显异常,而程序中直接或者间接的用到了NIO;

2、解决办法

直接内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样)

你可能感兴趣的:(JVM,JVM垃圾回收,java垃圾回收,JVM)