垃圾回收篇
GC概述
什么是垃圾(Garbage)呢?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
Java中垃圾回收的重点区域是?
Java堆是垃圾收集器的工作重点,只会涉及到堆和方法区。
从次数上讲:频繁收集Young区;较少收集Old区;基本不动Perm区(或元空间)
多种垃圾回收算法
垃圾判别阶段的算法
-
引用计数算法
引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
-
可达性分析算法
可达性分析(或根搜索算法、追踪性垃圾收集):
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 相较于引用计数算法,这里的可达性分析就是Java、c#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage collection)。
GC Roots
在Java 语言中,GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。 - 本地方法栈内JNI(通常说的本地方法)引用的对象
- 类静态属性引用的对象
比如:Java类的引用类型静态变量方法区中常量引用的对象
比如:字符串常量池(string Table)里的引用 - 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、outOfMemoryError),系统类加载器。 - 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但自己又不存放在堆内存里面,那它就是一个Root。
注意点:
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须“stop The world”的一个重要原因。即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
垃圾清除阶段的算法
标记-清除算法
执行过程:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
- 标记:collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除:collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
(很多书、视频讲错了!说是标记的垃圾对象。这里要注意了!)
缺点:
1、效率比较低:递归与全堆对象遍历两次(先标记,再清除)
2、在进行GC的时候,需要停止整个应用程序,导致用户体验差
3、这种方式清理出来的空闲内存是不连续的,产生内存碎片
复制算法
核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中(复制时就已经重新排好位置,没有碎片),之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
缺点:
1、此算法的缺点也是很明显的,就是需要两倍的内存空间。
2、对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
3、如果系统中的存活对象很多,复制算法不会很理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
标记-压缩算法
执行过程:
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
之后,清理边界外所有的空间。
指针碰撞(Bump the Pointer):
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer)。与之对应的时空闲列表。
缺点:
1、从效率上来说,标记-压缩算法要低于复制算法。效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。对于老年代每次都有大量对象存活的区域来说,极为负重。
2、移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
3、移动过程中,需要全程暂停用户应用程序。即:STW
分代收集算法
分代收集算法:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
- 年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。 - 老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
Mark阶段的开销与存活对象的数量成正比。
Sweep阶段的开销与所管理区域的大小成正相关。
Compact阶段的开销与存活对象的数据成正比。
增量收集算法
基本思想:
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
GC相关概念
system.gc()
提醒jvm的垃圾回收器执行full gc,但是不确定是否马上执行gc,与Runtime.getRuntime( ).gc()
的作用一样。
finalize()方法
finalize()
方法什么时候会被执行?当一个对象首次考虑要被回收时,会调用其finalize()
方法
finalize的作用:
(1) finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
(2) 不建议用finalize方法完成“非内存资源”的清理工作,但建议用于:①清理本地对象(通过JNI创建的对象);②作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。
OOM前必有GC?
- 这里面隐含着一层意思是,在抛出OutofNemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等;在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。 - 当然,也不是在任何情况下垃圾收集器都会被触发的
比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。
Java中内存泄漏的8种情况
1-静态集合类
静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests() {
object obj = new object();//局部变量,list不被回收,则obj对象就不能被回收
list.add(obj);
}
}
2-单例模式
单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
3-内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
4-各种连接,如数据库连接、网络连接和IO连接等
各种连接,如数据库连接、网络连接和IO连接等。
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
5-变量不合理的作用域
变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为nul1,很有可能导致内存泄漏的发生。
public class UsingRandom {
private String msg;
public void receiveMsg(){
// private string msg;
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
//msg = null;// 如果此行不注释掉,会出现内存泄露
}
}
6-改变哈希值
改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是 String为什么被设置成了不可变类型,我们可以放心地把 String存入HashSet,或者把String 当做 HashMap 的 key值。当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashcode不可变。
public class ChangeHashCode {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person( 10e1,"AA");
Person p2 = new Person( 1002,"BB") ;
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
system.out.print1n( set);//2个对象!
}
}
7-缓存泄露
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
8-监听器和回调
内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚。
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
5种引用
强引用:不回收
软引用:内存不足即回收
弱引用:发现即回收
虚引用:对象回收跟踪
终结器引用:
①它用以实现对象的finalize()方法,也可以称为终结器引用。
②无需手动编码,其内部配合引用队列使用。
③在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。
垃圾回收器
GC评估指标
- 吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)
吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间) - 垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用:Java堆区所占的内存大小。
- 快速:一个对象从诞生到被回收所经历的时间。
现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间。
垃圾回收器
串行回收器:serial、serial old
并行回收器:ParNew、Parallel scavenge、 Parallel old
并发回收器:CMS、G1
1.两个收集器间有连线,表明它们可以搭配使用:
Serial/Serial old、Serial/CMS、ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial old、Parallel Scavenge/Parallel old、G1
2.其中Serial old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
3.(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP 173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK 14中:弃用Parallel Scavenge和Serialold GC组合(JEP366)
- (青色虚线)JDK 14中:删除CMS垃圾回收器(JEP 363)
如何查看默认GC
-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
使用命令行指令:jinfo - flag 相关垃圾回收器参数 进程ID
Serial GC:串行回收
优势:
1、简单而高效(与其他收集器的单线程比),对于限定单个CPU 的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
2、运行在Client模式下的虚拟机是个不错的选择。
3、在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
参数:
在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC
ParNew GC:并行回收
由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器更高效?
1、ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。但是在单个CPU的环境下,ParNew收集器不比serial收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
2、因为除Serial外,目前只有ParNew GC能与CMS收集器配合工作
参数:
在程序中,开发人员可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。等价于新生代用ParNew GC,且老年代用Serial old GC
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。
Parallel GC:吞吐量优先
和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和”Stop-the-world”机制。在程序吞吐量优先的应用场景中,Parallel收集器和Parallel old收集器的组合,在Server模式下的内存回收性能很不错。
在Java8中,默认是此垃圾收集器。
参数配置:
- -XX:+UseParallelGC手动指定年轻代使用Parallel并行收集器执行内存回收任务
- -XX:+UseParalleloldGC手动指定老年代都是使用并行回收收集器。
分别适用于新生代和老年代。默认jdk8是开启的。
上面两个参数,默认开启一个,另一个也会被开启。(互相激活) - -XX:Paralle1GCThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU 数量。
当CPU数量大于8个,Paralle1GCThreads的值等于3+[5*CPU_Count]/8] - -XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒
为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高异发、整体的吞吐量。所以服务器端适合Paralle1,进行控制。
该参数使用需谨慎。 - -XX:GCTimeRatio垃圾收集时间占总时间的比例(= 1 / (N+ 1))。用于衡量吞吐量的大小。
取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。 - -XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
CMS:低延迟
在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS (Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
- 目前很大一部分的]ava应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
- CMS的垃圾收集算法采用标记-清除算法,并且也会”Stop-the-world”
不幸的是,CMS 作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统传用CMS GC。
收集过程:
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
- 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
- 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(比如:由不可达变为可达对象的数据),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
- 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户钱程同时并发的。
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CNS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阙值时,便开始进行回收。
有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把CMS算法换成Mark Compact呢?
因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提是它运行的资源不受影响。Mark-Compact更适合“Stop the world”这种场景下使用。
CMS的优点:
并发收集;低延迟
CMS的弊端:
1)会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
2)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
3)CMS收集器无法处理浮动垃圾(原来可达,后来不可达)。可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
参数
- -XX:+UseConcMarkSweepGC 手动指定使用CMS 收集器执行内存回收任务。
开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区用)+CMS(Old区用)+Serial old的组合。 - -XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阙值,便开始进行回收。
JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%;
如果内存增长缓慢,则可以设置一个稍大的值,大的阙值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阙值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。 - -XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿间变得更长了。
- -XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。
G1GC:区域化分代式
每次根据允许的收集时间,优先回收价值最大的Region。JDK9以后的默认GC选项,取代了CMS回收器。
Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以通免内存碎片。
参数
- -XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务。
- -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划纷出约2048个区域。默认是堆内存的1/2000。
- -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
- -XX: Paralle1GCThread 设置STw时Gc线程数的值。最多设置为8
- -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(Paralle1GCThreads)的1/4左右。
- -XX:InitiatingHeapOccupancyPercent设置触发并发GC周期的Java堆占用率蟒值。超过此值,就触发GC。默认值是45。
操作步骤
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式: YoungGC、Mixed GC和Full GC,在不同的条件下被触发。
垃圾回收过程
过程1:年轻代GC
过程2:并发标记过程
过程3:混合回收
过程4:FullGC
G1回收器优化建议
- 年轻代大小
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小;
固定年轻代的大小会覆盖暂停时间目标 - 暂停时间目标不要太过严苛
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间;
评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。
GC日志分析
日志参数
-verbose:gc 输出gc日志信息,默认输出到标准输出
-XX:+PrintGC 输出GC日志。类似: -verbose:gc
-XX:+PrintGCDetails 在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
-XX:+PrintGCTimeStamps 输出GC发生时的时间戳
-XX:+PrintGCDateStamps 输出GC发生时的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 每一次GC前和GC后,都打印堆信息
-Xloggc:<file> 表示把GC日志写入到一个文件中去,而不是打印到标准输出中
JVM性能监控篇
9大命令行监控及诊断工具
6大GUI监控及诊断工具
其它多种辅助监控工具
JVM运行时调优参数
JVM调优案例篇
性能优化步骤
性能评价指标
4大OOM案例
OOM案例1∶堆益出
报错信息:java.lang.OutOfMemoryError:Java heap space
# 参数配置:初始-Xms30M -Xmx30M
-XX:+PrintGCDetails
-XX:MetaspaceSize=64m
-XX:+HeapDumpOnOutofMemoryError
# hprof不会被覆盖,第二次不会生成
-XX:HeapDumpPath=heap/heapdump.hprof
-XX:+PrintGCDateStamps -Xms200M -Xmx200M
# 第二次生成log文件会重新被覆盖
-Xloggc:log/gc-oomHeap.log
dump文件分析:jvisualvm
分析;MAT
分析
原因及解决方案
- 原因
1、代码中可能存在大对象分配
2、可能存在内存泄漏,导致在多次Gc之后,还是无法找到一块足够大的内存容纳当前对象。 - 解决方法
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用MAT等工具分析一下,检查是否存在内存泄漏的问题
3、如果没有找到明显的内存泄漏,使用-Xmx加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable对象,也有可能是框架内部提供的,考虑其存在的必要性
OOM案例2:元空间溢出
报错信息:java.lang.OutOfMemoryError: Metaspace
# JNM参数配置
-XX:+PrintGCDetails
-XX:MetaspaceSize=60m
-XX:MaxMetaspaceSize=60m
-Xss512K
-XX:+HeapDumponOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof
-XX:SurvivorRatio=8
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-XX:+PrintGCDateStamps
-Xms60M -Xmx60M
-Xloggc:log/gc-oomMeta.log
原因及解决方案
JDK8后,元空间替换了永久代,元空间使用的是本地内存
- 原因:
1.运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
2.应用长时间运行,没有重启
3.元空间内存设置过小 - 解决方法:
因为该OOM原因比较简单,解决方法有如下几种:
1.检查是否永久代空间或者元空间设置的过小
2.检查代码中是否存在大量的反射操作
3.dump之后通过mat检查是否存在大量由于反射生成的代理类
enhancer.setUseCache(false)
,选择为true的话,使用和更新一类具有相同属性生成的类的静态缓存,而不会在同一个类文件还继续被动态加载并视为不同的类,这个其实跟类的equals()和hashCode()有关,它们是与cglib内部的class cache的key相关的。
# jstat -gc pid 打印GC信息间隔时间(毫秒) 打印次数
jstat -gc 10376 1000 10
OOM案例3: GC overhead limit exceeded
# JVM配置
-XX:+PrintGCDetails
-XX:+HeapDumponoutOfMemoryError
-XX:HeapDumpPath=heap/dumpExceeded.hprof
-XX:+PrintGCDatestamps
-XXms10M -Xmx10M
-Xloggc: 1og/gc-oomExceeded.log
原因及解决方案:
原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
解决方法:
1.检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2.添加参数-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.outOfMemoryError: Java heap space
。
3.dump内存,检查是否存在内存泄漏,如果没有,加大内存。
OOM案例4∶线程溢出
报错信息:java.lang.OutOfMemoryError : unable to create new native Thread
# jinfo -flag 参数名 pid
jinfo -flag ThreadStackSize 5016
线程总数:
能创建的线程数的具体计算公式如下:
(MaxProcessMemory - JVMWemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
其中,MaxProcessMemory
指的是进程可寻址的最大空间;
JVMMemory
JVM内存;
ReservedOsMemory
保留的操作系统内存;
ThreadStackSize
线程栈的大小。
# 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
/proc/sys/kernel/pid_max # 系统最大pid值,在大型系统里可适当调大
/proc/sys/kernel/threads-max #系统允许的最大线程数,主要关注此参数
maxuserprocess (ulimit -u)#系统限制某用户下最多可以运行多少进程或线程
/proc/sys/vm/max_map_count
经实测,在32位windows系统下较为严格遵守;64位系统下只能保证正/负相关性,甚至说相关性也不能保证。
性能测试工具:Jmeter
7大性能优化案例
性能优化案例1
调整堆大小提高服务的吞吐量
修改tomcat JVM配置
初始配置
优化配置
# 写入文件setenv.sh中
#接下来我们测试另外一组数据,增加初始化和最大内存:
export CATALINA_OPTS="$CATALINA_OPTS -Xms120m"
export CATALINA_OPTS="$CATALINA_OPTS -xx:SurvivorRatio=8"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx120m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseParallelGc"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=64m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -xloggc:/opt/tomcat8.5/logs/gc.log"
#重新启动tomcat,查看gc.log
vi gc.log
性能优化案例2
JVM优化之JIT优化
-
堆,是分配对象的唯一选择吗
是–>不是–>是 -
编译的开销
1)时间开销
说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。JIT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。所以,对“只执行一次”的代码而言,解释执行其实总是比JIT编译执行要快。怎么算是只执行一次的代码
呢?粗略说,下面条件同时满足时就是严格的只执行一次
。- 只被调用一次,例如类的构造器(class initializer,())
- 没有循环,对只执行一次的代码做JIT编译再执行,可以说是得不偿失。
- 对只执行少量次教的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。
只有对频繁执行的代码(热点代码),JIT编译才能保证有正面的收益。
2)空间开销
对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10+是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致代码爆炸。这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。
-
即时编译对代码的优化
1)逃逸分析- 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
- 逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
- 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
- 逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
2)优化一:栈上分配
将堆分配转化为栈分配。如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。可以减少垃圾回收时间和次数。
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
# 栈上分配测试
-Xmx1G -Xms1G -XX : -DoEscapeAnalysis -XX :+PrintGCDetails
只要开启了逃逸分析,就会判断方法中的变量是否发生了逃逸。如果没有发生逃逸,则会使用栈上分配
3)同步消除
即同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
public class synchronizedTest (
public void f() {
/**代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在()方法中,
*并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
*问题:字节码文件中会去掉hollis吗 ?
**/
object hollis = new object();
synchronized(hollis) {
system.out.println(hollis);
}
/**优化后;
* object hollis = new object();
* system.out.printLn( hollis);
**/
}
}
4)标量替换
标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。需要打开逃逸分析的前提下,标量替换才会生效。
参数-XX:+EliminateAllocations
:开启了标量替换(默认打开),允许将对象打散分配在栈上。
结论:Java中的逃逸分析,其实优化的点就在于对栈上分配的对象进行标量替换。
性能优化案例3
合理配置堆内存
-
推荐配置
在案例1中,增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?
分析:
依据的原则是根据Java Performance里面的推荐公式来进行设置。Java整个堆大小设置,Xmx和Xms设置为老年代存活对象的3-4倍,即设置为FullGC之后老年代内存占用空间大小的3-4倍。
方法区(永久代 PermSize和MaxPermSize或元空间MetaspaceSize和MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。 -
如何计算老年代存活对象
方式1:查看日志推荐/比较稳妥!
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。方式2:强制触发FulIGC方式1的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发FullGC,所以日志中并没有记录FullGC的日志。在分析的时候就比较难处理。所以,有时候需要强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。
注:强制触发FullGC,会造成线上服务停顿(STW),要谨慎!建议的操作方式为,在强制FullGC前先把服务节点摘除,FullGC之后再将服务挂回可用节点,对外提供服务,在不同时间段触发FullGC,根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小。- 如何强制触发Full GC?
1、jmap -dump:live,format=b,file=heap.bin <pid>将当前的存活对象dump到文件,此时会触发FullGC。
2、jmap -histo:live <pid>打印每个class的实例数目,内存占用,类全名信息。live子参数加上后,只统计活的对象数量,此时会触发FullGC。
3、在性能测试环境,可以通过Java监控工具来触发FullGC,比如使用VisualVM和JCorsole,VisualVM集成了JConsole,VisualVM或者JConsole上面有一个触发GC的按钮。
- 如何强制触发Full GC?
-
你会估算GC频率吗?
正常情况我们应该根据我们的系统来进行一个内存的估算,这个我们可以在测试环境进行测试,最开始可以将内存设置的大一些,比如4G这样,当然这也可以根据业务系统估算来的。
比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是(128 B/1024 Kb/1024M)* 1000 = 0.122M,那么我们程序可能需要并发读取,比如每秒读取100次,那么内存占用就是0.122*100 = 12.2M,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么333M*80%/12.2M =21.84s ,也就是说我们的程序几乎每分钟进行两到三次youngGC。这样可以让我们对系统有一个大致的估算。
UseAdaptiveSizePolicy的使用
使用ParallelGC的情况下,不管是否开启了UseAdaptiveSizePolicy参数,默认Eden与Survivor的比例都为:6:1:1。
这是因为DK 1.8默认使用 UseParallelGC垃圾回收器,该垃圾回收器默认启动了AdaptiveSizePolicy,会根据GC的情况自动计算 Eden、From 和To区的大小;所以这是由于JDK1.8的自适应大小策略导致的,除此之外,我们下面观察GC日志发现有很多类似这样的FULL GC (Ergonomics),也是一样的原因。
我们可以在jvm参数中配置开启和关闭该配置:
#开启:
-XX:+UseAdaptiveSizePolicy
#关闭
-XX:-UseAdaptivesizePolicy
注意事项:
1、在DK 1.8中,如果使用CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将UseAdaptiveSizePolicy设置为false;不过不同版本的JDK存在差异;
2、UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;
3、由于UseAdaptivesizePolicy会动态调整Eden、Survivor的大小,有些情况存在Survivor被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉Eden区后,还存活的对象进入Survivor装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。
附:对于面向外部的大流量、低延迟系统,不建议启用此参数,建议关闭该参数。
如果不想动态调整内存大小,以下是解决方案:
1、保持使用UseParallelGc,显式设置-XX:SurvivorRatio=8
。
2、使用CMS垃圾回收器。CMS默认关闭 AdaptiveSizePolicy。配置参数-XX:+UseConcMarkSweepGC
性能优化案例4
CPU占用很高排查方案
见文章JVM虚拟机最后部分即可。
性能优化案例5
G1并发执行的线程数对性能的影响
#VM参数设置,写入文件setenv.sh中
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -Xms30m"
export CATALINA_OPTS="$CATALINA_OPTS -Xmx30m"
export CATALINA_OPTS="$CATALINA_oPTS -XX:+PrintGCDetails"
export CATALINA_OPTS="$CATALINA_OPTS -XX:Metaspacesize=64m"
export CATALINA_OPTS="$CATALINA_oPTS -XX:+PrintGCDateStamps"
export CATALINA_OPTS="$CATALINA_OPTS -Xloggc:/opt/tomcat8.5/logs/gc.log"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=1"
#说明:最后一个参数可以在使用G1 GC测试初始并发GCThreads之后再加上。
#初始化内存和最大内存调整小一些,目的发生Ful1GC,关注GC时间
#关注点是:GC次数,GC时间,以及Jmeter的平均响应时间
性能优化案例6
调整垃圾回收器提高服务的吞吐量
性能优化案例7
日均百万级订单交易系统如何设置JVM参数
多种命令行工具的使用
问题一:有一个50万Pv的资料类网站(从磁盘提取文档到内存)原服务器是32位的,1.5G的堆,用户反馈网站比较缓慢。因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了!
1.为什么原网站馒?
频繁的GC,STw时间比较长,响应时间慢!
2.为什么会更卡顿?
内存空间越大,FGC时间更长,延迟时间更长
3.咋办?
垃圾回收器:paralle1 Gc ; ParNew + CMS ; G1
配置Gc参数:-XX:MaxGCPauseMillis 、-XX: ConcGCThreads
根据log日志、dump文件分析,优化内存空间的比例
jstat jinfo jstack jmap
问题二:系统内存飙高,如何查找问题?
一方面,jmap -heap 、jstat … ; gc日志情况
另一方面,dump文件分析
问题三:如何监控VM