JVM垃圾回收机制

瘦欲@ 提交于 2020-02-09 03:21:30

转自:http://www.importnew.com/1551.html

每个Java程序员迟早都会碰到下面这个错误:

  • java.lang.OutOfMemoryError

这个时候一般会建议采用如下方式解决这个错误:

  • 增加MaxPermSize值
  • 增加最大堆内存到512M(-xmx参数)

这篇文章会具体介绍Java堆空间和参数MaxPermSize的含义。这篇文章涉及下列主题,并采用Hotspot JVM:

  • 垃圾回收器(Garbage Collector,GC)
  • 哪个JVM?
  • JVM命令行选项

 

垃圾回收器

垃圾回收器负责:

  • 分配内存
  • 保证所有正在被引用的对象还存在于内存中
  • 回收执行代码已经不再引用的对象所占的内存

 

应用执行时,定位和回收垃圾对象的过程会占用总执行时间的将近25%,这会拖累应用的执行效率。

Hotspot VM提供的垃圾回收器是一个分代垃圾回收器(Generational GC)[9,16,18]-将内存划分为不同的阶段,也就是说,不同的生命周期的对象放置在不同的地址池中。这样的设计是基于弱年代假设(Weak Generational Hypothesis):

1.越早分配的对象越容易失效;

2.老对象很少会引用新对象。

 

这种分代方式可以减少垃圾回收的停顿时间以及大范围对象的回收成本。Hotspot VM将其堆空间分为三个分代空间:

1. 年轻代Young Generation

○     Java应用在分配Java对象时,这些对象会被分配到年轻代堆空间中去

○     这个空间大多是小对象并且会被频繁回收

○     由于年轻代堆空间的垃圾回收会很频繁,因此其垃圾回收算法会更加重视回收效率

2. 年老代Old Generationn

○     年轻代堆空间的长期存活对象会转移到(也许是永久性转移)年老代堆空间

○     这个堆空间通常比年轻代的堆空间大,并且其空间增长速度较缓

○     由于大部分JVM堆空间都分配给了年老代,因此其垃圾回收算法需要更节省空间,此算法需要能够处理低垃圾密度的堆空间

3. 持久代Permanent Generation

○     存放VM和Java类的元数据(metadata),以及interned字符串和类的静态变量

 

