java 闭包算法,从Java的视角看闭包以及内存泄漏

1fff678a874def0079dd3b76599864eb.png

本文转载自微信公众号「咸鱼正翻身」,作者MDove。转载本文请联系咸鱼正翻身公众号。

前言

主要聊几个点:

什么是闭包,为什么有的语言无时无刻都在提闭包这个概念(比如:JS)?

Java中有没有闭包?

内存泄漏

正文

无论上是Java还是Kotlin咱们基本都没听说过闭包这个概念的存在。但是如果我们去了解闭包解决的问题,咱们就会明白闭包:这不就是匿名内部类会持有外部对象的引用吗?

一、闭包

两段类似的代码,先看一段Kotlin代码:

val arr = arrayListOf Unit>()

for(indexin0..10) {

arr.add(object : () -> Unit {

override fun invoke() {

print(index)

}

})

}

arr[6].invoke()

输出结果6,没什么异议。但是,有趣的来了,这段代码在JS里:

var arr=[]

for(var i = 0; i<10; i++){

arr[i] = function(){

console.log(i)

}

}

arr

arr[6]()

这里运行是10。(据我前端的同学说,这是一道必考的前端面试题??)

为了方便代码理解,这里针对上述JS代码展开两个JS的规则:

变量提升:

for(var i = 0; i<10; i++)里边的i会进行一个叫做“变量提升”的操作,上述代码实际是这样:

var i

for(i = 0; i<10; i++){}

作用域:

函数体里的console.log(i)为什么能引用到i,是因为JS是按作用域查找变量,如果当前作用域没有这个变量就会向父级查找,以此类推。

有了上边两个点,大家应该就能get到为啥arr6的时候,通过父作用域找到了i,而此时的i = 10。

那么问题来了,JS里边怎么让console.log(i)打印6?答案是:闭包。

var arr=[]

for(var i = 0; i<10;i++){

(function(i){

arr[i]=function(){

console.log(i)

}

})(i)

}

arr[6]()

简单看一下代码发生了什么改动?用一个有一个参数的函数包了一下。每次for循环的时候都调用这个函数并传递一个当前的i进去。

此后对于console.log(i)来说,父级作用域就是包裹的那个函数,而找到的i也就是正确的i。

这就是JS的闭包。咱们再回忆一下Java是不是也是类似的处理方式?

9570090f4e933d329b870084452fc98f.png

做法出奇的相似,这里用了一个名为TestKt$main$1的类包裹了我们的Function。并且构造函数里接收我们需要的i。

所以无论上闭包,还是持有外部对象引用。本质想要解决的问题都是:正确的变量引用。这里还有一个题外话:匿名内部类持有外部引用的时候,为啥要加final?

这里了解了二者的实现原理,咱们再来聊一聊二者都会遇到的潜在问题:内存泄漏。

二、内存泄漏

出现内存泄漏的原因也很简单:

函数内要使用外部变量,那么势必要持有外部变量

而函数的执行时机有可能在外部变量生命周期外执行

为了保证2步骤的正常,那么原本应该被回收的外部变量就不能被回收了,因为函数还在引用。所以外部变量就内存泄漏了

我们来看一个比较常见的代码,在一个UI组件里delay一段时间,然后再拿到这个组件里的某个View做delay之后的事情:

class TestActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_fragment_container)

window.decorView.postDelayed({

Log.d("TEST", findViewById(R.id.container).toString())

}, 3000)

}

}

这段代码至少存在两个相关的问题:

3秒内退出这个Activity,在第3秒时会出现空指针异常。

TestActivity这个实例会被泄漏3秒钟。

这俩个问题的原因都很直接:因为postDelayed的代码块需要调用findViewById,所以隐式的持有了TestActivity实例。而Activity走完onDestroy()内部的View已经被remove了。所以postDelayed的代码块虽然能拿到Activity但是已经find不到View了。

由上述的代码,咱们来客观的思考内存泄漏:

客观的看待内存泄漏

个人观点:内存泄漏不是洪水猛兽。因为我们日常中很多优化手段的本质都会产生内存泄漏。

单例的缓存池

很多时候,内存泄漏并不会产生太大的影响,毕竟大家都没有刻意的针对内存泄漏的场景进行优化过。原因也很简单:我们一般泄漏的内存都很小。

