java - 为什么这个 Java 方法会泄漏——为什么内联它会修复泄漏?
问题描述
我写了一个最小的有些惰性(int
)序列类,GarbageTest.java,作为一个实验,看看我是否可以在 Java 中处理非常长的惰性序列,就像在 Clojure 中一样。
给定一个naturals()
返回惰性、无限、自然数序列的方法;drop(n,sequence)
删除 的第一个n
元素sequence
并返回其余部分的方法sequence
;和一个nth(n,sequence)
简单返回的方法: drop(n, lazySeq).head()
,我写了两个测试:
static int N = (int)1e6;
// succeeds @ N = (int)1e8 with java -Xmx10m
@Test
public void dropTest() {
assertThat( drop(N, naturals()).head(), is(N+1));
}
// fails with OutOfMemoryError @ N = (int)1e6 with java -Xmx10m
@Test
public void nthTest() {
assertThat( nth(N, naturals()), is(N+1));
}
请注意,主体dropTest()
是通过复制主体nthTest()
然后在调用中调用 IntelliJ 的“内联”重构来生成的nth(N, naturals())
。所以在我看来, 的行为dropTest()
应该与 的行为相同nthTest()
。
但并不完全相同!dropTest()
以 N 高达 1e8 运行完成,而 N 小至 1e6 则nthTest()
失败OutOfMemoryError
。
我避免了内部课程。而且我已经尝试了我的代码的变体ClearingArgsGarbageTest.java,它在调用其他方法之前将方法参数归零。我已经应用了 YourKit 分析器。我看过字节码。我只是找不到导致nthTest()
失败的泄漏。
“漏”在哪里?为什么nthTest()
有泄漏而dropTest()
没有?
以下是GarbageTest.java中的其余代码,以防您不想点击进入 Github 项目:
/**
* a not-perfectly-lazy lazy sequence of ints. see LazierGarbageTest for a lazier one
*/
static class LazyishSeq {
final int head;
volatile Supplier<LazyishSeq> tailThunk;
LazyishSeq tailValue;
LazyishSeq(final int head, final Supplier<LazyishSeq> tailThunk) {
this.head = head;
this.tailThunk = tailThunk;
tailValue = null;
}
int head() {
return head;
}
LazyishSeq tail() {
if (null != tailThunk)
synchronized(this) {
if (null != tailThunk) {
tailValue = tailThunk.get();
tailThunk = null;
}
}
return tailValue;
}
}
static class Incrementing implements Supplier<LazyishSeq> {
final int seed;
private Incrementing(final int seed) { this.seed = seed;}
public static LazyishSeq createSequence(final int n) {
return new LazyishSeq( n, new Incrementing(n+1));
}
@Override
public LazyishSeq get() {
return createSequence(seed);
}
}
static LazyishSeq naturals() {
return Incrementing.createSequence(1);
}
static LazyishSeq drop(
final int n,
final LazyishSeq lazySeqArg) {
LazyishSeq lazySeq = lazySeqArg;
for( int i = n; i > 0 && null != lazySeq; i -= 1) {
lazySeq = lazySeq.tail();
}
return lazySeq;
}
static int nth(final int n, final LazyishSeq lazySeq) {
return drop(n, lazySeq).head();
}
解决方案
在你的方法中
static int nth(final int n, final LazyishSeq lazySeq) {
return drop(n, lazySeq).head();
}
参数变量lazySeq
在整个drop
操作期间保存对序列第一个元素的引用。这可以防止整个序列被垃圾收集。
与...对比
public void dropTest() {
assertThat( drop(N, naturals()).head(), is(N+1));
}
序列的第一个元素由naturals()
的调用返回并直接传递给drop
,因此从操作数堆栈中删除并且在 的执行期间不存在drop
。
您尝试将参数变量设置为null
,即
static int nth(final int n, /*final*/ LazyishSeq lazySeqArg) {
final LazyishSeq lazySeqLocal = lazySeqArg;
lazySeqArg = null;
return drop(n,lazySeqLocal).head();
}
没有帮助,就像现在一样,lazySeqArg
变量 is null
,但lazySeqLocal
保存对第一个元素的引用。
局部变量通常不会阻止垃圾收集,允许收集其他未使用的对象,但这并不意味着特定实现能够做到这一点。
在 HotSpot JVM 的情况下,只有优化的代码才能摆脱这些未使用的引用。但这里,nth
并不是热点,因为重重的事情发生在drop
方法之内。
这就是为什么该drop
方法没有出现相同问题的原因,尽管它还包含对其参数变量中第一个元素的引用。该drop
方法包含执行实际工作的循环,因此很可能会被 JVM 优化,这可能会导致它消除未使用的变量,从而允许收集已处理的序列部分。
有许多因素可能会影响 JVM 的优化。除了代码的不同形状之外,似乎在未优化阶段快速分配内存也可能会降低优化器的改进。确实,当我使用-Xcompile
, 完全禁止解释执行时,两个变体都成功运行,甚至int N = (int)1e9
不再有问题。当然,强制编译会增加启动时间。
我不得不承认我不明白为什么混合模式的表现会差很多,我会进一步调查。但通常,您必须意识到垃圾收集器的效率取决于实现,因此在一个环境中收集的对象可能会留在另一个环境中的内存中。
推荐阅读
- text - 在 Crowdin 上翻译的最佳结构文本
- c - 高斯消除程序不能并行工作 - OpenCL
- vb.net - SSRS Distinct LookupSet 问题
- javascript - 使用 ReactJS 和 Flux 通过 URL :id 填充表单
- javascript - 如何根据 URL 字符串进行查找和警告 Javascript 对象
- google-cloud-platform - 将 Dataflow 和 Cloud Composer python 代码保存在哪里?
- websocket - 如何让camel websocket客户端监听服务器端口?没有通过骆驼客户端路由中的url连接到服务器端口
- .net-traceprocessing - 是否可以在 macOS 上使用 dotnet 跟踪库?
- qt - Qml 在运行时可以工作,但不会在 Qt 设计器中加载照片
- html - HTML 对话框不会在 Google 脚本上的共享电子表格中关闭