作者:京东科技 白洋
前言:
背景:为应答 618、双 11 大促,生产金融侧会依据批发侧大促节奏进行整体零碎备战。对外围流量入口承载的零碎进行加固优化,排除零碎危险,保障大促期间零碎稳固。因为大促期间,生产金融业务承当着直面用户,高并发,零碎危险大概率间接造成资损危险等问题。在日常压测和大促期间,常常会产生 Jvm 呈现大量 young Gc 和 局部 full GC 的状况,导致性能降落,可用率升高等状况。之前对 Jvm 的垃圾回收机制不是很熟,如何防止和如何调优,基本上无所不通,本文也是对本人学到的常识的一个坚固~
一、什么是 JVM 的 GC?
JVM(Java Virtual Machine)。JVM 是 Java 程序的虚拟机,是一种实现 Java 语言的解释器。
它提供了一种独立于操作系统的运行环境,使得 Java 程序在任何反对 JVM 的计算机上都能够运行。JVM 负责加载、验证、解释、执行和垃圾回收 Java 字节代码,并为 Java 程序提供内存治理、线程治理和安全控制等服务。
JVM 中的 GC(Garbage Collection)是垃圾回收的缩写,是 JVM 的内存管理机制。
Young GC 和 Full GC 是两种不同的 GC 算法。
Young GC:针对新生代对象的回收算法,通常应用的是复制算法或者标记整顿算法。因为新生代中的对象生命周期短,所以 Young GC 速度要比 Full GC 快得多。
Full GC:针对整个堆内存的回收算法,用于回收那些在 Young GC 中没有回收的存活对象。Full GC 速度比较慢,因为它须要扫描整个堆内存,因而对系统的性能影响较大。
所以在设计 Java 利用时,须要尽量减少 Full GC 的次数,以保证系统的性能。常见的办法包含扩充新生代的内存空间,缩小数组长度等。
以上根本是通用的对 Jvm 和 Gc 的解释。然而能够显著看出短少一些细节,对咱们来说还是没什么用,测试同学该如何了解具体的场景呢??
咱们首先来了解 young GC 的诞生过程:
首先,了解复制算法和标记整顿算法,它们是两种不同的 Young GC 回收算法。
复制算法:将新生代内存分成两个等大的局部,新创建的对象存储在一个局部,而另一个局部用于存储存活的对象。当新生代内存不够用时,Young GC 会产生,将存活的对象复制到另一个内存区域。复制算法不会导致内存碎片,然而会耗费肯定的内存空间。
标记整顿算法:每次 Young GC 时,会先标记所有存活的对象,而后再将所有不存活的对象整顿到一起。因而,内存碎片可能会导致空间节约。标记整顿算法实用于须要放弃内存空间整洁的利用,比方那些须要长时间运行的服务器利用。
这个看看就好,实质上 Young Gc 能够了解成 jvm 失常的扫垃圾过程
根据上述的解释,置信聪慧的小伙伴能够清晰的看到,young Gc 有着更高的回收效率,对业务侧的影响要小的多~ 因而,咱们进一步来看看头痛的 full Gc,是怎么来的?
Full GC 是 Full Garbage Collection 的缩写,是指把整个堆内存扫描一遍,回收不再应用的对象并且整顿内存的过程。因为堆内存的整体回收过程十分慢,因而,Full GC 可能导致应用程序的暂停。
如上所述,只有更正当的内存调配,防止不被应用的对象频繁呈现,调整堆内存的扫描时间。
full GC, 即全垃圾回收,是一种垃圾回收的过程 , 它会暂停所有的应用程序线程,对整个堆进行回收。(这个太可怕了。。)
初始标记:首先,垃圾回收器标记出哪些对象是须要被回收的。
并发标记:而后,垃圾回收器将标记任务分配给多个线程,并发地执行标记工作。
从新标记:在并发标记的过程中,如果有新的对象被创立,须要对这些对象进行从新标记。
整顿:接下来,垃圾回收器将没有被标记的对象整顿到内存的一端。
回收:最初,垃圾回收器回收被标记的对象,开释内存。
来个图大家看的明确一些~ Full Gc 的生命流程~ 实质上就是,垃圾太多,失常的活儿干不了了,内存空间不够了,得停下所有的事件,来一次大扫除
二、写代码的时候能做什么?
上述可得,fullGc 是很可怕的,因为堆内存的整体回收过程十分慢,因而,Full GC 可能导致应用程序的暂停,间接就崩掉了。。。
要防止 Full GC 产生,实质上就须要对系统堆内存大小进行适当设置以及对代码进行优化,基本上有以下这些技巧:
•调整堆内存大小:确定适合的堆内存大小是防止 Full GC 产生的要害。
•对代码进行内存优化:应用不同的数据结构,防止内存透露,应用对象池等技巧。
•应用较大的新生代:新生代是存储短生命周期对象的内存区域,更大的新生代能够缩小 Full GC 的频率。
•设置适当的垃圾回收算法:应用 G1 GC 算法等技术能够进步零碎性能并缩小 Full GC 的频率。
•这些是防止 Full GC 产生的一些常见倡议。请留神,每种状况都不同,所以要依据具体情况抉择适当的办法。
这些办法,看起来还是很形象 … 咱们来说点具体例子
首先,堆内存大小和垃圾回收算法,不是咱能操作和关怀的,业务侧也个别不怎么会调,交给运维同学了。浅提一下,调整内存大小:通过调整 JVM 参数,如 -Xms、-Xmx 来适当增大内存。
具体咱们能做到的,最次要的就是缩小数据对象的生命周期:
通过应用弱援用、软援用、虚援用等援用类型,能够在不须要数据对象时间接回收,从而防止 Full GC。
缩小数据对象的生命周期是指在程序中应用对象时,尽可能地缩短对象的存活工夫。
这样能够缩小垃圾对象数量,升高 Full GC 的频率。这是咱们重点须要关注的!!
以下是一些具体的例子:
1. 防止应用不必要的长期对象:
如果程序中有大量长期对象,它们可能很快就会被垃圾回收器清理掉。因而,应该防止创立不必要的长期对象,以缩小对象的生命周期。
eg:
double average(double[] values) {
double sum = 0;
for (double value : values) {sum += value;}
return sum / values.length;
}
在这个例子中,数组 values 是长期对象,在函数完结时会被销毁。这样,不用思考如何删除汇合,以防止内存透露的危险。
还有,
String concatenate(List<String> strings) {
String result = "";
for (String str : strings) {result += str;}
return result;
}
在这个例子中,每次循环都会创立一个长期的字符串对象,并将其附加到 result 中。随着循环的进行,这些长期对象可能会沉积,导致频繁的 GC 操作。为了防止这个问题,能够应用 Java 中的 StringBuilder 来构建字符串
String concatenate(List<String> strings) {StringBuilder result = new StringBuilder();
for (String str : strings) {result.append(str);
}
return result.toString();}
这样的话,不再须要创立长期字符串对象,从而缩小 GC 的次数。
2. 尽早开释对象:当对象不再须要时,应该尽早将其开释,以便及时回收它。例如,在程序实现解决后立刻开释对象,而不是等到下一次须要应用它之前。
比方咱们日常最罕用的 for 循环就很棒,
for (int i = 0; i < data.length; i++) {// do something with data[i]
}
在这个例子中,循环变量 i 只在循环中应用,并在循环完结后开释。这样做能够缩小不必要的内存应用,从而缩小全垃圾回收的次数。
另一个具体的例子是应用 try-with-resources 语句,这能够确保流等资源在不再应用后主动敞开,例如:
try (FileInputStream in = new FileInputStream("file.txt")) {// use the input stream} catch (IOException e) {// handle exception}
在这个例子中,文件输出流在不再应用后会被主动敞开,就不必手动关,这样也会更正当~
3. 重复使用对象:如果能够,能够尝试重复使用同一个对象,而不是频繁地创立和销毁新的对象。
这个比拟好了解,比方同样的事务流程,没必要搞两个变量 ~ 起码的变量干最多的活儿是最现实的~
4. 应用对象池:
能够应用对象池,重复使用固定数量的对象,而不是一直创立新的对象。这样能够缩小对象的生命周期,并升高 Full GC 的频率。
应用对象池是一种罕用的防止 Full GC 的形式。它的核心思想是反复利用曾经创立好的对象,而不是每次都创立新的对象。
以下是一个简略的对象池的代码例子:
import java.util.ArrayList;
import java.util.List;
public class ObjectPool {
private static final int POOL_SIZE = 100;
private static final List<Object> pool = new ArrayList<>(POOL_SIZE);
static {for (int i = 0; i < POOL_SIZE; i++) {pool.add(new Object());
}
}
public static Object getObject() {if (pool.isEmpty()) {return new Object();
}
return pool.remove(0);
}
public static void returnObject(Object object) {pool.add(object);
}
}
在代码中,咱们创立了一个大小为 100 的对象池,并在动态代码块中初始化了 100 个对象。当咱们须要应用对象时,能够调用 getObject 办法,如果对象池中有残余的对象,就从对象池中取出一个对象;如果没有残余对象,就新建一个对象。当不须要这个对象了,就能够调用 returnObject 办法,将对象放回对象池中。
这样,咱们能够反复利用曾经创立好的对象,缩小了对象的创立和销毁的频率,从而缩小了 Full GC 的几率。
5. 应用弱援用:
在程序中,如果有大量对象不会再应用,能够应用弱援用来援用它们。这个最多利用在类型缓存这样的场景,它们不是必须的对象,因而有些时候能够间接干掉
这是一个弱援用的例子,这玩意儿还是比拟形象的。。。
import java.lang.ref.WeakReference;
public class WeakReferenceExample {public static void main(String[] args) {Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null;
System.gc();
if (weakRef.get() != null) {System.out.println("Object is still alive");
} else {System.out.println("Object has been garbage collected");
}
}
}
举个更具体点的:
import java.lang.ref.WeakReference;
import java.util.HashMap;
public class WeakReferenceExample {public static void main(String[] args) {HashMap<String, WeakReference<MyObject>> cache = new HashMap<>();
MyObject obj = new MyObject("example");
cache.put("example", new WeakReference<>(obj));
obj = null;
System.gc();
System.out.println(cache.get("example").get());
}
static class MyObject {
String name;
public MyObject(String name) {this.name = name;}
@Override
public String toString() {return "MyObject{" + "name='" + name + '''+'}';
}
}
}
这个例子中,咱们将一个 MyObject 对象封装在弱援用中,并保留在 HashMap 缓存中,当咱们显式调用 System.gc() 办法时,JVM 会尝试回收这些不再应用的对象,如果内存不足,则会回收 MyObject 对象,那么 cache.get(“example”).get() 返回的将是 null。
三、测试能做啥
回顾全文,其实咱们能做的真不多,只能在业务代码测试的过程中,关注对象的应用频次,回绝有效的援用或 new 一大堆没必要的对象。
具体伎俩 :
定期监测 GC 日志:通过咱们的 jvm 关注,大我的项目上线后,或代码改变特地大的我的项目上线后,做一下读写压测的操作~ 关注咱们的 jvm,jvm 监测地址
数据结构优化:根据上述的伎俩,测试开发工程师能够通过上述伎俩,来优化数据结构来减小数据对象的生命周期,从而防止 Full GC。 在测试过程中,关注一下数据结构的合理性~
关注单元测试:通过运行研发的单元测试,或本人手动写一个,模仿理论的内存应用状况,来评估内存的应用状况(基本上,目前的业务代码能跑起来,大概率是没问题的,,)
总结:
日常的业务代码测试,对内存的敏感度要高一些,没 bug 不肯定不会出问题,当初咱们的零碎是成熟的牢靠的,然而面对大促的压力,如果能提前解决隐患,干掉有危险的内存应用,也是节俭咱们压测时的工作量嘛~