Java Virtual Machine和 Garbage Collector初探


在进入正文之前,我想先回答两个问题:

什么是Java Virtual Machine?

JVM(java虚拟机)是java语言的一个重要组件,重要到如果没有JVM的存在,那么java程序就无法正常运行。它代表着Java的运行时环境。我们知道java编译器会将java源码文件(.java文件)编译成.class文件,这并不是本地可执行文件,而是一个特定格式的字节码(bytecode)。它将会由JVM负责加载到内存中,并由JVM控制程序的执行。可见,JVM是底层系统与java程序员间的中间件(middleware)。正是由于它的存在,使得java语言具有一些特性,例如:平台无关性、安全性、write once,run anywhere、内存管理等。

The Java Virtual Machine is an abstract computing machine….Each JVM implementation for a specific operating system,translate the Java programming instructions into instructions and commands that run on the local operating system.

什么是Garbage Collector?

Garbage Collector(GC),垃圾回收器,是JVM的一个重要组成构件。顾名思义,它是JVM内存管理的执行引擎,作用是垃圾(无效的、无引用对象)回收。

下面进入正文,首先介绍JVM的架构。这里以HotSpot JVM为基础进行介绍(HotSpot JVM也是oracle JVM的基础)。下图是oracle官方网站提供的HotSpot架构图。
JVM基本结构图
整体上,HotSpot JVM分成四块,分别是:类加载子系统、运行时数据域、本地方法接口、执行引擎。这里对于类加载子系统、本地方法接口不做解释。我们只需要知道类加载子系统是用来把.class文件加载到JVM中的就可以了,什么双亲委托机制这里不做说明。
通常意义上,我们说要对JVM进行调优(JVM tuning)主要指的是对Heap、JIT编译器(Just in Time)、Garbage Collector这三块进行调优。所以本文将重点从这三个模块出发,一窥JVM和GC调优。

1.JIT Compile

什么是JIT

JIT,全称Just in Time,即即时编译器。正如上文提到的,JVM加载并运行字节码(bytecode),那么当JVM发现某块代码的执行非常频繁时,JVM会使用JIT编译器,将这部分代码翻译成本地代码(native code)从而加速这块代码的执行。通常情况GC调优是不涉及JIT的,由JVM智能的选择JIT参数即可,而我们应该更加关注Heap的组织和GC的选择。

Tips: JIT可以编译的最小单元是方法(method);默认情况下,某块代码被执行的次数达到1500次(Client模式)/10000次(Server模式)时,才会触发JIT行为,当然这个数值是可配置的(-XX:ComplieThreshold=N)。

In English:
JIT,stand for “Just in Time”.As disscued,the JVM loads and excutes bytecode.When JVM finds a section of code is being run frequently,it can optionally use JIT to translate the code into native code for increasing the speed of execution.

2.Heap

什么是Heap?

Heap,堆是JVM存放对象的地方,用户创建的所有对象实例都是存放在堆空间的(与之相对的栈是存放的是变量、方法以及堆中对象的引用)。堆空间的管理是由Garbage Collector(垃圾回收器)进行的,大多数的GC调优是需要涉及到堆空间的管理和组织的。

Heap空间的组织

整体上来看,Heap空间是一块连续的内存区域。由于管理堆的是GC,而不同的GC采用的GC策略会有区别,这会直接影响对Heap空间的组成。举例来说:如果GC采用引用计算法,那么堆空间直接使用,每次有新的对象进行,Memery Allocator直接向堆进行申请即可;如果GC采用分代策略,那么Heap空间会被分成多个代(section)。这里我以分代组织堆空间进行说明,因为这也是目前通用的组织形式。

何为代?答:代就是内存块(section),分代就是指把Heap空间分成多个块。比如通常JVM会把Heap分成三个块,分别是新生代(New Generation)、旧生代(Old Generation)、持久代(Permanent Generation),而新生代又会被分割成三个块,分别是Eden space(伊甸园区)、Survivor Space0(S0)、Survivor Space1(S1)。

