Java虚拟机可以有两种不同方法来区别活动对象和垃圾: 引用计数(Reference Counting)和跟踪(Tracing)。
1. 采用Reference Counting的垃圾回收器
对于采用Reference Counting的垃圾回收器,系统为堆上每一个对象都维护一个计数器,当一个对象被创建并且被引用时,这个计数就被置为1。当有新的变量引用该对象,计数器进行自加运算。当一个引用超出作用范围或者被赋予新值的时候,计数器进行自减运算。引用计数为0的对象,会被作为垃圾回收。当一个对象被回收,该对象所引用的对象的引用计数都会相应减少,因而,一个对象的回收有时会引起其它对象的回收。
Reference Counting方式的垃圾回收器,好处在于可以在很短的时间内运行,不会长时间的中断普通的程序运行,因而在RealTime的系统中应用较为普遍。
Reference Counting方式的垃圾回收器,问题在于无法识别循环引用,比如父类对象还有子类引用的情况,即便父类和子类都已经不再能被访问到(unreachable),引用计数也把它们清除。另外一个问题是引用计数器的加减运算会增加系统的计算开销。
2. 采用Tracing的垃圾回收器
采用Tracing的垃圾回收器,遍历由根节点(root nodes)出发的引用关系图。在遍历过程中遇到的对象,就被标记为活动。标记既可以是对应对象中的某一个标志,也可以是独立的位图中的标志。当遍历完成以后,那些没有被标记的对象,就被作为垃圾回收了。有以下几种算法:
标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除,不动被标记的对象。此算法需要暂停整个应用,同时,会产生内存碎片。
复制(Copying):
此算法把内存空间划分为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。此算法每次处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现碎片问题。当然,缺点也很明显,需要两倍的内存空间。
标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象,并把存活对象“压缩”到堆的其中一块,按顺序排放。该算法即解决了碎片问题,又解决了空间问题。
3.以上是从基本的回收原则来分的,按分区对待的方式分:
增量收集(Incremental Collectiing):实时垃圾回收算法,增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾,也可理解为把堆栈分成一小块一小块,每次仅对某一个块进行垃圾收集。这会造成较小的应用程序中断时间,使得用户一般不能觉察到垃圾收集器正在工作。可以理解成,应用程序和垃圾回收是同时进行的。JDK5.0中并未使用这种算法。
分代收集(Generational Collectiing):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用不同的算法进行回收。对于年轻代,研究表明大部分对象都是朝生暮死,随生随灭的。所以对于年轻代在GC时都采取复制收集算法;因为年老代的对象都没那么容易死掉,采用复制算法就要反复的复制对象,很不合算,所以对于年老代在GC时采用标记清理/标记整理算法。
a. Young(年轻代)
年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor复制过来的对象。而且,Survivor区总有一个是空的。
b. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
c. Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
GC的类型:
当每个代满了之后都会自动促发collection,各收集器触发的条件不一样,当然也可以通过一些参数进行强制设定。主要分为两种类型:
Minor Collection:GC用较高的频率对young进行扫描和回收,也叫Young GC。
Major Collection:同时对Young和Old进行内存收集,回收频率要比Young GC低很多,也叫Full GC。
更为具体的阐述如下:
由于年轻代进进出出的人多而频繁,所以年轻代的GC也就频繁一点,但涉及范围也就年轻代这点弹丸之地内的对象,其特点就是少量,多次,但快速,称之为Minor Collection。当年轻代的内存使用达到一定的阀值时,Minor Collection就被触发,Eden及某一Survior space(from space)之内存活的的对象被移到另一个空的Survior space(to space)中,然后from space和to space角色对调。当一个对象在两个survivor space之间移动过一定次数(达到预设的阀值)时,它就足够old了,够资格呆在年老代了。当然,如果survivor space比较小不足以容下所有live objects时,部分live objects也会直接晋升到年老代。
Survior spaces可以看作是Eden和年老代之间的缓冲,通过该缓冲可以检验一个对象生命周期是否足够的长,因为某些对象虽然逃过了一次Minor Collection,并不能说明其生命周期足够长,说不定在下一次Minor Collection之前就挂了。这样一定程度上确保了进入年老代的对象是货真价实的,减少了年老代空间使用的增长速度,也就降低年老代GC的频率。
当年老代或者永久代的内存使用达到一定阀值时,一次基于所有代的GC就触发了,其特点是涉及范围广(量大),耗费的时间相对较长(较慢),但是频率比较低(次数少),称之为Major Collection(Full Collection)。通常,首先使用针对年轻代的GC算法进行年轻代的GC,然后使用针对年老代的GC算法对年老代和永久代进行GC。
关于GC的参数配置:
Young(Nursery):年轻代
研究表明大部分对象都是朝生暮死,随生随灭的。所以对于年轻代在GC时都采取复制收集算法;
Young的默认值为4M,随堆内存增大,约为1/15,JVM会根据情况动态管理其大小变化。
-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,但实际上young启动时远低于这个比率?如果信不过JVM,也可以用 -Xmn硬性规定其大小,有文档推荐设为Heap总大小的1/4。
-XX:SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32。Survivio大了会浪费,小了的话,会使一些年轻对象潜逃到老人区,引起老人区的不安,但这个参数对性能并不太重要。
Old(Tenured):年老代
年轻代的对象如果能够挺过数次收集,就会进入老人区。
-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移入老人区,CMS中默认为0,熬过第一次GC就转入,可以用-XX:+PrintTenuringDistribution 查看。可以通过调用代码System.gc()引发major collection,使用-XX:+DisableExplicitGC禁止它,或设为CMS并发 -XX:+ExplicitGCInvokesConcurrent。
Permanent:持久代
装载Class信息等基础数据,默认64M,如果是类很多很多的服务程序,需要加大其设置 -XX:MaxPermSize=,否则它满了之后会引起fullgc()或Out of Memory。 注意Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存。一般情况下,持久代是不会进行GC的,除非通过 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进行强制设置。
使用jmap和jstat监控GC的例子:
> jmap -heap 2343
Attaching to process ID 2343, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0-b16
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 2686976 (2.5625MB)
MaxNewSize = -65536 (-0.0625MB)
OldSize = 5439488 (5.1875MB)
NewRatio = 2 (YG,OG 大小比为1:2)
SurvivorRatio = 8
PermSize = 21757952 (20.75MB)
MaxPermSize = 268435456 (256.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 1260060672 (1201.6875MB)
used = 64868288 (61.86322021484375MB)
free = 1195192384 (1139.8242797851562MB)
5.148028935546367% used
From Space:
capacity = 85524480 (81.5625MB)
used = 59457648 (56.70323181152344MB)
free = 26066832 (24.859268188476562MB)
69.52120375359195% used
To Space:
capacity = 85852160 (81.875MB)
used = 0 (0.0MB)
free = 85852160 (81.875MB)
0.0% used
~~~~~~~~~~~~~~~~~~~~~~~~~~这三块为上面所说的YG大小和使用情况
PS Old Generation
capacity = 2291138560 (2185.0MB)
used = 1747845928 (1666.8757705688477MB)
free = 543292632 (518.1242294311523MB)
76.28722062099989% used
~~~~~~~~~~~~~~~~~~~~~~~~~~OG大小和使用情况
PS Perm Generation
capacity = 108265472 (103.25MB)
used = 107650712 (102.6637191772461MB)
free = 614760 (0.5862808227539062MB)
99.43217353728436% used
这台机器简单说YG内存1G,OG内存2G,总内存4G
在这样的配置下,GC运行情况:
> jstat -gcutil -h5 2343 4s 100
S0 S1 E O P YGC YGCT FGC FGCT GCT
79.82 0.00 75.34 78.55 99.44 7646 1221.668 398 2052.993 3274.661
0.00 79.52 0.62 78.63 99.44 7647 1221.782 398 2052.993 3274.775 这里发生了一次YG GC,也就是MinorGC,耗时0.12s
0.00 79.52 28.95 78.63 99.44 7647 1221.782 398 2052.993 3274.775
0.00 79.52 46.34 78.63 99.44 7647 1221.782 398 2052.993 3274.775
同时可以看到总共进行了398次Major GC 总耗时2052.993 所以每次Major GC时间为:2052.993/398=5.16秒
这是个很严重的问题,进行Major GC的时候程序会暂停,无法响应,居然会暂停5秒多,这谁都无法接受吧 :)
同样Minor GC进行了7647次,总用时1221.782 平均时间为0.16秒,算是可以接受
再来看看修改配置后:
> jmap -heap 14103
Attaching to process ID 14103, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0-b16
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 536870912 (512.0MB)
MaxNewSize = 536870912 (512.0MB)
OldSize = 5439488 (5.1875MB)
NewRatio = 4 YG:OG 1:4
SurvivorRatio = 8
PermSize = 268435456 (256.0MB)
MaxPermSize = 268435456 (256.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 483196928 (460.8125MB)
used = 428284392 (408.4438247680664MB)
free = 54912536 (52.368675231933594MB)
88.63557841162434% used
Eden Space:
capacity = 429522944 (409.625MB)
used = 404788608 (386.0364990234375MB)
free = 24734336 (23.5885009765625MB)
94.24144010337199% used
From Space:
capacity = 53673984 (51.1875MB)
used = 23495784 (22.407325744628906MB)
free = 30178200 (28.780174255371094MB)
43.77499534970238% used
To Space:
capacity = 53673984 (51.1875MB)
used = 0 (0.0MB)
free = 53673984 (51.1875MB)
0.0% used
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~YG 大小和使用状态
concurrent mark-sweep generation:
capacity = 3758096384 (3584.0MB)
used = 1680041600 (1602.2125244140625MB)
free = 2078054784 (1981.7874755859375MB)
44.70459052494594% used
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~OG 大小和使用状态
Perm Generation:
capacity = 268435456 (256.0MB)
used = 128012184 (122.0819320678711MB)
free = 140423272 (133.9180679321289MB)
47.688254714012146% used
在这个配置下,GC运行情况:
>jstat -gcutil -h5 14103 4s 100
S0 S1 E O P YGC YGCT FGC FGCT GCT
47.49 0.00 64.82 46.08 47.69 20822 2058.631 68 22.734 2081.365
0.00 37.91 38.57 46.13 47.69 20823 2058.691 68 22.734 2081.425 这里发生了一次YG GC,也就是MinorGC,耗时0.06s
46.69 0.00 15.19 46.18 47.69 20824 2058.776 68 22.734 2081.510
46.69 0.00 74.59 46.18 47.69 20824 2058.776 68 22.734 2081.510
0.00 40.29 19.95 46.24 47.69 20825 2058.848 68 22.734 2081.582
MajorGC平均时间:22.734/68=0.334秒(上面是5秒多吧)
MinorGC平均时间:2058.691/20823=0.099秒(比上面略少)