Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

使用Chrome开发者工具分析内存泄漏

25
Jul
2016

使用Chrome开发者工具分析内存泄漏

By Alex
/ in Web
/ tags 性能剖析
0 Comments
基础知识
内存问题及其表现

从用户角度来看,Web应用内存问题可以表现为以下几种形式:

问题形式 症状
内存泄漏

页面的性能随着运行时间的增加越来越差,这是因为页面消耗越来越多的内存

内存消耗量和性能呈负相关的原因包括:

  1. 内存消耗量越大,GC运行越频繁、耗时。而GC运行会导致JS引擎暂停执行
  2. 内存消耗量越大,某些遍历性质的操作就越耗时
内存暴涨

页面性能一直很差,这是因为页面使用大量的内存,超过页面速度优化的限度

所谓“大量”并没有量化标准,主要取决于客户端的硬件资源

频繁GC 页面延迟或者卡顿。这时因为GC时脚本执行被暂停

这三种表现形式并不是孤立的。缓慢的内存泄漏最终会导致内存的大量使用,内存泄漏/暴涨会导致GC消耗更多的时间以及更频繁的运作。

本文主要探讨内存泄漏问题。

内存泄漏

所谓内存泄漏,是指应用程序不再需要的内存,由于某种原因没有归还给操作系统。

JavaScript是一种基于垃圾回收机制(GCed)的语言,垃圾回收器可以辅助内存的回收:它检查某一片内存是否从应用其它部分可达,以确定该内存是否可以被安全回收。GCed语言中的内存泄漏主要由多余引用(unwanted references)导致。所谓多余引用,是从活动GC Root直接/间接指向一片不再需要的内存的引用。

尽管现代JS引擎能够在一些情况下自动执行垃圾回收,由于代码逻辑错误导致的内存泄漏仍然存在,特别是在单页面应用/Ajax成为主流的今天,长时间运行的Web页面往往因此渐渐变慢甚至崩溃。

常见内存泄漏原因

“多余引用”在Web应用的场景下可以进一步的细分。下表列出常见的情形:

原因 说明
错误的使用全局变量

在JavaScript中不通过 var 关键字声明,而直接使用的变量是全局变量,这类变量本质上是 window 对象的属性。

全局变量(指向的内存)直到页面刷新之前,不会自动回收。因此如果使用全局变量保存大量数据,务必在不再需要这些数据时,手工设置全局变量的值为 null ,解除引用

忘记清理定时器

setInterval() 函数经常被使用,但是忘记清理。考虑下面的代码:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var resource = getData();
setInterval(function() {
    var node = document.getElementById('node');
    // 某个时间点之后node可能不再存在,因此该定时器什么也不做
    if(node) node.innerHTML = JSON.stringify(someResource));
}, 1000);
// 但是,定时器函数没有被销毁,而它引用resource这个数据(可能是一大片内存),这就造成了潜在的内存泄漏
 
// 因此,应当记住,定时器不需要的时候,手工销毁它
var handle = setInterval(NoOp);
clearInterval(handle); //执行清理
忘记清理回调函数

某些JS库则提供基于回调的模式实现,例如观察者模式

你应当在不需要时,明确的移除观察者(Listener函数),实例代码如下:

JavaScript
1
2
3
4
5
6
7
8
9
10
var e = document.getElementById( 'button' );
function clickHandler( event ) {
    e.innerHtml = 'text';
}
e.addEventListener( 'click', clickHandler );
 
// 先移除观察者
e.removeEventListener( 'click', clickHandler );
// 然后再移除DOM节点
e.parentNode.removeChild( e );

对于某些不能管理循环引用的浏览器(例如IE6,它不能检测DOM节点和JS代码之间的循环引用)来说,明确移除步骤非常重要。现代浏览器一般都能在被观察对象不可达时自动清理对应的观察者,尽管如此,明确移除观察者仍然是很好的实践

JS库,例如jQuery,会在移除DOM节点时自动完成上述“明确移除”

忘记清理DOM节点的引用

为了操作方便,有时候你会在数据结构中引用DOM节点。 这会导致DOM节点有两个in引用,一个来自DOM树,一个来自你的数据结构。当DOM节点不再需要时,你需要确保这两个in引用都不可达

特别需要注意的是:子DOM节点不可回收的时候,其祖先节点亦不可回收

Chrome任务管理器

按快捷键Shift + ESC,可以打开Chrome任务管理器(Task Manager)。通过持续的观察任务管理器,你可以查看每个页面的内存用量的变化,进而获得可能的内存泄漏提示。常用输出列如下:

列 说明
Task 显示浏览器主进程,或者扩展、页面、插件的名称
Memory

Native内存用量,DOM节点存放在Native内存中,如果此列持续增加,提示存在无法释放的DOM节点

只有DOM节点从页面DOM树中移除,同时没有JS变量引用之,DOM节点才能够被释放。已经从DOM中移除,却仍然被JS代码引用的DOM节点,称为Detached节点,这种节点是引发内存泄漏的常见原因

注意:Chrome开发者工具会导致被调试页面占用的Native内存大量增加。在启用堆时间线记录时,甚至可以增大数倍。关闭开发者工具增大的内存会自动释放

JavaScript Memory

JS堆内存用量,该列显示两个值,其中括号中的值(live)指示可达对象占用的内存。如果live值持续增加,提示存在无法释放的JS对象

结合Windows任务管理器观察,发现Windows任务管理器中chrome.exe进程的内存(私有工作集)总是和上面的Memory相同。因此JavaScript Memory应该是Memory的一部分,Memory是页面占用的总内存

CPU 页面的CPU占用率
Network 页面的网络通信速率
使用DevTools定位内存泄漏