下图是oracle官方给的HotSpot Heap分代结构图
HotSpot分代结构图
这里仅对图进行简单的说明,具体的一些操作等会在GC部分详细说明。首先可以看到一个连续的Heap堆空间被分割成了三大块,分别是新生代、旧生代、持久代。新生代用于存放那些生命期短暂的对象实例;旧生代用于存放那些生命期长的对象实例;持久代中存放的是类源信息等。同时,可以看到新生代被分割成了三个子块,分别是:Eden Space、S0、S1,其中S0、S1是两个完全一样的区域,合称幸存者区。

  • Eden Space,伊甸园区。用户新创建的对象实例都是先存放在Eden Space。当Eden Space空间满时,会触发Minor GC(一种小规模的GC)。
  • S0、S1,幸存者区。初始情况下,S0、S1都是空的,在进行Minor GC之后,会让其中一个用来存放幸存下来的对象,而另一个是空的。
  • Tenured区,即旧生代区。当某些对象在多次Minor GC后依然存活时(次数可配置),将会被promoted到旧生代区,所以这个区存放的是生存期较长的老对象。通常情况下,当Tenured区空间占用率达到一定阈值后,会触发Major GC(Full GC),这是一种十分耗时的GC过程。而GC调优的目的一般也是为了减少Full GC的时间。
  • Permanent区。这个区一般在GC tuning时是不做改动的。一般如果某个类不在使用时,可以考虑删除其在Perm区存放的源信息。

3.Garbage Collector

什么是垃圾回收器?

C语言中通过指针可以让我们访问对应的内存区域的值,并对这块区域的值进行修改。当我们在C++中new出一个对象时,就需要我们自己手动进行free,来回收这部分空间。这其实是很危险的行为, 也会容易造成内存泄漏。
Java语言抛弃了指针的概念,同时提供垃圾回收器来帮助我们进行垃圾回收,可以相对有效地避免内存泄漏现象的发生。作为java程序员,不再需要关心对象的回收问题,而更加关注如何解决应用问题本身。而对于创建的对象何时被回收?内存会不会泄漏等?都将由垃圾回收器智能地完成。

何时进行垃圾回收?

一般认为,当保存在Heap中的对象实例不再存在外围引用时,即该对象已经失效了、不再被调用了,GC就会对其进行回收。那是否是立即执行回收呢?很显然,不是。只有达到Minor GC或者Major GC的触发条件,GC才会启动。
也许有人会觉得,可以通过调用System.gc()来强制GC的触发。需要指出这种做法是非常不提倡的,因为就算你这么做了,GC是否会触发还是不确定的。

垃圾回收是如何进行的?

GC的策略有很多,常用的有标记-清除(Mark-Sweep)、标记-压缩(Mark-Sweep-Compact)。它的基本过程如下
1、标记。GC会遍历Heap中所有对象,并对未使用的对象和正在使用对象进行标记。如下图:
标记阶段
2、清除。GC会把标记为未引用的对象进行清除,从而回收他们的空间。并会在内存申请器(Memery Allocator)中维系着一个列表,这个列表保存指向那些空的内存块的引用。如下图:
清除阶段
3、压缩。为了提供性能,我们肯定不希望Heap出现太多的内存碎片(fragment),因为这样对申请新对象会造成很大的困难。所以有些GC在清理了“垃圾”后,还会对Heap空间进行整理,它会把那些“幸存下来”的对象进行压缩。通过Copy的方式,把所有幸存者以紧凑的方式放在堆的一边。这样,内存申请器再次申请空间时只需要顺序进行即可,性能得到了提高。然后,Compact的过程也是一个十分耗时的工作。如果把Compact的过程也放到GC过程中,那么可能会造成应用系统较长的停止时间。如下图:
压缩阶段

在标记、压缩阶段都需要对JVM中的所有对象进行遍历和处理,这是非常低效的过程。可以想象,随着对象越来越多的存放在Heap中,每次GC的时间也会逐步增加,这是不可接受的。目前存在一个假设,weak generational hypothesis,指的是:

  • Most objects soon become unreachable.
  • References from old objects to young objects only exist in small numbers.

下图是oracle官网提供的对象生命周期图。说明下:图中X坐标是随着时间的推移,被申请的字节数,Y坐标是当前被申请的字节数。大致想表达的是,随着时间的迁移,幸存下来的对象会越来越少,而大多数对象的生命周期都是很短暂的。
对象生命周期
这些都有力的告诉我们,对象是根据其生命周期长度是分类别的。大多数对象是短生命周期,少数对象是长生命周期。唯物辩证法告诉我们,具体问题具体分析。这里也一样,对待不同的对象,我们就应该采用不同的GC策略。这就引出了下面的分代垃圾回收。

分代垃圾回收

