日志中很少报告垃圾回收,尽管我们的基准测试程序每秒创建数百万个对象
few garbage collections are reported in logs, although our benchmark program creates millions of objects per second
我正在对一个名为 JOLT 的 JSON t运行sformation 库进行基准测试。图书馆看起来不错,但是
我们想确保它不会对我们的垃圾收集器造成太大压力。
因此,我创建了一个执行以下操作的小程序(转载如下):
- precompiles JSON tranformation scriptlet
- loop for a long time in each of ten threads, doing the following:
- create randomized JSON string input (approx 300 bytes)
- parse input using JOLT and apply tranform
当我们查看我们的垃圾收集器日志时,我们从未看到完整的 GC(即使在 运行ning 一半之后
一天),甚至更可疑的是,唯一报告发生的 GC 发生在第一个
启动程序几分钟,然后几个小时过去了,我们看不到
一个额外的 GC。该程序正在执行数量惊人的 t运行 操作
每秒 .. 并且根据 JOLT 文档:
"The transform process will create and
discard a lot of objects, so the garbage collector
will have work to do."
所以,我想我应该很高兴我们的 gc 日志即使在一个小时后仍然看起来像这样 运行:
Server VM (25.66-b17) for linux-amd64 JRE (1.8.0_66-b17), built on Oct 6 2015 17:28:34 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 12439584k(11065340k free), swap 12721148k(12721148k free)
CommandLine flags: -XX:InitialHeapSize=199033344 -XX:MaxHeapSize=3184533504 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SoftRefLRUPolicyMSPerMB=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
2017-01-10T11:13:39.752-0800: 1.458: [GC (Allocation Failure) 2017-01-10T11:13:39.753-0800: 1.459: [ParNew: 51904K->6464K(58368K), 0.0123836 secs] 51904K->8148K(188096K), 0.0138403 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.416-0800: 2.122: [GC (Allocation Failure) 2017-01-10T11:13:40.416-0800: 2.123: [ParNew: 58368K->6463K(58368K), 0.0193642 secs] 60052K->10934K(188096K), 0.0197870 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
2017-01-10T11:13:40.611-0800: 2.317: [GC (Allocation Failure) 2017-01-10T11:13:40.611-0800: 2.318: [ParNew: 58367K->4378K(58368K), 0.0098514 secs] 62838K->12472K(188096K), 0.0102622 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.762-0800: 2.468: [GC (Allocation Failure) 2017-01-10T11:13:40.762-0800: 2.468: [ParNew: 56282K->6464K(58368K), 0.0067945 secs] 64376K->14714K(188096K), 0.0071342 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.954-0800: 2.661: [GC (Allocation Failure) 2017-01-10T11:13:40.955-0800: 2.661: [ParNew: 58368K->6464K(58368K), 0.0102488 secs] 66618K->17490K(188096K), 0.0108123 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:41.216-0800: 2.922: [GC (Allocation Failure) 2017-01-10T11:13:41.216-0800: 2.922: [ParNew: 58368K->6464K(58368K), 0.0110529 secs] 69394K->21205K(188096K), 0.0114348 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
2017-01-10T11:13:41.412-0800: 3.119: [GC (Allocation Failure) 2017-01-10T11:13:41.413-0800: 3.119: [ParNew: 58368K->5049K(58368K), 0.0073977 secs] 73109K->22139K(188096K), 0.0077819 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
但这也许好得令人难以置信?也许我没有指定正确的 JVM 开关
捕获 >所有< GC,并且确实有一些未记录的事件?
程序不断地 运行ning(并不断地产生小对象)所以看起来
这会填满 eden space,并且会发生内存分配失败,并且
结果会启动 GC。即使我(我认为)对垃圾收集器有一定的了解
概念,我有点困惑。我能想到的唯一可能发生的事情是:1)也许
JOLT 在栈上分配它的小对象(就像在一个大数组中)而不是在堆上?或 2)(更有可能),也许我没有正确指定我的 JVM 开关。这是我用开关所做的:
export MAVEN_OPTS=" -XX:+UseConcMarkSweepGC \
-XX:SoftRefLRUPolicyMSPerMB=1 \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCDateStamps \
-XX:+PrintGCDetails \
-Xloggc:/tmp/gc.log -Xmx2048m "
然后我 运行 将程序作为单元测试,如下所示:
mvn clean test
测试源代码如下。
(如果需要,我可以在 github 上 post 完整的 maven 项目。)谢谢
提前寻求帮助!
import com.bazaarvoice.jolt.Chainr;
import com.bazaarvoice.jolt.JsonUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.math.stat.descriptive.SummaryStatistics;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
public class BenchmarkJoltTest {
static int numThreads = 20;
static int numSecondsToRun = 360 * 1000; // 10 hours
static boolean randomize = true; /* for non-random we will validate that the output always remains constant */
static int hashOfExpectedOutput = 0;
static SummaryStatistics summaryStatistics = new SummaryStatistics();
static String inputJson = "";
static String transformSpecJson = "";
static Random random = new Random();
static Chainr chainr = null;
static AtomicLong completedTransformsCountForInterval = new AtomicLong(0);
static AtomicLong completedTransformsCountGrandTotal = new AtomicLong(0);
//summaryStatistics
@BeforeClass
public static void initialize() throws Exception {
System.out.println("HI HI HO");
inputJson =
String.join( " ", IOUtils.readLines(ClassLoader.getSystemResourceAsStream("input.json")));
transformSpecJson =
String.join( " ", IOUtils.readLines(ClassLoader.getSystemResourceAsStream("spec.json")));
chainr = Chainr.fromSpec(JsonUtils.jsonToList( transformSpecJson ));
/* Warm up: do JIT compilation, load classes and whatever else might need to be done only during startup */
for (int i = 0; i < 100; i++) {
runOneTransform(false);
}
}
@Test
@DisplayName("Benchmark for Jolt Json transforms")
public void runBenchmark() throws Exception {
runTransformsInThreadPool(10 /* threads */);
System.out.println("OK - here");
for (int i = 0; i < numSecondsToRun; i++) {
Thread.sleep(1000); /* 1 second */
double value = completedTransformsCountForInterval.doubleValue();
System.out.println("adding to count: " + value );
summaryStatistics.addValue(value);
completedTransformsCountForInterval.set(0L); // reset counter
if (completedTransformsCountForInterval.get() > 100) {
throw new Exception("how did the counter suddenly jump so high? -- unexpected");
}
}
System.out.println(
"Summary statistics for number of transforms performed per second\n\n" +
summaryStatistics.toString() + "\ntotalCount: "+ completedTransformsCountForInterval);
System.out.println("Grand total of completed operations: " + completedTransformsCountGrandTotal);
System.exit(0);
}
private void runTransformsInThreadPool(final int threadCount) throws InterruptedException, ExecutionException {
Callable<Void> task = new Callable<Void>() {
@Override
public Void call() throws Exception {
while (true) {
runOneTransform(true /* include in official count */);
}
}
};
List<Callable<Void>> tasks = Collections.nCopies(threadCount, task);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (Callable c : tasks) {
System.out.println("submitting " + c);
executorService.submit(c);
System.out.println("finished submitting ");
}
System.out.println("INVOKED all");
}
private static void runOneTransform(boolean includeInOfficialCount /* don't include if in warm-up mode */) throws Exception {
String targetJson = randomize ? getRandomizedJson() : inputJson;
Object transformedJson = chainr.transform(JsonUtils.jsonToObject(targetJson));
String transformedJsonAsString = JsonUtils.toJsonString(transformedJson);
if (hashOfExpectedOutput == 0) {
hashOfExpectedOutput = transformedJsonAsString.hashCode();
}
if (! randomize && hashOfExpectedOutput != transformedJsonAsString.hashCode()) {
throw new Exception("hash codes of transformed strings don't match. multi-threading issue ?");
}
if (includeInOfficialCount) {
completedTransformsCountForInterval.incrementAndGet();
completedTransformsCountGrandTotal.incrementAndGet();
} else {
System.out.println(transformedJsonAsString);
}
}
private static String getRandomizedJson() {
Integer num = random.nextInt() % 10;
inputJson.replaceAll("3", num.toString());
return inputJson;
}
}
日志表明垃圾收集器>><< 运行正在运行。这些行中的每一行都是 ParNew
收集器的 运行。但是,如果我正确地解释日志,它们都是 "new object" space 的集合,而且速度非常快。
如果您的应用程序继续这样 运行,它最终会达到 CMS 收集器需要 运行 收集已从新 space.
您的应用程序似乎没有产生太多负载这一事实不一定值得怀疑。您在 Jolt 文档中找到的内容可能已过时或不准确。 (例如,他们可能一直在错误地概括。)
如果您怀疑您的应用程序无法正常工作,请寻找其他方法来验证结果。
Maybe I did not specify my JVM switches correctly.
低 GC 率是一件>>好<<事情。如果有问题,你应该只看看调整开关。
看起来 runOneTransform
只创建了非常短暂的对象。由于跟踪 GC 的成本取决于活动对象集的大小,而这些对象的寿命不长,这意味着 GC 几乎无事可做。
很可能在这个人工测试用例中,您主要测量的是实际代码执行和分配的成本,而不是垃圾回收的成本。
现实世界的应用程序通常具有较少左偏的生命周期直方图,需要收集器进行更多的跟踪和复制工作。
感谢 Stephen C、@the8472、@Holger 提供的提示。我将总结我从 Jolt 开发人员(特别是 Milo Simpson)那里得到的答案,并 post 和 link 详细信息。
总而言之:Milo 给出的解释是 EscapeAnalysis 正在启动并且 Jolt 正在创建以执行转换的对象正在堆栈上分配。如果我想强制 GC(我不想在生产中这样做;^),他建议设置测试,以便对输出 JSON 的引用跨线程边界。
我不认为我会那样做,因为我们练习的目的是评估 Jolt 的表现......而且它似乎非常可靠。
我从这个练习中学到了很多东西,建议您完整阅读 Milo 的回答。 https://github.com/bazaarvoice/jolt/issues/305#issuecomment-271861868
我正在对一个名为 JOLT 的 JSON t运行sformation 库进行基准测试。图书馆看起来不错,但是 我们想确保它不会对我们的垃圾收集器造成太大压力。 因此,我创建了一个执行以下操作的小程序(转载如下):
- precompiles JSON tranformation scriptlet
- loop for a long time in each of ten threads, doing the following:
- create randomized JSON string input (approx 300 bytes)
- parse input using JOLT and apply tranform
当我们查看我们的垃圾收集器日志时,我们从未看到完整的 GC(即使在 运行ning 一半之后 一天),甚至更可疑的是,唯一报告发生的 GC 发生在第一个 启动程序几分钟,然后几个小时过去了,我们看不到 一个额外的 GC。该程序正在执行数量惊人的 t运行 操作 每秒 .. 并且根据 JOLT 文档:
"The transform process will create and discard a lot of objects, so the garbage collector will have work to do."
所以,我想我应该很高兴我们的 gc 日志即使在一个小时后仍然看起来像这样 运行:
Server VM (25.66-b17) for linux-amd64 JRE (1.8.0_66-b17), built on Oct 6 2015 17:28:34 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
Memory: 4k page, physical 12439584k(11065340k free), swap 12721148k(12721148k free)
CommandLine flags: -XX:InitialHeapSize=199033344 -XX:MaxHeapSize=3184533504 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SoftRefLRUPolicyMSPerMB=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
2017-01-10T11:13:39.752-0800: 1.458: [GC (Allocation Failure) 2017-01-10T11:13:39.753-0800: 1.459: [ParNew: 51904K->6464K(58368K), 0.0123836 secs] 51904K->8148K(188096K), 0.0138403 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.416-0800: 2.122: [GC (Allocation Failure) 2017-01-10T11:13:40.416-0800: 2.123: [ParNew: 58368K->6463K(58368K), 0.0193642 secs] 60052K->10934K(188096K), 0.0197870 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
2017-01-10T11:13:40.611-0800: 2.317: [GC (Allocation Failure) 2017-01-10T11:13:40.611-0800: 2.318: [ParNew: 58367K->4378K(58368K), 0.0098514 secs] 62838K->12472K(188096K), 0.0102622 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.762-0800: 2.468: [GC (Allocation Failure) 2017-01-10T11:13:40.762-0800: 2.468: [ParNew: 56282K->6464K(58368K), 0.0067945 secs] 64376K->14714K(188096K), 0.0071342 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:40.954-0800: 2.661: [GC (Allocation Failure) 2017-01-10T11:13:40.955-0800: 2.661: [ParNew: 58368K->6464K(58368K), 0.0102488 secs] 66618K->17490K(188096K), 0.0108123 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2017-01-10T11:13:41.216-0800: 2.922: [GC (Allocation Failure) 2017-01-10T11:13:41.216-0800: 2.922: [ParNew: 58368K->6464K(58368K), 0.0110529 secs] 69394K->21205K(188096K), 0.0114348 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
2017-01-10T11:13:41.412-0800: 3.119: [GC (Allocation Failure) 2017-01-10T11:13:41.413-0800: 3.119: [ParNew: 58368K->5049K(58368K), 0.0073977 secs] 73109K->22139K(188096K), 0.0077819 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
但这也许好得令人难以置信?也许我没有指定正确的 JVM 开关 捕获 >所有< GC,并且确实有一些未记录的事件?
程序不断地 运行ning(并不断地产生小对象)所以看起来 这会填满 eden space,并且会发生内存分配失败,并且 结果会启动 GC。即使我(我认为)对垃圾收集器有一定的了解 概念,我有点困惑。我能想到的唯一可能发生的事情是:1)也许 JOLT 在栈上分配它的小对象(就像在一个大数组中)而不是在堆上?或 2)(更有可能),也许我没有正确指定我的 JVM 开关。这是我用开关所做的:
export MAVEN_OPTS=" -XX:+UseConcMarkSweepGC \
-XX:SoftRefLRUPolicyMSPerMB=1 \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCDateStamps \
-XX:+PrintGCDetails \
-Xloggc:/tmp/gc.log -Xmx2048m "
然后我 运行 将程序作为单元测试,如下所示:
mvn clean test
测试源代码如下。
(如果需要,我可以在 github 上 post 完整的 maven 项目。)谢谢 提前寻求帮助!
import com.bazaarvoice.jolt.Chainr;
import com.bazaarvoice.jolt.JsonUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.math.stat.descriptive.SummaryStatistics;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
public class BenchmarkJoltTest {
static int numThreads = 20;
static int numSecondsToRun = 360 * 1000; // 10 hours
static boolean randomize = true; /* for non-random we will validate that the output always remains constant */
static int hashOfExpectedOutput = 0;
static SummaryStatistics summaryStatistics = new SummaryStatistics();
static String inputJson = "";
static String transformSpecJson = "";
static Random random = new Random();
static Chainr chainr = null;
static AtomicLong completedTransformsCountForInterval = new AtomicLong(0);
static AtomicLong completedTransformsCountGrandTotal = new AtomicLong(0);
//summaryStatistics
@BeforeClass
public static void initialize() throws Exception {
System.out.println("HI HI HO");
inputJson =
String.join( " ", IOUtils.readLines(ClassLoader.getSystemResourceAsStream("input.json")));
transformSpecJson =
String.join( " ", IOUtils.readLines(ClassLoader.getSystemResourceAsStream("spec.json")));
chainr = Chainr.fromSpec(JsonUtils.jsonToList( transformSpecJson ));
/* Warm up: do JIT compilation, load classes and whatever else might need to be done only during startup */
for (int i = 0; i < 100; i++) {
runOneTransform(false);
}
}
@Test
@DisplayName("Benchmark for Jolt Json transforms")
public void runBenchmark() throws Exception {
runTransformsInThreadPool(10 /* threads */);
System.out.println("OK - here");
for (int i = 0; i < numSecondsToRun; i++) {
Thread.sleep(1000); /* 1 second */
double value = completedTransformsCountForInterval.doubleValue();
System.out.println("adding to count: " + value );
summaryStatistics.addValue(value);
completedTransformsCountForInterval.set(0L); // reset counter
if (completedTransformsCountForInterval.get() > 100) {
throw new Exception("how did the counter suddenly jump so high? -- unexpected");
}
}
System.out.println(
"Summary statistics for number of transforms performed per second\n\n" +
summaryStatistics.toString() + "\ntotalCount: "+ completedTransformsCountForInterval);
System.out.println("Grand total of completed operations: " + completedTransformsCountGrandTotal);
System.exit(0);
}
private void runTransformsInThreadPool(final int threadCount) throws InterruptedException, ExecutionException {
Callable<Void> task = new Callable<Void>() {
@Override
public Void call() throws Exception {
while (true) {
runOneTransform(true /* include in official count */);
}
}
};
List<Callable<Void>> tasks = Collections.nCopies(threadCount, task);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (Callable c : tasks) {
System.out.println("submitting " + c);
executorService.submit(c);
System.out.println("finished submitting ");
}
System.out.println("INVOKED all");
}
private static void runOneTransform(boolean includeInOfficialCount /* don't include if in warm-up mode */) throws Exception {
String targetJson = randomize ? getRandomizedJson() : inputJson;
Object transformedJson = chainr.transform(JsonUtils.jsonToObject(targetJson));
String transformedJsonAsString = JsonUtils.toJsonString(transformedJson);
if (hashOfExpectedOutput == 0) {
hashOfExpectedOutput = transformedJsonAsString.hashCode();
}
if (! randomize && hashOfExpectedOutput != transformedJsonAsString.hashCode()) {
throw new Exception("hash codes of transformed strings don't match. multi-threading issue ?");
}
if (includeInOfficialCount) {
completedTransformsCountForInterval.incrementAndGet();
completedTransformsCountGrandTotal.incrementAndGet();
} else {
System.out.println(transformedJsonAsString);
}
}
private static String getRandomizedJson() {
Integer num = random.nextInt() % 10;
inputJson.replaceAll("3", num.toString());
return inputJson;
}
}
日志表明垃圾收集器>><< 运行正在运行。这些行中的每一行都是 ParNew
收集器的 运行。但是,如果我正确地解释日志,它们都是 "new object" space 的集合,而且速度非常快。
如果您的应用程序继续这样 运行,它最终会达到 CMS 收集器需要 运行 收集已从新 space.
您的应用程序似乎没有产生太多负载这一事实不一定值得怀疑。您在 Jolt 文档中找到的内容可能已过时或不准确。 (例如,他们可能一直在错误地概括。)
如果您怀疑您的应用程序无法正常工作,请寻找其他方法来验证结果。
Maybe I did not specify my JVM switches correctly.
低 GC 率是一件>>好<<事情。如果有问题,你应该只看看调整开关。
看起来 runOneTransform
只创建了非常短暂的对象。由于跟踪 GC 的成本取决于活动对象集的大小,而这些对象的寿命不长,这意味着 GC 几乎无事可做。
很可能在这个人工测试用例中,您主要测量的是实际代码执行和分配的成本,而不是垃圾回收的成本。
现实世界的应用程序通常具有较少左偏的生命周期直方图,需要收集器进行更多的跟踪和复制工作。
感谢 Stephen C、@the8472、@Holger 提供的提示。我将总结我从 Jolt 开发人员(特别是 Milo Simpson)那里得到的答案,并 post 和 link 详细信息。
总而言之:Milo 给出的解释是 EscapeAnalysis 正在启动并且 Jolt 正在创建以执行转换的对象正在堆栈上分配。如果我想强制 GC(我不想在生产中这样做;^),他建议设置测试,以便对输出 JSON 的引用跨线程边界。
我不认为我会那样做,因为我们练习的目的是评估 Jolt 的表现......而且它似乎非常可靠。
我从这个练习中学到了很多东西,建议您完整阅读 Milo 的回答。 https://github.com/bazaarvoice/jolt/issues/305#issuecomment-271861868