Chrome任务管理器只能给出一些粗略的提示。实际工作中我们通常使用Chrome Developer Tools来发现并定位内存泄漏。Dev Tools由9个面板组成,其中Timeline、Profiles有助于内存分析。

Timeline面板

通过该面板可以直观的看到不正常的内存使用,你可以使用它获得对问题的初步认识。

切换到Timeline面板,然后勾选Memory,点击Selection_003可以启动时间线记录,点击弹窗的Finish按钮则停止记录。注意,在启动、停止前,你应该点击collect-garbage执行强制垃圾回收。时间线记录示例如下:

dev-tools-timeline

在这个示例中,随着时间的推进,周期性的内存用量跳涨而之后不能收缩到原有水平、Nodes、Listeners计数的持续增加,这些都是明显的内存泄漏信号。

Profiles面板

该面板是定位内存泄漏的关键,使用它你可以:

  1. 获取内存的快照,分析JS对象、DOM节点如何占用内存。支持比对两个快照,发现内存使用情况的变化
  2. 录制内存分配随时间的变化,可用于隔离出内存泄漏。
堆快照剖析

切换到Profiles面板,点选Take Heap Snapsot,然后点击Task Snapshot按钮,即可获得一张实时的堆快照。注意:获取快照前,会自动执行垃圾回收。快照示例如下:

dev-tools-profile-heapsnapshot

点击左上角的下拉菜单,可以在几个视图之间切换:

视图 说明
Summary

在上半部分的对象列表子面板中,显示获取快照的那一时刻,各类型对象总体分配情况,数据项包括:

  1. Constructor:显示对象类型(以其构造函数名称表示) ⇨ 对象实例 ⇨ 对象属性...树列出当前驻留对象。如果一个条目的背景为黄色,表示它可以直接通过JavaScript代码访问到。闭包引用的对象,可能是无法直接访问的,因此不会显示黄色背景
  2. Distance:距离GC Root的距离
  3. Objects Count:此类对象的数量
  4. Shallow Size:此类对象的总大小,注意对象大小是指变量引用的目标内存的大小。通常情况下只有数组、字符串可能有很大的Shallow Size,字符串常常会将其主体内容存放在Renderer内存,仅仅在JS Heap中暴露一个很小的Wrapper
  5. Retained Size:Shallow Size的总大小 + 因为此类对象而驻留(因为被此类对象引用,无法释放)的其它对象的总大小。对于没有out引用的对象,例如一个数字或者字符串类型的对象属性,其Shallow Size = Retained Size

你可以点击一个对象类型前面的小箭头(或者按Right方向键)展开,查看此类型对象的实例,注意实例后面会跟着灰色的@id,id是对象的唯一标识符。进一步展开,可以看到对象实例的属性

选中一个对象的实例/对象实例属性,可以在下半部分的Retainers子面板中查看选中条目的引用链,这个链从直接引用选中条目的那个对象开始,逐层上溯

点选顶部工具栏All objects右侧的小箭头,你可以选择查看所有对象,或者在任意两个快照之间分配的对象

Comparison 对比两个快照,查看其分配内存大小、对象个数的比值
Containment 已一系列根对象为起点,显示其容纳对象的树
Statistics 使用饼图来展现各大类对象的内存用量比例
定位内存泄漏的流程

定位内存泄漏需要一些技巧,但是一般都是从快照的Summary视图开始分析。一个常用的工作流程是所谓三快照(three snapshot)技术:

  1. 执行必要的预热操作,确保那些需要长期驻留的对象初始化完毕
  2. 获取一个快照
  3. 执行可能导致内存泄漏的操作
  4. 获取一个快照
  5. 再次执行你觉得可能导致内存泄漏的操作
  6. 再次获取一个快照
  7. 选择最后一个快照,在列表顶部选择:Selection_002
  8. 这样,你就可以看到那些在快照1-快照2这个时间段内分配的,到目前为止仍然驻留在内存中的那些对象。这些对象可能是导致内存泄漏的对象
  9. 展开对象类型节点,选中一个对象,这时下面的Retainers面板会显示该对象的驻留树——即该对象是如何被其它对象引用的

三快照技术有效的依据是:在应用经过预热(warm-up)阶段之后,分配的新对象一般都是由(定时器/用户)操作触发的、朝生暮死的对象。这些本该朝生暮死的对象如果一直驻留,意味着存在内存泄漏。

堆分配时间线剖析

切换到Profiles面板,点选Record Heap Allocations,然后点击Start按钮,即可录制内存分配沿着时间线的变化情况——堆分配时间线(Heap Timelines)。点击stop-recording按钮可以停止录制,并生成一个快照。

注意:时间线对象列表子面板,仅仅包含在时间线结尾时刻仍然存活的对象。

堆分配时间线的优势在于,你可以任意选择大小的时间区间,查看区间内发生的内存分配,有利于隔离出内存泄漏。时间线示例如下:

heap-timeline

截图中,最上面的是时间轴,每一个蓝色/灰色的柱子表示一系列相关的内存分配。柱子的高度表示分配内存的大小,蓝色部分表示到结束录制的那个时间点为止,仍然驻留的内存大小,灰色部分表示已经被回收的内存大小。

中间的对象列表面板,显示选中的时间区间内的内存分配,按类型分组。展开一个类型节点,可以看到仍然存活的实例。

下面是Retainers面板,可以显示某个存活实例的引用链。例如上图中当前选中的字符串实例被window.arr[18]引用。

对象类型说明

在对象列表子面板的Constructor列、Retainers子面板的Object列,都有以构造器表示的对象类型,这些类型的说明如下:

对象类型 说明
(closure) 闭包,这些闭包引用了上下文对象
(compiled code) 被Chrome编译过的JavaScript代码,引擎内部使用,我们无法控制
(array) 引擎内部使用的数组对象
Array JavaScript数组,这些数组常常包含大量的数据
Object 简单(plain old)JavaScript对象
system / Context 调用一个函数所需要的潜在对象,例如闭包使用的实际数据
system 引擎内部使用的数据
(shared function info) 引擎内部使用的函数共享信息
HTML***Element 一个瘦包装器,引用位于Native内存中的DOM对象
示例:Detached DOM树泄漏

这里我们举一个利用堆快照剖析DOM树泄漏的例子。考虑下面的代码:

XHTML
1
2
3
4
5
6
7
8
9
10
11
12
<button id="btn">Create</button>
<script type="text/javascript">
    var detachedTree;
    document.getElementById('btn').addEventListener('click', function () {
        var div = document.createElement('div');
        for (var i = 0; i < 10; i++) {
            var span = document.createElement('span');
            div.appendChild(span);
        }
        detachedTree = div;
    });
</script>

点击Create按钮后,监听器会创建一个div元素,其内部包含10个span元素。由于div元素尚未添加到页面DOM树中且被全局变量detachedTree引用,因此它属于Detached节点。

我们在点击Create按钮后获取一个快照,在Summary视图的Class filter中搜索Detached,可以看到如下结果:Selection_001

在对象列表面板中,黄色背景表示Detached DOM子树的根节点,这类节点被JavaScript代码直接引用;红色背景(非直接引用)则表示Detached DOM子树的分支/叶子节点,没有被JavaScript代码直接引用。

在上图中点选一个对象,下面的Retainers面板会显示该对象的引用链:

detached-tree-retainers

可以看到,引用链从直接引用对象的变量开始,逆向追溯。引用链上的每个元素使用如下语法描述:

1
2
3
4
5
6
# name 可以是对象属性名称、native(表示该引用是本地代码)、[数组索引]
# ObjectType 指明对象的类型
# @id 指明对象的唯一标识
name1 in ObjectType1 @id1
  name2 in ObjectType2 @id2
  # ObjectType2类型的对象id2的name2属性指向ObjectType1类型的对象id1

 对于上图左右两个引用链,应当这样解读:

  1. 对于HTMLDivElement,window的detachedTree属性(就是我们声明的全局变量)直接引用之
  2. 对于HTMLSpanElement,window的detachedTree属性引用了HTMLDivElement ⇨ HTMLDivElement的native属性引用了DOM子树数组  ⇨ DOM子树数组的第1个元素引用了HTMLSpanElement

在追踪DOM树泄漏时,可以不关注红色背景节点,而仅追踪黄色背景节点被哪些JavaScript变量使用,然后清除多余引用。

实战:机房实景内存泄漏问题
背景简介

“机房实景”是某系统中的一个模块,它以图形化的方式展示数据中心的各种设备:包括服务器、网络设备、监控设备。设备及其状态基于位图、矢量图、文本等形式展示。如果数据中心规模较大,可以为其设计多张实景图,并在这些实景图之间轮巡(即定期切换显示),以了解数据中心的网络和环境状态。

从技术角度来说,该模块以Draw 2D Touch 5.3.4为基础,而后者的底层框架则是Raphael以及jQuery。

故障现象

在测试阶段,我们发现长时间运行机房实景模块后,浏览器占用的内存不断上涨。特别是启用实景图轮询的情况下,数小时后内存占用超过1G,最终浏览器会崩溃。

查看任务管理器

通过Chrome任务管理器查看:

  1. 刚进入机房实景模块时:Native内存占用80MB,JS内存占用20MB
  2. 10分钟后,强制GC后:Native内存占用110MB,JS内存占用45MB
  3. 20分钟后,强制GC后:Native内存占用130MB,JS内存占用64MB

这组数据说明机房实景模块很可能存在内存泄漏问题。

查看堆分配时间线

重新进入机房实景模块,等待模块预热完毕(机房实景模块在启动时需要加载所有实景图的UI元素数据,并在内存中创建相应的数据结构),启用轮巡,在Profiles面板中启动堆时间线记录,持续十分钟,执行GC后停止记录,结果如下:

Selection_001

可以看到,大尺寸内存分配,周期非常稳定,这恰恰是轮巡切换实景图的周期。这些内存分配的大部分(蓝色)始终无法回收,说明实景图切换会导致相当严重的内存泄漏。为何实景图切换会导致内存泄漏?这很可能是大量界面元素的复杂引用关系没有被正确的清理,后面我们会证实这一点。

重新进入机房实景模块,等待模块预热完毕,禁用轮巡, 再次启动堆时间线记录,持续十分钟,执行GC后停止记录,结果如下:

Selection_002

可以看到,在禁用轮巡的情况下,只有2分钟左右存在一次可能的JavaScript内存泄漏,其它时间分配的内存都大多数被回收了(灰色)。

禁用轮巡时的堆时间线分析

首先看看禁用轮巡时,那唯一高耸的蓝色柱子是怎么回事,缩小区间,查看对象列表面板:

Selection_003

这个所谓的system类型,是浏览器内部行为导致的内存分配,和应用程序没有直接关系。因此我们可以认为它不是内存泄漏,并进一步推断:禁用轮巡时机房实景模块没有明显的JavaScript内存泄漏。

启用轮巡时的堆时间线分析
概览

我们着重分析启用轮巡时那一系列蓝色柱子,这些柱子高度常常以M计,再多的系统内存也经不起这样的泄漏。

缩小区间,选择第一个柱子,设置对象列表面板按Retained Size降序排列,Retainers面板按Distance排列,结果如下:

timeline-polling-enabled

这样排序的根据是:

  1. Retained Size大的对象,可能造成的内存泄漏量也大
  2. 离目标对象距离一样的多个引用引用,其Distance值越小(即离GC Root越近)越可能是内存泄漏的根源。上图的cache、159两个引用前者更有可能导致了内存泄漏