次收集(Minor GC)和全收集(Full GC

当这三个分代的堆空间比较紧张或者没有足够的空间来为新到的请求分配的时候,垃圾回收机制就会起作用。有两种类型的垃圾回收方式:次收集和全收集。当年轻代堆空间满了的时候,会触发次收集将还存活的对象移到年老代堆空间。当年老代堆空间满了的时候,会触发一个覆盖全范围的对象堆的全收集。

 

次收集

  • 当年轻代堆空间紧张时会被触发
  • 相对于全收集而言,收集间隔较短

全收集

  • 当老年代或者持久代堆空间满了,会触发全收集操作
  • 可以使用System.gc()方法来显式的启动全收集
  • 全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。不过,如果全收集时间超过3到5秒钟,那就太长了[1]

 

全收集通常时间最长,并且是程序无法延迟执行或者无法达到吞吐量目标的主因。GC的目标是去减少程序运行过程中垃圾回收的频率。为了达到这个目的,可以从这两方面入手:

  • 从系统方面考虑:

○    尽量采用大堆,但是不要大到需要系统从磁盘上“换”页。一般而言,可用的RAM(没有被系统进程占用的)的80%都应该分配给JVM。

○    Java堆空间越大,垃圾回收器和java应用在吞吐量(throughput)和延迟执行(latency)方面的效果越好。

  • 从应用方面考虑:

○    减少对象分配(object allocations)操作,或者采用对象保留(object retention)方式有助于减小存活的数据大小,这也可以反过来帮助垃圾回收做的更好。

○    参考这篇文章—Java性能提升窍门[19]

 

一、jvm堆内存的分代划分

在基于分代的内存回收策略中,堆空间通常都被划分为3个代,年轻代,年老代(或者tenured代),永久代。在年轻代中又被划分了三个小的区域,分别为:Eden(伊甸)区,S0区(survivor 0),S1区(survivor 1),如下图所示:

 

其中,新的对象总被分配到年经代中,当年轻代空间被填满时,这时需要执行一次垃圾回收,即执行 minor GC,回收不再被引用的对象,并同时提升幸存的对象其年龄,年经代中的幸存对象都有年龄标识字段,一旦其达到一定的阈值,则仍然幸存的对象将被提升到老年代空间中。

老年代的空间用于存放长时间幸存的对象,即生命周期较长的对象,一旦年轻代空间的幸存对象达到一定的年龄阈值后,将被自动提升到年老代,当年老代空间被对象填满时,这时执行一次Major GC。相较于minor GC, Major GC的执行次数要比minor GC要少很多,同时,Major Gc 执行的时间较Minor Gc要长。因为其涉及到更多的对象扫描。这种分代的思想,也是基于在实践中,对于新分配的对象具有更短的生命周期,年老的对象具有更长的生命周期所作出的较佳的选择。

与此同时,Minor Gc 和 Major Gc 在执行垃圾收集时,采取的是stop the world event ,即终止正在运行的线程,等GC执行完毕在恢复所有的线程。

对于永久代的内存,主要是用来存放元数据的相关信息,类及其方法的信息。当一个类不再使用时将会被回收,当执行Full GC时,将会扫描永久代内存,对其进行垃圾回收。

 

二、基于分代的垃圾回收的处理过程


首先,初始时,新对象被分配到Eden区域,s0,s1为空。当Eden中的空间被填满时,执行一次Minor GC。垃圾收集器会将被引用的对象移动s0区,不再被引用的对象将被删除,与此同时,对于幸存的对象标识其年龄为1. GC后,Eden和S1区为空如下图所示。

 

下一次执行Minor GC后,与先前的执行步骤相同,唯一的区别时,这次的被引用的对象,即幸存下来的对象将会被移动到S1区,与此同时在s0区幸存的对象的年龄会增加1,变成2,如下图所示。

当再次,执行Minor GC后,与先前的步骤相同,幸存对象会被移到S0区,给幸存对象年龄加1.如下图所示

 

最后执行Minor GC 时,发现 S1中的幸存的对象年龄达到8(假设阈值 设为8),此时该对象将被提升到老年代内存中,如下图所示。

Z

 

当老年代堆空间被对象填满时,将会执行一次Major Gc,将会清除老年代不再被引用的对象,与此同时,对该空间执行压缩。如下图所示。

 

内存溢出错误(OutOfMemoryError

可怕的内存溢出错误是Java程序员最不愿意看到的。然而这个错误还是会出现,尤其应用中涉及到大量的数据处理时,或应用运行时间过长时。

一个应用所占内存大小包括:

  • Java堆大小
  • 线程栈
  • I/O缓冲区
  • 原生库所分配的内存

 

当一个应用耗尽了内存并且JVM GC也无法回收任何对象空间的时候,就会发生内存溢出错误。但是,内存溢出错误并不一定就意味着内存泄露(memory leak)。也有可能只是一个配置问题,例如设置的堆大小(如果没有设置那就是缺省的堆大小)对于应用来说是不够用的。

 

JVM命令行参数

无论是客户端应用还是服务器端应用,一旦系统运行缓慢并且垃圾回收所占时间过长,你就会希望通过调整堆大小来改善这一点。不过,为了不影响其他也跑在同一个系统中的应用,不应该将堆大小设置的过大。

GC调优是很重要的。找到最佳的分代堆空间是一个迭代的过程[3,10,12]。这里我们假定你已经为你的应用找到了最佳堆大小。那么你可以采用下面的JVM命令来进行设置:

 

GC 命令行选项 描述
-Xms 设置Java堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”).
-Xmx 设置Java堆大小的最大值
-Xmn 设置年轻代对空间的初始值,最小值和最大值。请注意,年老代堆空间大小是依赖于年轻代堆空间大小的
-XX:PermSize=<n>[g|m|k] 设置持久代堆空间的初始值和最小值
-XX:MaxPermSize=<n>[g|m|k] 设置持久代堆空间的最大值

 

最后一点,最早在Java SE 5.0中有对服务器的人机工程学的介绍[13]。这个可以很好的减少服务器端应用的调优时间,尤其是在堆大小测量和复杂GC调优方面。很多情况下,服务器端调优的最好方式就是不去调优。

 

 

程序员们都知道JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样、不一样!(怎么不一样说的朗朗上口),这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

Java中的引用你了解多少

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

常用的垃圾收集算法

标记-清除算法

复制算法

标记-整理算法

分代收集算法

常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:

    • Serial收集器(复制算法)
      新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
    • Serial Old收集器(标记-整理算法)
      老年代单线程收集器,Serial收集器的老年代版本。
    • ParNew收集器(停止-复制算法) 
      新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
    • Parallel Scavenge收集器(停止-复制算法)
      并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
    • Parallel Old收集器(停止-复制算法)
      Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
    • CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
      高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

GC是什么时候触发的

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

5.1 Scavenge GC(Minor GC

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

5.2 Full GC

  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC之后Heap的各域分配策略动态变化;

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!