但也有例外,我猜大家多少都听说过一个原则:需要传递Context的时候优先传Application的Context。

很多时候Context的背后是Activity/Fragment等UI组件,这些组件相对来说内存占用相对比较大。比如ImageView,ImageView本身不大,但是它会强引用Bitmap这种极大内存的对象。

如果我们Activity/Fragment中碰巧又强引用这种大内存的对象(比如:ImageView)。此Context一旦泄露就是毁灭级的。

因此一些ImageView为了兜底内存泄漏问题,有如下的优化方案。

override fun onDetachedFromWindow() {

super.onDetachedFromWindow()

recycleBackground(this)

recycleImageView(this)

}

private staticvoid recycleBackground(Viewview) {

if (view==null) {

return;

}

Drawable drawable = view.getBackground();

if (drawable != null) {

drawable.setCallback(null);

view.setBackground(null);

}

}

private staticvoid recycleImageView(ImageView iv) {

if (iv == null) {

return;

}

Drawable drawable = iv.getDrawable();

if (drawable != null) {

drawable.setCallback(null);

iv.setImageDrawable(null);

}

}

如何解决内存泄漏

我们都知道JVM中的垃圾回收一般使用 :根搜索算法。也就是咱们常听到的可行性分析。

一句话理解:当该触发垃圾回收的时候,尝试确定哪些对象已经不再引用,一波将这些对象带走就完事了。(而我们的内存泄漏的本质:该被带走的对象被还活着的对象引用着)

上边说的简单,但是会带来额外的问题:

1. 垃圾的回收不是实时的

极端情况下会频繁触发gc(比如常说的内存抖动)

2. gc时对全部内存进行可达性分析是很耗时的(而出现gc的时候是会stop-the-world,停掉除gc线程外的所有线程)

针对问题1,JVM的配置里是有一些配置,可以更细粒度的控制回收时机。

针对问题2,也就出现了各式各样的垃圾回收器,来优化耗时

堆内存和栈内存

为啥要聊这个话题。主要引出来堆/栈内存的区别。

函数中new出来的变量只要不发生逃逸,都会随栈帧的出入栈来走过自己“华丽的一生”。所以局部变量一般不太需要考虑。

而成员变量都是伴随着类出现。类的实例化是在堆上,堆上内存的“生老病死”是由gc说的算。正常情况下类中成员变量都是强引用,所以这就构成了引用链。只要还挂在GC-Root这条链上,那么就意味着可达。这种case从gc的视角来说这些内存就该活着。

强引用和弱引用

根据上述的分析,其实我们已经明白内存泄漏的根本就是本该寿终正寝的对象,由于错误的强引用,导致“延年益寿”了。

强/弱引用很好理解:

强引用:拥有免死金牌(引用),只要免死金牌不到期,不死不灭

弱引用:如同韭菜,需要割(释放)的时候就被割(释放)了

而这个错误的强引用,在一定情况下可以用弱引用来解决。

解决方案1:弱引用(不推荐)

咱们明确了错误的强引用导致内存泄漏,那我们很自然的想到把强引用改成弱引用:

// 强引用

val ctx = context

// 弱引用

val weakCtx = WeakReference(context)

当触发GC的时候,让GC自己去回收吧。很简单,改造成本也很小。但是存在问题:

弱引用只有触发GC的时候才会释放,因此它没有根本解决存在泄漏的问题,只是一种兜底方案而已。

GC后发生弱引用回收,此时业务get()就是null,有可能不符合业务场景。

解决方案2:切断引用

这一条是正路,从根本上解决问题。

但凡需要注册回调(产生匿名内部类),都要考虑一下这个注册进去的对象,是不是生命周期比隐式持有的对象长?如果是那就存在内存泄漏。

而解决起来也很简单,就是把被长生命周期对象强引用的短生命周期对象在合适的时机置为null即可。

三、LeakCanary原理

在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA(Headless Android Heap Analyzer)这个开源库去分析dump之后的heap内存。

ReferenceQueque:当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中,待 ReferenceQueue 处理。当 GC 过后对象一直不被加入 ReferenceQueue,说明它可能存在内存泄漏。

@Synchronized private fun moveToRetained(key: String) {

removeWeaklyReachableObjects()

val retainedRef = watchedObjects[key]

if (retainedRef != null) {

retainedRef.retainedUptimeMillis = clock.uptimeMillis()

// 主动gc/判断是否存在泄漏->dump内存

onObjectRetainedListeners.forEach { it.onObjectRetained() }

}

}