注意第一个驻留对象Object@1483747,其导致1870.752KB的内存无法释放;第二个对象Object@1483541,其导致1870.556KB内存无法释放。然后,所有Object总计才导致1873.092KB内存无法释放,这是怎么回事呢?

其实展开Object@1483747你就可以看到原因何在——Object@1483747的events字段恰恰引用了Object@1483541:

retained-size-issue-1

在任何时候,你都可以按Esc调出Console。选中对象列表面板或者Retainers面板中一条数据后,在Console中输入 $0 可以打印该条数据对应的对象(对于Retainers是in 后面的那个的对象,而非in前面属性名所指向的对象)的详细信息。你也可以悬停鼠标在一条数据上,相应对象的详细信息也会自动显示。

分析Object@8364337

我们选取Retained Size较大的一个Object进行分析,显示其详细信息:

Object@8364337

可以看到这是一个简单对象,有events、handle两个属性。你会注意到events属性的Retained Size远远大于events的子属性Retained之和,这一现象我也没弄清楚是怎么回事。

查看Retainers面板,可以发现jQuery.cache引用了Object@8364337,而jQuery.cache导致的内存驻留接近28M。看起来这个cache在占据大量内存,它会不会随着时间不停变大呢?继续启用轮巡运行机房实景模块15分钟,证实了这一猜测,cache的Retained Size膨胀到120MB:

jquery.cache.ins

jQuery.cache为何膨胀

上节我们已经确认jQuery.cache存在大量的内存泄漏,那么这个cache是做什么用的呢,为何会无限制的膨胀呢?

我们看看Object@8364337是如何分配,并加入到cache中的,切换Retainers到Allocation stack,查看该对象被创建时的调用栈:

Selection_001

(注意:默认不会录制内存分配时的栈信息,需要在开发者工具设置 ⇨ General ⇨ Profiler中启用。)

