0. JVM内存组成
JVM内存主要由两部分组成:a.线程私有内存区域;b.线程公共内存区域。
线程公用的内存区域主要包括:
- 堆
- 方法区
线程私有的内存区域主要包括:
- jvm栈
- 程序计数器
- 本地方法栈
JVM GC主要就是针对线程公共的内存区域进行的。
1. GC 概念描述
GC,即 garbage collection 是进程查看堆内存,分辨哪些对象还在被使用中,哪些对象已经不再使用,并删除不再使用的对象的过程。“被使用” 指程序中的某些部分任然持有对该对象的引用,而 “不再被使用” 就是指没有任何部分持有对该对象的引用,对于 “不再被使用” 的对象,其消耗的内存可以被回收重用。
与C和C++不同,java的内存回收不再全权由程序员否则,而是交给系统自动进行。java中的GC可以分为下面这些步骤:
步骤 1 标记
标记回收的第一步就是标记出哪些对象已经不再使用了。当对象很多时这一步需要消耗大量时间。
步骤 2a 删除
删除标记出哪些对象不再使用后,下一步自然是将其删除了。但是从上图可以看出,这个方法有一个问题:在内存中形成了不连续的片段,这样可能无法分配一大块空间给某个大对象。
步骤 2b 删除并整理
英文原文叫 Compacting ,个人认为翻译为压缩更符合原文的意思。
删除并整理在删除无用对象后,将还在使用中的内存移动到一个连续的空间内,这样消除了 “内存碎片” 的影响。
步骤 2c 删除并复制
这种方式使用两个内存区域,一个内存区域中需要进行GC后,就将该内存区域中仍然被引用的对象移动到另外一个区域中,不会产生内存碎片,速度也很快,然而缺点是需要使用额外的内存。
分代回收
对象生命周期上图描述了对象生命周期,可以看出绝大多数对象都是 “朝生暮死”,根据对象不同的特性,可以将对象分为几类,分别使用不同的方法进行GC,减少GC消耗的时间。
2. HotSpot 的分代回收
分代回收说明
java8 HotSpot分代上图是 JAVA 8 的 HotSpot的分代策略。注意在 JAVA 8 中 PermGen 被MetaSpace取代了,具体可以参见这篇文章。eden+S0+S1合起来称为 Young Generation,S0+S1 称为Survivor Space,而 Tenured 则是 Old Generation。
年轻代GC对象首先被分配到Eden区域,程序刚开始运行时 from 和 to 都是空的。
minor GC随后,Eden区域被填满,于是触发 minor GC,将有效对象全部移动到 S0 或 S1 中去。
对象 aging随着一次次的 minor GC,存活的对象在S0和S1之间移动来移动去,将会在其对象头部记录经历过的 GC 次数。
promotion到老年代当对象经历的GC次数大于某个值后,将会被认为是一个有较长生命周期的对象,从而将其移动到老年代。
使用VisualVm,加上GC-plugin可以看到垃圾收集的过程,以我电脑上运行的IDEA为例,可以看到survivor0和survivor1交替变为空,而 Old Gen 和 metaspace 在很长一段时间内几乎保持不变。
垃圾收集实例与GC相关的部分JVM参数
Switch | Description |
---|---|
-Xms | jvm堆的初始大小. |
-Xmx | jvm堆的最大大小. |
-Xmn | 年轻代的大小. |
-XX:PermSize | Java1.8之前的永久代大小 |
-XX:MaxPermSize | Java1.8之前的永久代最大大小 |
-XX:MetaspaceSize | MetaspaceSize大小 |
-XX:MaxMetaspaceSize | MetaspaceSize最大大小 |
Serial GC算法
Serial GC 是一种单线程执行的 标记-整理 GC算法,适用于对GC延迟不敏感的客户端java程序。另外,如果很多个jvm运行在同一台机器上时,也适合使用这种算法。
该算法使用如下参数打开:
-XX:+UseSerialGC
一个运行实例:
java -Xmx12m -Xms3m -Xmn1m XX:MetaspaceSize XX:MaxMetaspaceSize=20m
-XX:+UseSerialGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar
Parallel GC
parallel GC 使用多线程进行年轻代收集,是Serial的多线程版本,当主机有N个 CPU 时,默认会使用N个垃圾线程。使用命令行选项可以控制垃圾收集的线程数目:
-XX:ParallelGCThreads=<线程数目>
注意,在单核主机上,是不会使用Parallel GC的,在2核主机上,Parallel GC 可能和默认的 GC 收集器工作效率相同,而在大于2核的主机上,Parallel GC 一般会有更好的表现。当然,一切还是需要以实际测量为准。
Parallel GC适合于需要高吞吐量而对暂停时间不敏感的场合,比如批处理任务。当然,一切还是需要以实际测量结果为准。
启用Parallel GC:
-XX:+UseParallelGC
命令行例子:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m
-XX:+UseParallelGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar
另外,使用-XX:+UseParallelOldGC
命令行选项可以让新生代和老年代同时使用多线程并行垃圾收集。在老年代,ParallelOld是一种 标记-整理 算法,会在标记后将存活对象搬运到一起来防止出现内存碎片。用例如下:
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m
-XX:+UseParallelOldGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar
Concurrent Mark Sweep (CMS)
CMS 是一种希望最小化响应时间的垃圾收集算法,GC的某些步骤可以与应用线程并行的进行,它的收集周期为:
- 初始标记(CMS-initial-mark) :标记 Roots 能直接引用到的对象
- 并发标记(CMS-concurrent-mark):进行 GC Root Tracing
- 重新标记(CMS-remark) :修正并发标记期间由于用户程序运行而导致的变动
- 并发清除(CMS-concurrent-sweep):进行清除工作
初始标记和重新标记会导致 stop the world。由于最耗时的并发标记和并发清除都可以和用户程序同时进行,所以其实可以认为 GC 和用户程序是同时进行的。
CMS的一个劣势是对CPU资源比较敏感,不过现代的后端系统通常是重IO的,所以个人感觉影响并不会太大。另外一个问题是,由于 GC 和用户程序同时进行,可能会有部分新产生的垃圾无法被直接回收,需要等到下一次 GC 时再回收。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。使用-XX:CMSInitiatingOccupancyFraction
可以定义老年代使用了多少空间后就开始进行 GC,默认为68%。
CMS的在新生代与 Parallel GC 一样使用并行复制收集器,而在老年代使用 标记-清除 算法。在老年代,由于不会进行整理操作,会导致空间碎片的出现,如果空间碎片过多,则需要使用Serial Old进行一次老年代垃圾回收,为了防止该情况出现,可以设置若干次CMS GC以后进行一次内存整理;同时,由于和应用线程并行的进行垃圾回收,所以需要在内存耗尽前就开始垃圾收集,否则可能导致应用无内存可用。
相关参数介绍:
- 启用CMS:-XX:+UseConcMarkSweepGC。
- CMS默认回收线程数目是 (ParallelGCThreads + 3)/4) ,ParallelGCThreads是年轻代并行收集线程数目;如果你需要明确设定,可以通过-XX:ParallelCMSThreads=2来设
- CMS阶段整理内存:-XX:+UseCMSCompactAtFullCollection;
- 设置每多少次CMS后进行一次内存整理:-XX:+CMSFullGCsBeforeCompaction=<次数>
- 老年代进行CMS时的内存消耗百分比,默认为68:-XX:CMSInitiatingOccupancyFraction=75
G1 GC
使用 G1GC 时,Java 堆的内存布局与其他的收集器有很大区别,它将整个 Java 堆划分成多个大小相等的独立区域(Region),虽然还是有年轻代和老年代的概念,但是新生代和老年代不是物理隔离的,它们都是一部分 Region的集合,并且这些Region无需在物理上连续。一般建议当机器内存较大(6G以上),且之前使用的GC不能满足需求时才使用G1GC。
G1GC堆内存分布G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。 G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。 这种使用Region划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
在G1收集器中,Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。 G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。 当进行内存回收时,在 GC 根节点的枚举范围中加入Remembered Set 即可保证不对全堆扫描也不会有遗漏。
G1 垃圾回收的过程有下面几步:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,这阶段需要停顿线程,但耗时很短。 并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。 而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。
G1GC的相关参数:
- -XX:+UseG1GC 使用G1GC
- -XX:MaxGCPauseMillis=n 最大暂停时间,jvm不保证做到 单位为毫秒
- -XX:InitiatingHeapOccupancyPercent=n 启动一个并发垃圾收集周期所需要达到的整堆占用比例。这个比例是指整个堆的占用比例而不是某一个代,如果这个值是0则代表‘持续做GC’。默认值是45
- -XX:MaxTenuringThreshold=n 经过多少轮minor GC,对象会进入老年代
- -XX:ParallelGCThreads=n 并行阶段使用线程数
- -XX:ConcGCThreads=n 并发垃圾收集的线程数目
- -XX:G1ReservePercent=n 防止晋升老年代失败而预留的空闲区域的数目
- -XX:G1HeapRegionSize=n 空闲区域大小 最小1Mb 最大 32Mb.
3. 其他相关命令行选项
-
-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
-
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-
-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-
-XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为1:1:8,一个Survivor区占整个年轻代的1/10。
-
-Xss:每个线程的堆栈大小。减小这个值可以增加最大线程数。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,一定要测试调优。
-
-XX:NewRatio:老年代与新生代的比例 比如3表明老年代大小为新生代3倍。
-
-Xloggc:path 垃圾回收日志记录位置
-
-XX:+PrintGCDateStamps 用真实日期打印垃圾回收时间
-
-XX:+PrintGCDetails 打印垃圾回收 详情
-
-XX:+HeapDumpOnOutOfMemoryError OOM时候记录heap dump
-
-XX:HeapDumpPath=C:\Users\millions\java_error_in_idea.hprof: heapdump的文件位置
4. 总结
jvm一直在发展,不同应用也有各自的实际情况和应用场景。调整应用 GC 参数一定要从吞吐量和反应时间两方面考虑,以实际测试结果为准,而不能轻信任何人给的经验结论。
网友评论