分代垃圾回收将Heap进行分代,不同代采用不同的策略进行垃圾回收。关于Heap分代,上面已经介绍了,这里不再说明。分代垃圾回收的基本过程是:
1、任何新申请的对象会被放在Eden空间。两个幸存者空间开始都是空的。如下图:
对象申请
2、当Eden Space空间满时,会触发Minor GC。
3、被引用的对象被移动到第一个幸存者区,而那些未被引用的将会被删除。如下图:
拷贝被引用对象
4、当下次Minor GC发生时,会发生相同的事情。Eden空间和存放上次幸存者对象的Survivor区将会被GC进行垃圾回收,而幸存下来的对象会被拷贝到另一个空的幸存者区中。同时上次幸存下来的对象如果在本次GC中也幸存下来了,那么它们的Age值会加1。这样一次GC完成后,幸存下来的对象它们的Age值可能会不一样。如下图:
对象老龄化
5、当再次发生Minor GC时,相同的过程再次发生。如下图:
年龄增加
6、当存放在幸存者区的对象的年龄达到一定的阈值时,该对象会被迁移到Tenured区(老生代)。如下图:
进入老生代
7、伴随着Minor GC的不断发生,老生代中的对象也越来越多,当达到一定阈值后,Major GC(Full GC)将会被触发,从而对老生代中的对象进行清理。如下图:
GC总结

到此,分代垃圾回收的过程已经做了简要的介绍。相信大家对这个过程已经也比较熟悉。下面将会介绍几种常用的垃圾回收器

几种常用的GC

1、串行GC(The Serial GC)

在1.5和1.6版本,串行GC是默认的客户端机器上的垃圾回收器。顾名思义,串行GC使用一个线程进行GC操作。采用的策略是:标记-压缩法。适用于:客户端机器、单CPU机器、资源紧缺且核少的嵌入式机器、同一台机器上JVM的个数>=Core的个数。整体评价:虽然串行GC只用一个线程进行垃圾回收,表面上看起来GC时间是比较长的,但是在长久的实验测试和使用中,串行GC还是有很多可用的情景的,至少它很稳定,对资源的消耗比较少,只是GC比较慢而已嘛。

Tips:就好比推荐系统的方法,虽然学术界已经更新了很多推荐系统算法,但是产业界还是比较倾向于使用协同过滤,这一传统、经典的推荐系统方法。串行GC在很多嵌入式系统以及资源紧张的系统上表现还是很良好的,甚至超越了并行GC。

如何启用串行GC?
命令行:-XX:+UseSerialGC

2、并行GC(The Parallel GC)

相比于串行GC,并行GC唯一的区别就在使用了N个线程并行地进行GC操作。从整体的GC时间来看,并行GC的时间更短,停止时间也更小,被称为是Throughput Collector(吞吐量回收器)。采用的策略是:对Minor GC采用多线程GC,对Major GC采用单线程的串行GC。适用于:批处理环境、大批量的数据库查询、高吞吐量、对停止时间要求不高的环境。整体评价:并行GC通过同时使用N个线程并行进行GC操作,有效地加速了GC过程,但是线程的创建时需要消耗资源的,线程上下文的切换也是需要额外的时间的,所以对额外的资源有一定的要求。如果系统要求吞吐量,而对响应时间没有过多要求,那么并行GC就很适合。
当我们需要同时对新生代和旧生代采用并行GC时,可以使用并行Old GC。它与并行GC的唯一区别是,对旧生代也采用多线程并行GC。

Tips:串行GC和并行GC都是使用“Stop the World”。何为“Stop the World”,简单来说,就是GC在进行垃圾回收时,会暂停除了和GC有关的一切其他线程运行。只有等到GC全部完成,那些线程才会被唤醒。所以在这个过程中,系统是处于无响应的。

如何启动并行GC?
命令行:-XX:+UseParallelGC / -XX:+UseParallelOldGC
并行的线程数也是可以设置的,命令行:-XX:ParallelGCThreads=N

3.CMS GC(Concurrent Mark Sweep GC)并发标记-清除GC

CMS GC相比上上面两种GC回收过程更加的复杂。主要步骤有:初始标记、并发标记、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而并发标记、并发清除和并发重置是可以和应用程序并发一起运行的。所以整体上来说,CMS GC并不是独占式的,因此CMS GC有着低停止时间(low latency GC)。但是由于CMS GC在运行的同时,应用程序线程也在运行,可能会有新的垃圾产生,所以在CMS GC回收垃圾的过程中,还需要保证有足够的内存供其他线程使用。采用的策略是:对新生代采用多线程GC,对旧生代采用CMS GC。触发的条件:当旧生代的空间使用率达到一定阈值触发CMS GC。适用于:那些需要低停止时间的应用,例如:桌面UI应用对事件的响应、网络服务器对一次请求的响应等。