有了这个调用栈,即使你不熟悉实景图模块,也能够下手进行调试了。通过单步跟踪,发现大概的程序逻辑如下:

  1. Ext.define.onTick其实一个定时器,每过一定时间它就会调用mm.takeTurnTask.run()方法,即执行轮巡,切换实景图。切换是通过调用MapManager.switchTo()实现的
  2. Ext.define.switchTo:轮巡定时器触发,切换实景图
    map-run.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Ext.define( 'kssi.map.run.MapManager', {
        switchTo: function ( mapId ) {
            var me = this;
            var curMap = this.getCurMap();
            // 只在目标实景图不是当前实景图的情况下才切换
            if ( curMap && mapId == curMap.getId() ) {
                return;
            }
            me.show( mapId );
        },
        show: function ( mapId ) {
            var me = this;
            // 缓存当前实景图
            me.cache( me.getCurMap() );
            // 创建新的需要切换到的实景图
            me.createMapComp( mapId );  // ⇨
            me.updateMapView();
        }
    });
  3. Ext.define.createMapComp:创建新的实景图UI包装器组件,在创建前总是会尝试移除原先的组件:
    map-run.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Ext.define( 'kssi.map.run.MapManager', {
        createMapComp: function ( mapId ) {
            var me = this;
            if ( me.currMapComp ) {
                // 创建新实景图之前,移除老的实景图
                me.mapCntr.remove( me.currMapComp, true );
                me.currMapComp = null;
            }
            // 创建新的实景图并添加到容器
            var mapComp = Ext.create( 'kssi.map.design.MapComponent', mapData );
            me.afterMapCompCreated( mapComp );
            me.mapCntr.add( mapComp );
        }
    });
  4. Map$d2d是实景图UI组件,基于Draw2D的实现。在其重绘(repaint)函数中,会创建Draw2D画布对象,并调用其初始化方法:
    map-d2d.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
        extend: 'com.kingsmartsi.secmon.map.domain.Map',
        canvas: null,
        repaint: function () {
            var map = this;
            var items = map.painted ? map.marshalItems() : map.items;
            map.clear();
            map.dom.innerHTML = '';
            // 就是把当前ExtJS对应的DOM元素传递给Draw2d画布的构造函数
            map.canvas = new draw2d.Canvas( map.dom.id );  // ⇨
        }
    }
  5. Draw2D画布类遵循Class.js的类继承模拟机制,其构造函数的职责实质上由初始化方法init()承担
    draw2d-5.3.4/Class.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    (function () {
        // 声明一个全局变量
        this.Class = function () {
        };
     
        Class.extend = function ( prop ) {
            // ...
            function Class() {
                // 实际的构造了逻辑是委托给init方法完成的
               if ( !initializing && this.init )
                    this.init.apply( this, arguments );
            }
            // ...
            return Class;
        };
    })();

     因此,repaint()传递给new draw2d.Canvas()的map.dom.id,会直接转给init(),作为画布的容器元素

    draw2d-5.3.4/draw2d.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    draw2d.Canvas = Class.extend(
        {
            init: function ( canvasId, width, height ) {
     
                this.canvasId = canvasId;
                // Map$d2d组件对应的DOM元素、Draw2D画布的html属性指向的DOM元素是同一个,只不过后者是一个jQuery元素封装
                this.html = $( "#" + canvasId );  // canvasId  ==  Map$d2d.dom.id
                // 构建Raphael画布对象
                if ( typeof height !== "undefined" ) {
                    this.paper = Raphael( canvasId, width, height );
                }
                else {
                    this.paper = Raphael( canvasId, this.getWidth(), this.getHeight() );
                }
                // 对容器元素进行事件绑定
                this.html.bind( "mouseup touchend", function ( event ) { // ⇨
                } );
                // bind()方法只是on()的一个代理,可以认为两者一回事
                this.html.on( "dblclick", function ( event ) { // ⇨
                } );
                // 更多事件绑定...
            }
    }

    可以看到Draw2D画布在初始化时,调用jQuery的bind/on进行事件绑定。我们开始接近jQuery.cache膨胀的源头了

  6. jQuery在进行事件绑定时,实际上是调用jQuery.event对象的add()方法:
    draw2d-5.3.4/jquery-1.10.2.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    jQuery.fn.extend(
            {
                on: function ( types, selector, data, fn, /*INTERNAL*/ one ) {
                    // 遍历jQuery元素集合(这里只有一个id为canvasId的DIV元素),分别进行添加事件
                    return this.each( function () {
                        jQuery.event.add( this, types, fn, data, selector ); // ⇨
                    } );
                }
            }
    );
  7. jQuery.event.add在一开始就会调用_data()方法:
    draw2d-5.3.4/jquery-1.10.2.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    jQuery.event = {
        add: function( elem, types, handler, data, selector ) {
            // 这个_data()其实是从缓存中拿数据,下面可以看到这一点
            var elemData = jQuery._data( elem );  // ⇨
            if ( !elemData ) return;
        }
    };
     
    jQuery.extend({
        _data: function( elem, name, data ) {
            return internalData( elem, name, data, true );  // ⇨
        }
    });
  8. 而_data()则是internalData的简单代理,后者会初始化元素缓存:
    draw2d-5.3.4/jquery-1.10.2.js
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function internalData( elem, name, data, pvt /* Internal Use Only */ ) {
        // 对于元素节点,使用jQuery.cache,也就是就是我们的泄漏对象
        var cache = isNode ? jQuery.cache : elem;
        // 分配缓存key
        if ( !id ) {
            // Only DOM nodes need a new unique ID for each element since their data
            // ends up in the global cache
            if ( isNode ) {
                id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++;
            }
            else {
                id = internalKey;
            }
        }
        // 如果尚未缓存,为元素建立缓存条目
        if ( !cache[ id ] ) {
            // Avoid exposing jQuery metadata on plain JS objects when the object
            // is serialized using JSON.stringify
            cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
        }
        thisCache = cache[ id ];
        // 缓存一条数据,以name为key,data为value
        if ( data !== undefined ) {
            thisCache[ jQuery.camelCase( name ) ] = data;
        }
        // ...
        return ret;
    }

从上面的分析可以看到,通过jQuery来注册事件监听器时,会创建目标元素的缓存,从而导致jQuery.cache膨胀。

jQuery.cache如何收缩

jQuery的设计中,必然包含了控制jQuery.cache大小的合理逻辑,否则缓存可能会不断膨胀,最终导致内存耗尽。

很多缓存系统使用注入LRU之类的算法,来限制缓存的大小,jQuery.cache好像没有这样设计。

既然jQuery.cache是关于元素的缓存,那么元素销毁后,其缓存条目理当一并销毁。如果这一设想成立,你必须让jQuery知道你销毁了元素,这样jQuery才有机会删除缓存条目(缓存属于jQuery内部实现)。

我们先从代码中寻找删除jQuery.cache条目的逻辑。直接在jquery-1.10.2.js中搜索“jQuery.cache”,匹配结果只有4条,很容易找到清理缓存条目的函数:

draw2d-5.3.4/jquery-1.10.2.js
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function internalRemoveData( elem, name, pvt ) {
    if ( !jQuery.acceptData( elem ) ) {
        return;
    }
    var thisCache, i,
        isNode = elem.nodeType, //元素1,文本3,注释8,文档9,文档类型10,文档片段11
        cache = isNode ? jQuery.cache : elem,
        id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
 
    // 如果缓存条目不存在,直接返回
    if ( !cache[ id ] ) {
        return;
    }
 
    if ( name ) {
        // 获得当前元素的缓存对象
        thisCache = pvt ? cache[ id ] : cache[ id ].data;
 
        if ( thisCache ) {
            // 把name转换为缓存key的数组
            i = name.length;
            while ( i-- ) {
                // 删除name中所有key对应的缓存数据
                delete thisCache[ name[i] ];
            }
            // 如果缓存条目中还有数据,不销毁缓存条目
                        // isEmptyDataObject:data属性不影响为空判断
                        // isEmptyObject:必须真正的没有任何属性
            if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {
                return;
            }
        }
    }
 
    // 销毁缓存条目
    if ( isNode ) {
        jQuery.cleanData( [ elem ], true );  // ⇨
    } else if ( jQuery.support.deleteExpando || cache != cache.window ) {
        delete cache[ id ];
    } else {
        cache[ id ] = null;
    }
}

如果入参elem是DOM节点,则调用cleanData()执行复杂的处理逻辑;否则,仅仅将缓存条目从jQuery.cache中移除

draw2d-5.3.4/jquery-1.10.2.js
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
jQuery.extend({
        cleanData: function (elems, /* internal */ acceptData) {
            var elem, type, id, data,
                i = 0,
                internalKey = jQuery.expando,
                cache = jQuery.cache,
                deleteExpando = jQuery.support.deleteExpando,
                special = jQuery.event.special;
            //在这里,elems必然是jQuery元素集合对象,遍历
            for (; (elem = elems[i]) != null; i++) {
                // 如果支持缓存
                if (acceptData || jQuery.acceptData(elem)) {
 
                    id = elem[internalKey]; // 缓存条目键
                    data = id && cache[id]; // 缓存条目值
 
                    if (data) {
                        // 移除事件处理函数
                        if (data.events) {
                            for (type in data.events) {
                                if (special[type]) {
                                    jQuery.event.remove(elem, type);
                                } else {
                                    jQuery.removeEvent(elem, type, data.handle);
                                }
                            }
                        }
 
                        if (cache[id]) {
                            // 从jQuery.cache中删除该缓存条目
                            delete cache[id];
                            if (deleteExpando) {
                                delete elem[internalKey];
                            } else if (typeof elem.removeAttribute !== core_strundefined) {
                                elem.removeAttribute(internalKey);
                            } else {
                                elem[internalKey] = null;
                            }
                            core_deletedIds.push(id);
                        }
                    }
                }
            }
        }
});

那么,哪些函数调用此函数呢?直接调用它的函数有两个:

draw2d-5.3.4/jquery-1.10.2.js
JavaScript
1
2
3
4
5
6
7
8
9
jQuery.extend({
    cache: {},
    removeData: function (elem, name) {
        return internalRemoveData(elem, name);
    },
    _removeData: function (elem, name) {
        return internalRemoveData(elem, name, true);
    }
});

对于第二个函数,存在以下调用链:jQuery.off()  ⇨ jQuery.event.remove()  ⇨ jQuery._removeData()。因此,解除注册元素上的事件处理器,可能会让元素缓存条目被清理。

在上面的分析中,我们知道internalRemoveData()可能调用cleanData()来清理节点的缓存数据,那么还有其它函数调用cleanData()么?搜索发现,下面的方法也会调用它:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
jQuery.fn.extend( {
    // 移除匹配的元素
    remove: function ( selector, keepData ) {
        var elem,
            elems = selector ? jQuery.filter( selector, this ) : this,
            i = 0;
        for ( ; (elem = elems[ i ]) != null; i++ ) {
 
            if ( !keepData && elem.nodeType === 1 ) {
                jQuery.cleanData( getAll( elem ) );
            }
        }
 
        return this;
    },
    // 清空子节点
    empty: function () {
        var elem,
            i = 0;
 
        for ( ; (elem = this[ i ]) != null; i++ ) {
            // Remove element nodes and prevent memory leaks
            if ( elem.nodeType === 1 ) {
                jQuery.cleanData( getAll( elem, false ) );
            }
        }
        return this;
    },
} );

小结一下:

  1. 函数internalRemoveData负责:
    1. 从缓存条目中删除指定的键值对
    2. 当缓存条目“为空”时,清理DOM节点的事件监听器数据,并且从jQuery.cache中移除缓存条目
  2. 方法cleanData负责:清理元素的缓存数据
  3. 当通过jQuery API移除元素时,它的位于jQuery.cache中的缓存条目被删除
  4. 当通过jQuery API移除元素的事件监听器时,它位于jQuery.cache中缓存条目的数据项会被删除,如果缓存条目在此数据项删除后变“空”,则删除缓存条目

因此:由于你通过jQuery API操控DOM,而导致其驻留jQuery.cache后,必须通过jQuery的API——例如remove()、off()进行清理操作,才能确保缓存条目在不需要时顺利移除。

实景图的销毁逻辑

通过上一节的分析,我们已经确信:必须通过jQuery的API进行清理操作,才能避免jQuery.cache的内存泄漏。实景图模块并没有直接使用jQuery,直接使用它的是Draw2D。那么,后者是否暴露了适当的接口供上层组件执行清理?

我们看一下实景图模块的清理逻辑。在前面的分析中我们已经看到,轮巡(switchTo)时,原有实景图UI包装器组件会被删除:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switchTo: function ( mapId ) {
    var me = this;
    me.show( mapId );
}
show: function ( mapId ) {
    var me = this;
    //首先缓存当前实景图UI包装器组件的数据内容
    me.cache( me.getCurMap() );
    //然后创新新实景图
    me.createMapComp( mapId );
    me.updateMapView();
}
createMapComp: function ( mapId ) {
    var me = this;
    if ( me.currMapComp ) {
        // 移除老的实景图
        me.mapCntr.remove( me.currMapComp, true );
        me.currMapComp = null;
    }
    var mapData = me.mapDataCache[ mapId ];
    // 创建新实景图并添加到容器中
    var mapComp = Ext.create( 'kssi.map.design.MapComponent', mapData );
}

上面的调用ExtJS标准API:Container.remove()用来移除原有的实景图UI包装器组件,由于实景图组件被设置为autoDestory,因此它会被销毁。除了ExtJS组件标准的销毁逻辑以外。实景图模块还扩展了一些逻辑:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 实景图UI包装器组件
Ext.define( 'kssi.map.design.MapComponent', {
    map : null ,  //Map$d2d
    // 扩展的逻辑
    onDestroy : function()
    {
        this.map.onDestroy();
    }
    // 销毁从这里开始,这个是ExtJS框架的逻辑,复制过来便于理解流传
    destory : function() {
        me.onDestroy();
        if (!me.preserveElOnDestroy) {
            me.el.remove(); // 移除DOM元素
        }
    }
} );
//实景图UI组件
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    canvas: null,
    // 扩展的逻辑
    onDestroy: function () {
        this.canvas.destroy();
        this.tip.close();
    }
}