private fun removeWeaklyReachableObjects() {

var ref: KeyedWeakReference?

do {

ref = queue.poll() asKeyedWeakReference?

if (ref != null) {

watchedObjects.remove(ref.key)

}

} while (ref != null)

}

最新的库已经不用HAHA了,新搞了一套。有兴趣的同学可以github自行搜索

结语

内存泄漏不是洪水猛兽,但也不应该视而不见。理论上来说不应该写出存在内存泄漏的代码,但是如果真的需要,可以问自己两个问题:

这里内存泄漏是必须的吗?

这里内存泄漏的对象大吗?

如果你的答案是true,那么泄漏也不算什么大事。

【编辑推荐】

【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: B'Warshall算法是一种用于寻找所有节点对之间最短路径的动态规划算法。在Java中实现传递闭包可以使用二维数组来存储图的邻接矩阵,根据Warshall算法的思路,对每个节点进行遍历,利用传递闭包的性质更新邻接矩阵中的元素,最终得到所有节点对之间的最短路径。' ### 回答2: Warshall算法是一种计算传递闭包算法,其基本思想是利用一个矩阵记录图中各个点之间的关系,并通过三重循环逐步更新矩阵,最终得到传递闭包矩阵。下面我们来详细介绍如何使用Java实现Warshall算法求传递闭包。 首先,我们需要定义一个表示图的二维矩阵,并将图中相邻点的位置值设置为1,不相邻点的位置值设置为0。如下所示: int[][] matrix = { {0, 1, 0, 1}, {0, 0, 1, 0}, {0, 0, 0, 1}, {0, 0, 0, 0} }; 接着,我们定义一个n*n的布尔类型矩阵,表示传递闭包。这个矩阵最初的值与图的二维矩阵相同,如下所示: boolean[][] closure = { {false, true, false, true}, {false, false, true, false}, {false, false, false, true}, {false, false, false, false} }; 接下来,我们使用Warshall算法计算出传递闭包。具体做法是遍历矩阵中的每一个位置,如果存在一个点k和点i之间连通,并且点i和点j之间连通,则表示点k和点j之间也连通,于是将closure[k][j]赋值为true。最终,得到的closure数组就是图的传递闭包。代码如下: for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (matrix[i][k] == 1 && matrix[k][j] == 1) { closure[i][j] = true; } } } } 最后,我们打印出传递闭包矩阵,即可得到图的传递闭包。代码如下: for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { System.out.print(closure[i][j] + " "); } System.out.println(); } 以上就是使用Java实现Warshall算法求传递闭包的方法。通过Warshall算法,可以快速有效地计算图的传递闭包,避免了一次次深搜或广搜的繁琐工作,提高了程序的效率。 ### 回答3: Warshall算法是求解传递闭包的经典算法之一。在任何有向图中,一些节点之间可能存在传递关系,例如,假设节点i可以到达节点j,节点j可以到达节点k,则节点i也可以到达节点k。传递关系意味着传递闭包,也就是说,在传递闭包中,如果节点i可以到达节点j,节点j可以到达节点k,则节点i也可以到达节点k。 Warshall算法的实现步骤如下: 1. 声明一个二维数组,表示有向图的矩阵形式,其中矩阵的值为1表示存在从i到j的边,为0表示不存在。 2. 嵌套三层循环,依次枚举i、j和k,如果矩阵中存在从i到j和从j到k的边,则将矩阵中从i到k的边置为1。 3. 最终得到的矩阵就是传递闭包。 下面是Java代码实现: public class Warshall { public static int[][] transitiveClosure(int[][] graph) { int n = graph.length; int[][] closure = new int[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { closure[i][j] = graph[i][j]; } } for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { closure[i][j] = (closure[i][j] != 0) || ((closure[i][k] != 0) && (closure[k][j] != 0)) ? 1 : 0; } } } return closure; } } 在这个实现中,我们首先复制了输入的graph数组,然后使用三层循环来更新传递闭包。在第三层循环中,我们检查从i到j和从j到k是否存在边,如果存在,则将从i到k的边标记为1。 最后,我们将传递闭包返回给调用者。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值