自动垃圾收集是查看堆内存的过程,可以识别哪些对象正在使用,哪些不是,以及删除未使用的对象。一个正在使用的对象或一个被引用的对象,意味着你的程序的某个部分仍然保持着一个指向这个对象的指针。未使用的对象或未引用的对象不再被程序的任何部分引用。所以未被引用的对象所使用的内存可以被回收。
在像C这样的编程语言中,分配和释放内存是一个手动过程。在Java中,释放内存的过程由垃圾收集器自动处理。基本过程可以描述如下。
这个过程的第一步就是标记。这是垃圾收集器标记内存中哪些对象正在被使用,哪些对象已经没有被使用。
有用的对象显示为蓝色,没有用的对象显示为黄色。在标记阶段扫描所有对象,然后做出这个决定。如果必须扫描系统中的所有对象,这可能是非常耗时的过程。
内存维护着一个空闲内存列表,每次分配空间时,会来这个列表上找到合适的空间分配。正常删除时,会把没有用到的对象的内存空间还给空闲列表。
为了进一步提高性能,除了删除未引用的对象之外,还可以压缩剩余的引用对象。 通过移动被引用的对象,这使得新的内存分配变得更容易和更快。
如前所述,标记和压缩JVM中的所有对象效率不高。 随着越来越多的对象被分配,对象列表的增长和增长导致更长和更长的垃圾收集时间。 然而,应用程序的实证分析表明,大多数对象是短暂的。
这里给个数据的例子。
正如你所看到的,随着时间的推移对象保持存活的越来越少。 实际上,大多数对象的寿命都很短,如图左侧较高的值所示。
根据上面的对象的行为特性,我们可以总结出一个更好的方式来提高JVM垃圾回收的效率。所以,就把堆内存分成几种代, 新生代 , 老年代 , 永久代 (Java8之后就没有永久代了,取而代之的是元数据Metaspace)。
一个新的对象会被分配在 新生代 上,并且新的对象会在新生代里慢慢变老。当新生代的空间被占满后,就会触发一次minor gc。假设新生代里的对象死亡率很高的话,那么新生代的垃圾回收就是很优的。一个充满死亡对象的新生代收集起来其实很快。幸存下来的对象会慢慢变老,直到可以移入老年代。
Stop the World Event——所有的新生代手机都是停止世界的事件。Stop the World Event的意思是,所有的应用程序的线程都会被暂停,直到垃圾回收完成。新生代GC总是Stop the World。
老年代是存放那些经历了多次minor gc,年纪达到一个阈值之后的存活的对象。一般来说,会给对象设置一个年龄阈值,达到阈值之后,就会移入老年代。最后,老年代需要进行垃圾回收,就会触发一次major gc。
Major gc也是导致Stop the World。在大部分情况下,major gc是会比minor gc慢很多。所以,对于一个关注响应时间的应用来说,应该尽可能的降低major gc的次数。这里也要注意到,major gc的停顿时间(Stop the World的时间)是和你选取的垃圾收集器有关的。
永久代包含了JVM所需要的class和method的定义等元数据。永久代会随着JVM运行时加载的class而填充新的元数据。除此之外,Java SE的类库也会被存储在这里。
如果JVM检测到这部分class不会被使用了,而且需要更多的内存空间来加载其他的class,那么class也会被回收(unloaded/卸载)。这个收集包含在一次full gc中。(即便是在Java8之后,没有了所谓了永久代,取而代之的是元数据,但是,也会存在类型卸载的回收)
现在你已经明白了为什么需要把堆细分成不同的几个代,现在是时候仔细的看看这种空间是如何工作的了。下面的图演示了在JVM中,对象的分配和变老的过程。
1.首先,任何对象都会被分配在eden区。两个suvivor区一开始都是空的。
这里是为了给读者介绍垃圾回收器的设计过程,和一步步的思考过程,在之后还是会有很多优化,可能会和一开始的设计意图相违背,请见谅。比如,有的对象甚至不分配到堆里(逃逸分析),有的大对象甚至会直接分配到old区(大对象分配),有的对象甚至会分配到堆外内存(nio等),等等各种特殊情况。
2.当eden满了之后,就会触发一次minor gc。
3.活着的对象会被移到第一个suvovor区(第一个第二个都是相对的)。没有被是用的对象就直接被清除了。
4.下一次minor gc发生时,同样的操作。没有被使用的对象被清除,活着的对象和被移到另一个suvivor区。而且,这些对象年龄会+1,然后被移入第二个suvivor。所有的活着的对象都被移入这个新的suvivor1,那么eden和suvivor0又都空了。但是,现在在suvivor1中,对象的年龄是不一样的。
5.下一次minor gc,又会重复上面的步骤。不过对象是从eden和suvicor1移入到suvivor0中了。
6.终于,随便不断的minor gc,对象的年龄越来越大,达到了阈值(这里是8)时,他们会晋升带老年代。
7.随着更多的minor gc,也有更多的对象晋升到老年代。
8.上面已经涵盖了新生代的整个过程。最后,老年代需要进行一次major gc来清除,压缩老年代的空间。
上面说了那么多,相信机智的读者已经大致了解垃圾回收的过程了。现在让我们亲眼看一看这个执行过程。这一部分,我们会运行一个Java应用程序,然后使用Visual VM分析回收的过程。Visual VM是JDK提供给我们的一个工具,开发者可以使用这个工具对JVM进行各个方面的监视。
确保你的电脑已经安装了JDK,并下载了上一步说的demo。然后解压到本地一个目录下。我的目录是 /Users/teeyoung/Desktop/code4me/javademos8
。
然后执行Java2demo.jar, java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar
注意:1.这些命令稍后会解释;2.-XX:PermSize=20m -XX:MaxPermSize=20m如果是在Java 8之后是提示无效,以为已经被移除了。
程序运行起来是这个样子:
你可以看到很多tab,那些演示了Java的绘图功能(看到这个程序,让我想到了买家秀和卖家秀,别人写的代码和我写的代码)。
随意点击各个tab,大致是这样的:
这个界面可以看到垃圾回收行为的结果,看右下角的内存监视。我们先让他运行着,我们稍后会用到它的。
如果你的jdk/bin已经在你的path下了,那么直接执行 jvisualvm
,否则需要输入完整的路径,如: /usr/bin/jvisualvm
。
我们需要安装Visual GC这个插件,但是java.net这个站点都关了,无法联网安装,所以我写了另一篇文章演示如何安装插件。请移步 jvisualvm插件安装的正确姿势(解决网络问题):http://www.dubby.cn/detail.html?id=9061 。
安装之后就是这个样子:
首先双击Java2Demo这个本地进程,或者右击->Open:
然后点击 Visual GC 这个tab:
然后就自由尝试各个tab页,看看每个信息代表JVM的什么指标。还有,你可以尝试着改变Java2Demo上的string和image的数量,看看对垃圾回收有什么影响。
现在你知道了垃圾回收的基本概念,还有如何去监视JVM的垃圾回收。现在我们来了解Java给我们提供的不同的垃圾回收器,还有我们需要掌握如何使用这些垃圾回收器的命令行。
这里给出一些通用的命令行,不管你是什么收集器,都会用到的。
选项 | 描述 |
---|---|
-Xms | 设置JVM启动时,堆的初始大小 |
-Xmx | 设置堆的最大的容量 |
-Xmn | 设置新生代的容量 |
-XX:PermSize | 设置永久代的初始大小( Java 8以废弃 ) |
-XX:MaxPermSize | 设置永久代的最大容量( Java 8以废弃 ) |
-XX:MinHeapFreeRatio | 设置堆最小空闲容量,低于这个阈值就扩容,但是堆总量还是要在Xmx和Xms之间 |
-XX:MaxHeapFreeRatio | 设置堆最大空闲容量,高于这个阈值就收缩,但是堆总量还是要在Xmx和Xms之间 |
Serial收集器是客户端默认的收集器。使用Serial收集器,minor gc和major gc都是单线程处理。而且,老年代使用并发-压缩算法。把老年代的活着的对象移到老年代的前面,后面空出空闲区域,以供后续分配,可以避免空间碎片。
Serial收集器是一些客户端(PC,不是服务器)应用使用,而且对于低延时要求不高的。他的优势是单线程处理。直到今天,对于一些不是很重要,堆内存只有几百MB的应用来说,Serial GC依然是个很有效的垃圾收集器。
还有一个广泛使用Serial收集器的场景是,一个机器上运行着很多JVM(在某些场景下,JVM的数量比处理器的核数还要多)。在这种情况,使用Serial收集器可以减少JVM之间冲突,即便GC的时间变长了。
最后,随着嵌入式设备的普及,内存少,核数少,Serial收集器可能会重新绽放光彩。
开始Serial收集器:
-XX:+UseSerialGC 复制代码
给个完整的例子:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2demo.jar 复制代码
Parallel收集器在回收新生代时,使用多线程进行收集。默认的回收线程数等于机器的核数。可以使用 -XX:ParallelGCThreads=<desired number>
来设置希望的线程数。
在只有一个CPU的机器上,即便你已经开启了Parallel收集器,JVM还是会使用默认的收集器来工作。
Parallel收集器也被称为 吞吐收集器 。因为他可以利用多线程来加快应用的吞吐。这个收集器一般被用作有很多工作需要做,而且对低延要求时不那么高的应用。例如,批处理(报表,账单,或者是很大的数据库查询等)。
这个命令行选项是开启新生代的多线程收集,老年代的多线程收集。老年代也是整理方式。
给个完整的例子:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelGC -jar Java2demo.jar 复制代码
这个选项是开启新生代的多线程收集,老年代的多线程收集。老年代也是整理方式。
整理:就是会把活着的对象移到内存的前面,这样就对象和对象之间的小的空闲的空间(内存碎片)。内存碎片可能导致,空闲空间足够,但是大对象无法分配的情况。
完整的例子:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelOldGC -jar Java2demo.jar 复制代码
并发(Concurrent)标记(Mark)清除(Sweep)收集器(CMS)(也被叫做:并发低延时收集器)是一个手机老年代的垃圾收集器。他试图把大部分垃圾收集工作和应用程序的线程并发执行,以降低所造成的停顿(Stop the World)时间。通常情况下,CMS不会压缩整理活着对象。所以,会存在内存碎片的问题。如果内存碎片成为你的问题,那么可以考虑换用更大的堆(哈哈,也可以考虑换收集器,但是,换更大的堆是直接并且简单的方法)。
注意:CMS收集器在新生代的收集方式和Parallel在新生代的收集方式一样(单线程,复制)。
CMS适用对低延时有高要求的应用。比如,响应事件的桌面应用,响应请求的Web服务器,或者响应查询的数据库。
开启命令:
-XX:+UseConcMarkSweepGC 复制代码
设置线程数:
-XX:ParallelCMSThreads=<n> 复制代码
这里给个完整的例子:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=2 -jar Java2demo.jar 复制代码
具体的可以查看 【浅度渣文】JVM——G1收集器:http://www.dubby.cn/detail.html?id=9059
这里简单描述一下吧,G1在Java 7才出现的,是一个并发的,低延时的,整理收集器。对堆内存管理和之前的收集器都不一样。