到这里为止,我们可以确认:

  1. Draw2D画布的目标元素,亦即ExtJS组件MapComponent对应的DOM元素,最终被(ExtJS API)销毁了。注意上一节我们的结论——必须通过jQuery API对操纵过的元素进行清理操作,因此这里的销毁肯定不能阻止jQuery.cache的泄漏
  2. 在销毁上述DOM之前,实景图模块调用Draw2D画布的destroy()方法

这个destroy()方法应当是删除Draw2D画布的相关资源,我们看看它的代码:

draw2d-5.3.4/draw2d.js
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
draw2d.Canvas = Class.extend(
        destroy: function () {
            // 清空
            this.clear();
            // 解除document上的事件绑定
            $( document ).unbind( "keydown", this.keydownCallback );
            $( document ).unbind( "keyup", this.keyupCallback );
            this.eventSubscriptions = {};
            // 删除Raphael画布对象
            this.paper.remove();
        },
        // 清理画布上的Figure、Line等元素
        clear: function () {
            this.fireEvent( "clear" );
            var _this = this;
            // 移除所有线条
            this.lines.clone().each( function ( i, e ) {
                _this.remove( e );
            } );
            // 移除所有图形
            this.figures.clone().each( function ( i, e ) {
                _this.remove( e );
            } );
            // 重置实例变量
            this.zoomFactor = 1.0;
            this.selection.clear();
            // ...
            return this;
        }
    }
);

