如果我可以拥有一项特异功能,那么我希望我可以收拾掉世界上所有的’垃圾’。但是在java虚拟机的世界里,有垃圾收集器来帮我做这个事情,真的可以说是dkrx!
jvm有两种运行模式,一种是client模式,一种是server模式,server模式适合于服务端应用,client模式适合于桌面client应用。两种模式的主要不同是默认的虚拟机参数会有不同,server模式下会给jvm分配更多的资源,其中就包括内存资源,不管怎么说,内存资源总是有限的,想用有限的内存资源去支撑用不宕机的java服务?理想倒是很丰满,java服务可能用不宕机?
所以就有了垃圾收集这个事啦,将jvm内存中没有用的对象实例进行回收,释放出更多的内存空间供程序使用。
判断对象是否是垃圾
在进行垃圾回收之前,首先要判断对象是否是垃圾,JVM对对象进行标记,区分可以被回收和不可以被回收的对象,有两种标记的算法:
1、引用计数法
java对象头里面有个引用计数器,当对象被引用一次的时候计数器加1,如果计数器为0则说明对象没有被引用到,可以回收掉,这种方法的缺点是解决不了循环引用的问题(造成内存泄漏),还有每次引用和不引用都要更新计数器,浪费时间。
2、可达性分析法
从一系列GC ROOT出发,一直向下找出引用的对象,每一个被引用的对象都会至少存在一条从GC ROOT过来的引用链,这样的对象就不是垃圾。
可以作为GC ROOT的有:
虚拟机栈中引用的对象,
本地方法栈中JNI引用的对象,
方法区中类静态属性引用的对象,
方法区中常量引用的对象
这种方法可以解决循环依赖无法回收问题。
如果有两个对象互相引用依赖对方,但是它们都不存在GC ROOT可达的引用链,这样使用引用计数法是不会被判定为垃圾的,但是使用可达性分析法却会。按理来说两个对象相互引用,却没有被外界引用,形成孤岛,应该是要被回收掉的。
垃圾回收算法
标记出哪些是垃圾对象,哪些不是垃圾对象后,接下来就是把垃圾对象清理掉了,清理垃圾对象也有不同的算法:
1、标记-清除算法
先把垃圾对象标记出来,再直接清除掉,清除后会产生内存碎片,因为对象被清除后被使用的内存就不连续了,会影响后续新创建对象的内存分配,因为可能没有足够大的连续内存空间分配给要创建的对象而再次触发GC。另外,当需要清除的对象占大多数时,会需要花费大量时间,效率低。
2、标记-整理算法
先把垃圾对象标记出来,把存活的对象挨着移到一边,剩下的空间清除掉,这样就会清理出一整块连续的空间
3、标记-复制算法
内存分两块,只用其中一块,进行垃圾对象标记后,将存活的对象复制到另一块内存,并将本块内存整块清理掉,但是这样只能用一半的内存,有点浪费
分代收集理论
现在绝大多数的JVM都采用分代收集的理论来设计,把堆分成新生代和老年代,新生代和老年代由于其特性的不同,采用不同的垃圾回收算法去回收,新生代一般采用标记复制算法,老年代一般采用标记整理算法。其中默认
新生代:占整个堆内存的1/3,分为from survivor、to survivor、eden三个区
老年代:占整个堆内存的2/3
新创建的对象一般分配在新生代的eden和from survivor上,当这两个区域满了以后,通过标记复制算法进行垃圾回收,存活的对象复制到to survivor上,接下来新创建的对象分配在eden和存活对象所在的to survivor上,当对象熬过一定次数的GC后,会被转移到老年代中。当老年代中的内存也被占满容不下新的对象的时候,会在老年代中进行垃圾回收。
垃圾收集器
刚才讲了垃圾回收的几种算法,属于理论,JVM针对这些理论,有不同的实践,我们把这些不同的实践叫做垃圾收集器,因此每一种垃圾回收算法有对应很多种垃圾收集器,比较典型的有如下:
新生代:Serial、ParNew(新生代并行收集器)、Parallel Scavenge(jdk8默认使用)
老年代:Serial Old(jdk1.8后不建议使用)、Parallel Old、CMS
新生&老年:G1,
命令行参数设置:
-XX:+UserSerialGC //设置垃圾收集器
-Xms8m //设置初始堆内存大小
-Xmx8m //设置最大堆内存大小
-Xmn8m //设置新生代堆内存大小
-XX:+PrintGCDetails //打印GC详情
新生代中的垃圾收集器
1、Serial 串行垃圾收集器
参数设置:-XX:+UseSerialGC,启用串行垃圾收集器进行新生代的垃圾回收
特点:采用复制算法;单GC线程收集;
当进行垃圾收集时,会"Stop the World";
比较老的垃圾收集器,适用于jvm运行在client模式下使用,是client模式下的默认收集器;
因为单线程,所以不涉及线程切换的开销,回收起来效率比较好;
适用于单CPU或双核场景,配合老年代Serial Old使用
2、ParNew 并行垃圾收集器
参数设置:-XX:+UseParNewGC,启用并行垃圾收集器进行新生代的垃圾回收
特点:Serial收集器的多线程版本,多GC线程进行垃圾回收,其余行为跟Serial一致;
使用复制算法;
适用于jvm运行在server模式下;
如果在单个CPU下,不会比Serial效果好,因为多线程并行的话会存在线程切换的开销;
默认开启的GC线程数与CPU核心数相等,可以通过-XX:ParallelGCThreads指定;
能跟老年代的CMS配合使用,都是注重时延而不是吞吐量的,比较适用于与用户交互的应用
3、Parallel Scavenge 并行垃圾收集器
参数设置:-XX:+UseParallelOldGC 或 -XX:+UseParallelGC
-XX:MaxGCPauseMillis ——控制最大垃圾收集停顿时间
-XX:GCTimeRatio ——设置垃圾收集时间占总时间的比例
-XX:+UseAdptiveSizePolicy ——垃圾收集会自适应调节,不用手动设置其他参数
特点:使用复制算法;多GC线程并行收集;
主要关注CPU吞吐量而不是关注用户线程停顿时间;
适用于后台计算密集型应用(如批处理、订单处理、工资支付等),而不是用户交互密集型场景
CPU吞吐量=CPU运行用户代码的时间/(CPU运行用户代码的时间+垃圾收集时间)
跟老年代的Parallel Old配合运行
老年代中的垃圾收集器
1、Serial Old 老年代串行垃圾收集器
特点:单GC线程收集;主要用于client模式;
采用标记-整理算法;其余特点跟新生代的Serial一致。
使用:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,
另一种用途是作为CMS收集器的后备方案;
可以与新生代Serial搭配使用。
2、Parallel Old 老年代并行垃圾收集器
参数设置:-XX:+UseParallelOldGC或-XX:+UseParallelGC
特点:Parallel Scavenge收集器的老年代版本(两者结合使用);
采用GC多线程标记-整理算法;
适用于注重吞吐量和CPU资源的场合;jdk1.6以后才有;
3、CMS(Concurrent Mark Sweep)老年代并发标记清除垃圾收集器
设置:-XX:+UseConcMarkSweepGC
特点:以获取最短回收停顿时间为目标,适用于web服务器应用;针对老年代;
采用标记-清除算法,不压缩整理,会产生碎片;
并发收集、低停顿;适用于注重用户体验的场景;
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,前后还是会有一小段串行时间
工作过程:
初始标记:
暂停所有的其他线程,初始标记仅仅标记老年代中GC Roots能直接关联到的对象(包括GCRoot直接引用的老年代对象和GCRoot引用的新生代对象中直接引用到的老年代对象),速度很快,可以通过-XX:+CMSParallelInitialMarkEnabled参数开启多个GC线程;
并发标记:
并发标记就是进行GC Roots Tracing的过程(标记出初始标记中标记出来的对象引用到的老年代中的对象);同时开启GC线程和用户线程。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。 所以这个算法里会跟踪记录这些发生引用更新的地方;
重新标记(最终标记):
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用多线程并行执行来提升效率);需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
并发清除:
开启用户线程,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象
当然少不了G1这样的垃圾收集器,由于G1是一个比较新而且比较不一样的垃圾收集器,因此后续看看是否单独出一篇文章来讲一下。
对于垃圾收集器的选择,总结如下:
client端应用:Serial + Serial Old
注重时延用户体验:ParNew + CMS(Serial Old)
注重吞吐量:Parallel Scavenge + Parallel Old
转载请注明:XAMPP中文组官网 » 不认识垃圾收集器怎么进行JVM调优