Tips:这里需要注意,CMS GC实际采用的是标记-清除法进行垃圾回收,所以会造成堆空间的碎片化。由于没有自动进行Compact,所以如果要将一个大对象放到旧生代,可能会出现问题,产生“Concurrent Model failure”。一旦出现这个问题,就需要对旧生代进行Compact,而这是个十分耗时的事情。如果把Compact的时间加到CMS GC的时间上,那么GC的总时间就会比上面介绍的几种GC都要长(谁叫你GC的过程那么复杂呢,虽然你机智地把Compact从GC的过程中分离出去,但是出来混,始终是要还的!)。

如何启动CMS GC?
命令行:-XX:+UseConcMarkSweepGC。修改并发线程数:-XX:ParallelCMSThreads=n
设置在CMS GC后进行一次内存碎片整理:-XX:+UseCMSCompactAtFullCollection;
设置在进行多少次CMS GC后进行一次内存压缩:-XX:CMSFullGCsBeforeCompaction
设置旧生代空间使用率达到多少阈值时,触发Full GC:-XX:CMSInitiatingOccupancyFraction

4.G1 GC(Garbage First GC)

G1 GC是Java1.7之后提供的一个垃圾回收器,它的强大在于,它满足了上面几种GC的全部优秀特性,简单来说,就是多线程并行、并发、标记-压缩、低停止时间、高吞吐量。它的结构与上面几种GC也完全不一样,它是一种类似于网格地形式组织Heap空间,这里已经完成抛弃新生代、旧生代的概念了。

如何启动G1 GC?
命令行:-XX:+UseG1GC

到此,已经把几种常用的GC做了简单的介绍,如果想要详细地了解这些GC的实现细节,请自行查阅相关资料。

最后,想简单介绍GC tuning的方法和常用工具。

GC tuning(调优)和常用工具

GC tuning是一个复杂且十分具有经验性的工作。就像机器学习里面,我们需要对超参数进行调优一样,当应用程序的GC不能达到我们的要求时,我们就需要对其进行调优。大多数情况下,GC是不需要tuning的,因为GC的智能程度已经很高了,大多数应用GC都能在合理的时间内完成。然而总是会有特殊情况,在某些情况下,或者在程序员的疏忽下会造成内存泄漏,这时我们就需要对GC进行监控以及进一步的调优。
通常情况下,GC调优只会涉及GC类型的选择和Heap大小的调整,对于JIT我们一般是不做修改的。

GC调优的一般步骤是什么呢?

  1. 监控JVM和GC
  2. 根据监控的结果判定是否需要进行GC调优。如果是转下一步,如果不是,打住。
  3. 选择不同类型的GC;调整Heap大小。对于调整Heap大小,不仅可以调整Heap整体的大小,也可以调整新生代、旧生代的大小等。
  4. 根据选定的参数,监控JVM和GC的变化。这个过程不是一两个小时就可以的,可能会需要更长的时间收集数据。
  5. 如果幸运的话,GC达到要求了,JVM运行时也很正常,没有内存泄漏,那么就结束吧。如果不幸的话,就需要重新从步骤三开始,继续进行调优。

那么如何监控JVM和GC的运行状态呢?
方法就比较多了。主要分成两类:

  • CUI:使用命令行的形式对其进行监控。这里需要使用JDK提供的工具,jstat和verbosegc。前者从整体上查看JVM的运行状态,后者能够记录每一次GC的信息。
  • GUI。可以使用JDK提供VisualVM来对JVM进行监控,如果想要监控GC时,Heap的变化,那么需要安装插件ViusalGC。

当然了,还有什么线程转存、堆转存(dump)等都可以用来JVM的诊断,这里就不多说了。

总结

到此,JVM和GC初探就结束了。本文主要是对JVM的整体架构做了介绍。并着重对Heap、JIT、GC进行了介绍。而这三者中更加侧重了对GC的介绍。GC是Java中非常重要的过程,了解GC的运行原理以及简单GC调优方法对写出优质的Java程序有帮助。希望这篇博文能帮助大家初窥JVM和GC。

oracle官网对JVM、GC的介绍
一个韩国人对JVM、GC的介绍
有着一些JVM、GC的面试题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值