注意到没有?Draw2D的Canvas类在初始化时注册了很多事件监听器(this.html.on/bind调用),但是在上面的destroy()方法中:

  1. 没有通过off()调用解除this.html的监听器绑定
  2. 也没有通过remove()调用删除this.html元素

(为了防止代码分析存在遗漏,你可以在jQuery的internalRemoveData、cleanData函数上添加断点,然后再运行实景图模块,如果断点没有命中或者调用针对的元素不是this.html,则证明上述结论无误)

因此,jQuery.cache导致的内存泄漏发生了。

解决jQuery.cache泄漏

弄清楚来龙去脉,解决这个问题就非常简单了,只需要再Map$d2d的onDestroy()钩子中添加一行代码即可:

map-d2d.js
JavaScript
1
2
3
4
5
6
7
8
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    onDestroy: function () {
        this.canvas.destroy();
        // 解决jQuery.cache内存泄漏问题
        this.canvas.html.remove();
        this.tip.close();
    }
});
重新分析时间线

上节我们通过修改Draw2D的代码,处理了jQuery.cache泄漏问题,现在我们需要验证一下,问题是不是真正解决了。重新录制10分钟的堆时间线: 

 timeline-after-jquery-cache-fixed

和一开始的录制的时间线对比一下可以看到,我们解决jQuery.cache泄漏后,实景图整体内存消耗有了很大改善。 观察此图,我们可以看到:

  1. 每个高柱子(都大于1.0MB线)都是在轮巡切换实景图时发生的内存分配
  2. 总有4-5个蓝色高柱子,它们总是位于时间线的最右侧。这是个好现象,说明随着时间的推移,以前分配的内存大多被回收了
  3. 仍然有少量内存过了很久都没有被回收,注意观察那些非常低矮的蓝色短柱

通过多次重新录制、查看,发现上面第3条提及的蓝色短柱是随机出现的,其Retained Size也主要由Chrome本身导致,因此不需要刻意去分析。

那么,大片的灰色区域中,是否存在“肉眼不可见”(量太少以至于画不出蓝色柱子)的内存泄漏呢?选定一个大范围的区间查看:

timeline-after-jquery-cache-fixed-2

那些带括号的一般是JS引擎内部使用,可以不去考虑。因此可能属于内存泄漏的类型包括:constructor、Object、Array、HasListeners。我们分别展开这些类型的第一个对象实例,合并到一张图片中进行对照:

ext-cmp-1035注意在这4张图片的Retainers子面板中出现的ext-com-1035,很明显,后3个对象分别是第1个对象的events、protoEl.classList、hasListeners属性。只要第1个对象被回收,其它3个对象自然会被回收。因此,我们只需要搞清楚第1个对象是什么,为什么一直驻留就可以了。

分析constructor@2398693

下面看看constructor对象的列表,注意那些Retained Size一样的对象,它们往往是基于同样的逻辑创建出来了,一般你只需要分析其中一个就可以了:

Selection_001

以constructor@2398693为例,它的引用链是: window.Ext.ComponentMgr.all.map['ext-comp-1035'] 。如果你熟悉ExtJS的架构,就知道所有ExtJS组件创建后都在组件管理器(ComponentMgr)中注册,销毁前则从组件管理器中解除注册:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
Ext.define('Ext.AbstractComponent', {
    constructor : function(config) {
        // ...
        me.initComponent();
        // 在initComponent阶段之后,把自己注册到组件管理器
        Ext.ComponentManager.register(me);
    },
    destroy : function() {
        // ...
        Ext.ComponentManager.unregister(me);
    }
});

constructor@2398693一直没有解除注册,可能因为实景图模块没有适当的销毁它。 

下面看一下该对象的Allocation stack,看看是什么代码创建了constructor@2398693:

Selection_002

从栈顶向下跟踪,可以发现实景图UI组件创建了它:

map-d2d.js
JavaScript
1
2
3
4
5
6
7
8
9
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    constructor: function () {
        this.callParent( arguments );
        this.tip = Ext.create( 'Ext.tip.Tip', {
            cls: 'tooltip'
        } );
        return this;
    }
});

原来constructor@2398693是一个Tooltip,其实你随时可以通过鼠标悬停或 $0 来查看constructor@2398693的细节,进而获知它是一个Tooltip。

我们顺便看看Map$d2d是怎样销毁这个Tooltip的:

map-d2d.js
JavaScript
1
2
3
4
5
6
7
8
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    onDestroy: function () {
        this.canvas.destroy();
        //解决jQuery.cache内存泄漏问题
        this.canvas.html.remove();
        this.tip.close();
    }
});

Map$d2d调用Toltip的close()方法(这个方法从Ext.panel.Panel继承得到)。这个方法的行为是什么呢?官方文档描述如下:

关闭面板,默认行为是将其从DOM中移除,删除面板对象及其所有子组件。注意:该方法的行为受到closeAction设置的影响,如果要明确的控制,请使用destroy()或者hide()方法

注意到了吗,close()不一定销毁组件,也可能是隐藏组件,这取决于closeAction设置,而Ext.tip.ToolTip的closeAction默认是hide,这个ExtJS官方文档说明的很清楚。所以实景图模块存在BUG。

解决Ext.tip.ToolTip泄漏

要解决上面的泄漏很简单,要么在onDestroy()时明确调用destroy(),要么修改closeAction设置:

map-d2d.js
JavaScript
1
2
3
4
5
6
7
8
9
10
11
Ext.define( 'com.kingsmartsi.secmon.map.domain.Map$d2d', {
    constructor: function () {
        this.tip = Ext.create( 'Ext.tip.Tip', {
            cls: 'tooltip',
            closeAction: 'destroy'
        } );
    },
    onDestroy: function () {
        this.tip.destroy();
    },
});

回过头来想想,真的需要为每个实景图UI组件配备单独的Tooltip实例吗?完全没必要,ToolTip本身就倾向于全局使用。 

重新分析时间线

处理好Tooltip泄漏后,更新服务器,再次录制10分钟的时间线:

timeline-final

可以看到,驻留的对象都是Chrome引擎内部使用的对象。连续运行实景图轮巡一小时,通过Chrome任务管理器观察,内存也没有明显增长。我们认为不存在内存泄漏了。

启用轮巡时的堆时间线分析:Round 2

上面的分析结束后,我们认为内存泄漏问题已经解决。

但是,实景图轮巡是需要支持无限时间连续运行的,所以我们还要进行长期稳定性测试。连续运行16小时后,实景图模块占用的内存从100MB增长到400MB左右。看来,还是存在内存泄漏——平均每小时泄漏18MB内存,只是由于垃圾回收机制的干扰,我们无法在短时间内直观的察觉。

重新分析时间线

这里,我们还是首先考虑通过时间线分析。不要刷新浏览器,录制一个时间线。

和上一个时间线一样,我们选取一大片区间,中间并没有可供分析的对象。是不是泄漏都是在时间线之前分配的呢?毕竟实景图模块在录制前已经连续运行16小时了。这里有个技巧:将区间起点拉到最左边,可以看到在录制时间线之前分配的那些对象:

obj-alloc-before-timeline

看到没有,在这16小时内:Object导致116MB内存驻留;constructor导致97MB内存驻留;Array导致80MB内存驻留。虽然这些内存可能会有重合、这些对象也不一定都是内存泄漏,但是,内存泄漏的存在是无容置疑的——最初运行的1小时内,实景图模块总计占据的内存还不到100MB。

iframe导致的实景图管理器泄漏

我们先看看Object,大概浏览一下前面的几十条,发现它们都是window.mapManager.mapDataCache:

two-mm

也就是说,内存中存在多个实景图管理器(kssi.map.run.MapManager)对象(它与mapDataCache是1:1关系)。这是严重的编程错误,在实景图模块,实景图管理器是全局唯一的对象,绝不应该存在多个实例。

检查代码,发现创建实景图管理器的逻辑是存放在Ext.onReady()方法中的,这意味着在页面的生命周期中,实景图只会创建一次。那么时间线中的多个mapManager,该如何解释?

页面的生命周期中,实景图只会创建一次,这个推论没有问题。出现多个mapManager,是因为时间线跨越了页面的生命周期,说它跨越的依据是 in Window@后面的数字。看到了吗,左边9601而右边是17947。window的标识符都变了,说明页面刷新了。我们重新录制时间线、结合console.log可以证明这一点:

timeline-3600

看到没有,在运行1小时后,出现大量内存分配,日志也显示mapManager被创建。到这里我们可以确信实景图模块有自动刷新的逻辑,搜索reload找到:

run-core.js
JavaScript
1
2
3
4
5
6
7
Ext.TaskManager.start( {
    run: function () {
        mm.cacheTransientState( true );
        window.location.reload();
    },
    interval: 3600 * 1000
} );

这段代码原本是用作临时解决内存泄漏的,而现在,解药却变成了毒药。这是为什么?

页面刷新后、页面内存被清空,整个时间线信息应该丢失才对。而我们的时间线完整的记录了多个window对象实例。这个现象可能的原因是,实景图是运行在iframe中的。top窗口一直没有刷新。你可以切换到Elements面板,搜索iframe发现:

XHTML
1
2
<iframe src="/pems-web-manager/maprun/group" name="frame-frame" width="100%" height="100%" frameborder="0" id="ext-gen1114">
</iframe>

页面刷新后,其内存仍然不能销毁,唯一可解释的原因是:其它iframe或者top窗口的某个变量直接/间接引用该页面中的变量。

通过查看top窗口的脚本monitoring.js,我们看到其中大量的引用了子页面的mapManager。

解决实景图管理器泄漏

由于“每小时刷新实景图页面”是以前解决内存泄漏的临时方案,现在已经不需要了,删除定时器代码即可。

总结

关于如何发现、定位并解决内存泄漏,请参考下面的几条建议:

  1. 不要猜测!去测试、去剖析
  2. 再小的内存泄漏,经过足够长的时间后也会对应用稳定性和性能产生严重影响,因此不能放过
  3. 尽早测试,特别是那些需要长时间运行的程序。随着程序规模的增长、新框架和新组件的引入,定位内存泄漏变得越来越困难
  4. 通过堆时间线去定位、隔离出内存泄漏
  5. 识别Chrome引擎随机性的内存分配,这些分配在对象列表中,其类型往往有括号包围
  6. 记录Allocation stack,快速的跟踪什么代码创建了对象
  7. 必要时,开启Debug进行单步跟踪,了解自己不熟悉的第三方框架的行为
  8. 解决了一个内存泄漏后,最好重新录制堆时间线,因为其它对象的驻留可能也是由这个已解决的内存泄漏引起

 

← Go语言学习笔记
2016年7月塘沽极地海洋世界 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 使用Sysdig进行系统性能分析
  • Go应用性能剖析
  • 利用perf剖析Linux应用程序
  • 使用Eclipse Memory Analyzer分析JVM堆Dump
  • 使用Oracle Java Mission Control监控JVM运行状态

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2