锁定老帖子 主题:Javascript 垃圾收集机制
精华帖 (0) :: 良好帖 (0) :: 新手帖 (3) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2011-10-16
最后修改:2011-10-16
clue 写道 C并不简单,主要语法看起来没啥,但边边角角的东西太多太多,我就被好些问题恶心到过,比如内存对齐、线程同步、各种宏啊指针转换啊等等 知道公认的开发效率比较吧? JS > Java > C。当然,这是在每语言都掌握了的情况下。(这也是NodeJs为什么兴起) JS有时是有很多例外,但都是些非必需的,比如eval、new Function等,实际开发中几乎根本用不到。真正重要的就只有那几点: 函数定义 函数执行(变量初始化、执行顺序等) new创建(原型链) 这些弄懂了,最大头的啥原型链、闭包什么的都是秒杀,而这些内容在ECMAScript文档中总共也就几页纸而已。 前端JS开发其实并不是难在JS语法及特性上,而是浏览器环境上(DOM、CSS、浏览器差异),东西太多太杂。当然目前有了JS框架后改善了很多。 信不信如果大学开JS课程,那Web前端开发绝对不会缺成这样子。 本人非计算机专业的,第一看次到就开发效率,js跟C,java比较, JS能跟C比吗?JS运行还依赖C吧,浏览器大部分代码是用C写的吧。 说到浏览器到差异,那是各浏览器的implementation不同,说的不准确点,就是浏览器有各自JS的语法规则。 但是C呢,语法规则应该不会随系统环境而变吧。 |
|
返回顶楼 | |
发表时间:2011-10-17
psychopath 写道 本人非计算机专业的,第一看次到就开发效率,js跟C,java比较, JS能跟C比吗?JS运行还依赖C吧,浏览器大部分代码是用C写的吧。 说到浏览器到差异,那是各浏览器的implementation不同,说的不准确点,就是浏览器有各自JS的语法规则。 但是C呢,语法规则应该不会随系统环境而变吧。 为什么不能比较开发效率?实现同样的业务逻辑,JS绝对比C、Java快,动态语言比静态语言灵活太多了 写JS不用考虑线程同步,可以灵活扩展,天生支持各种函数回调 NodeJs为什么流行?就是这个原因。 说JS运行依赖C明显有问题,顶多JS的解释器为了运行效率大多用C写的;同理于Java 但这对用JS开发有任何影响吗? 浏览器在纯JS上的差异很小的,所有浏览器都是向ECMAScript标准看齐 再说到环境,C最依赖运行平台了,32位和64位、大端小端等等,换个环境都需要重新编译;反倒是JS的平台通用性比C高多了,解释器已经把差异都抹平了。 好了,这个其实已经偏题了,有兴趣你可以查查相关资料,看动态语言与静态语言的开发效率对比。 |
|
返回顶楼 | |
发表时间:2011-10-17
clue 写道 好像说的有问题吧?
我记得标记法是这样: 从全局开始逐一标记能引用到的各个对象,标记完成后,没有被标记的就是永远不可能被用到的对象,就会被释放。 你对引用计数举的例子分析也有些问题,说到变量就不能脱离作用域链(scope chain),及当前执行环境(activate object) 以你的例子来说: function countMethod(){ // 进入新的函数,当前activate object及作用域链为: activate object = (scope1) -> global // (scope1)引用数为1, global引用数+1 var object1 = new Object(); // 新建的对象(以o1称它)被当前作用域引用:(scope1).object1 = o1 // o1引用数为1 var object2 = new Object(); // 新建的对象(以o2称它)被当前作用域引用:(scope1).object2 = o2 // o2引用数为1 object1.method1 = object2; // object1 计数器 -1,object2 计数器 +1 // 创建o2引用: o1.method1 = object2 // o2引用数为2 object2.method2 = object1; // object1 计数器 +1,object2 计数器 -1 // 创建o1引用: o2.method2 = o1 // o1引用数为2 // 离开countMethod,当前activate object恢复调用前,即: activate object = global // (scope1)引用数为0,释放它,同时它的两个属性object1与object2引用也释放,导致o1 o2引用计算-1 // o1引用数为1,o2引用数为1 <-- 这是最简单的循环引用 } // 定义完countMethod方法(以f1称它),产生两个引用: global.countMethod = f1, f1.[[scope]] = global // f1引用数为1, global引用数+1 // 当前activate object及作用域链为: activate object = global countMethod(); 如果有闭包,根据引用计数就是这样子 function f1(){ // activate object = (scope1) -> global // (scope1)+1=1, global+1 var a = {}; // (scope1).a = o1 // o1+1=1 return function f2(){} // (scope1).f2=f2, f2.[[scope]]=(scope1) // f2+1=1; (scope1)+1=2 // activate object=global // (scope1)-1=1 <-- 还有一个引用,是f2造成的,函数定义会存储当前的作用域 } // global.f1 = f1, f1.[[scope]] = global // f1+1=1, global+1 var b = f1(); // global.b=f2 // f2+1=2 b = null; // global.b=null // f2-1=1 // 此时也是有循环引用,(scope1).f2 = f2, f2.[[scope]] = (scope1) 以上是纯理论上的最简单的引用计数,但是可以看到,非常脆弱。现在不管什么浏览器都是几种方法结合,JS内部的循环引用都能识别。 我们常说的循环引用无法释放是指JS与DOM两套体系之间的循环引用 LZ的说法不太赞同 |
|
返回顶楼 | |
发表时间:2011-10-17
clue 写道 为什么不能比较开发效率?实现同样的业务逻辑,JS绝对比C、Java快,动态语言比静态语言灵活太多了
写JS不用考虑线程同步,可以灵活扩展,天生支持各种函数回调 NodeJs为什么流行?就是这个原因。 说JS运行依赖C明显有问题,顶多JS的解释器为了运行效率大多用C写的;同理于Java 但这对用JS开发有任何影响吗? 浏览器在纯JS上的差异很小的,所有浏览器都是向ECMAScript标准看齐 再说到环境,C最依赖运行平台了,32位和64位、大端小端等等,换个环境都需要重新编译;反倒是JS的平台通用性比C高多了,解释器已经把差异都抹平了。 好了,这个其实已经偏题了,有兴趣你可以查查相关资料,看动态语言与静态语言的开发效率对比。 我说的JS运行依赖C,是因为JS的运行环境绝大多数是浏览器,而浏览器大部分代码是C写的。 就开发效率问题,只是某个方面而已。你有见过用JS开发系统,开发浏览器的吗? JS当然开发效率高了,人家用C帮你搭好平台了。 然后在用C开发同样的程序,这当然是无法比的。 还有,我们比的是JS和C的语法规则,不是什么各种西系统的编译环境,系统不同,各种包含的头文件当然不同的。 比的的语法规则,而不是什么运行环境。 |
|
返回顶楼 | |
发表时间:2011-10-17
的确,现在在浏览器中应用计数法来释放系统垃圾的方法很少用了,一般时候完全可以忽略这种方法。但是万恶的IE中的 BOM 和 DOM 中的对象是使用 C++ 以 COM 对象的形式来实现的,而 COM 的垃圾收集机制就是采取了这种引用计数的方式。因此,即使 IE 的 Javascript 引擎是使用标记清除策略来实现的,但 javascript 访问的 COM 对象依然是基于引用计数策略的。研究的时间虽然不多,但是毕竟只要这种方式的存在,就要大概明白其原理。
|
|
返回顶楼 | |
发表时间:2011-10-17
[quote="kidneyball"]什么是这里的“全局函数”呢?我们平时说的全局函数是指Global下提供的内置函数,但你这里说的好像不是这个意思。[/quote]
var testVariable = function(){ name = "pluto" // 未用 var 声明,全局变量 var name = "pluto_local" // var 变量声明,局部变量 this.name = "xxPluto" // 对象属性,可以通过类来调用 this.height ="180"; // 对象属性,通过类来调用使用
// 下面声明一个全局函数(对内对外均可调用),age 前不带 var (这里就是一个全局函数) age = function(a,b){ return a+b; } //infoOutput(); // 调用 infoOutPut 函数输出姓名,年龄。去掉最前方的 // 可以调用 infoOutput进行输出 }
var infoOutput = function(){ alert("Name: " + name); alert("Age: " + age(3,4)); }
// 原型函数,类的对象可以调用它 testVariable.prototype.callProto= function(){ //只能访问:全局变量和全局函数 this.where = function(){ // 下面name的输出为 xxPluto, 即 this.name 的值 alert("Where can we go? " + name); callOutput(); }
var callOutput = function(){ alert("callOutput name: " + name); // age = function(a,b) 现在为一个全局函数,可以尝试在 age 前面加入 var,则显示 age is not defined。 alert("callOutput age(10,12): " + age(10,12)); } }
var testInfo = new testVariable(); // 通过 “类的对象名+属性” 来调用对象属性的值,即上面 this.height = "180" alert("height: "+testInfo.height);
var newCall = new testInfo.callProto(); //调用原型函数的实例函数 newCall.where();
举了上面这么一个例子,希望可以帮助你理解。 |
|
返回顶楼 | |
发表时间:2011-10-17
psychopath 写道
firefly_zp 写道
变量声明是在什么地方声明的,是函数体内局部变量还是全局变量?如果是局部变量必然会有其运行环境(子函数内,闭包函数内等等),在包含此局部变量的函数运行结束后(变量 a 没有被其他变量引用),就会被释放。如果是全局变量,当包含这个全局变量的函数体运行结束后,这个变量才会被释放,否则一直在环境内,不会释放的。
单说 a = 1 这个函数声明(如果只有这一行,那就不是 js 了。他必然包含在某个函数中 - 或全局函数或内部函数),a = 1 执行完成后是“进入环境”,因为他被包含于某个函数中,当他被包含于的这个函数执行结束后,并且没有其他变量对他的引用时,这个时候的状态才是“离开环境”。
我们现在所说的“离开环境”的状态主要有两点:第一,变量本身离开其所处的环境(最直接的可以看出)。第二,如果变量被其他变量所引用,则要去判断引用 a 的变量是否离开了他的环境,如果两点都符合,则此时的 a = 1的状态是“离开环境”。
a = 1; 这个是函数? 不是JS? JS难道规定了要成为JS,必须要多少代码,必须要有函数?
我晕,变量定义而已。 这个变量成了全局对象的一个property。 这个环境是the global object, 不是什么函数!!!
In top-level JavaScript code (i.e., code not contained within any function definitions),
全局变量,仅仅声明而不被调用,个人认为垃圾收集器也会为其保留完整的声明周期。它会在此函数运行结束时将其销毁。我个人认为的进入环境和离开环境的意义主要是针对局部变量和局部函数来说的。当其结束了本身的运行环境并且没有被其他变量或函数所引用,就将其销毁。 |
|
返回顶楼 | |
发表时间:2011-10-17
ol_beta 写道 clue 写道 好像说的有问题吧?
我记得标记法是这样: 从全局开始逐一标记能引用到的各个对象,标记完成后,没有被标记的就是永远不可能被用到的对象,就会被释放。 你对引用计数举的例子分析也有些问题,说到变量就不能脱离作用域链(scope chain),及当前执行环境(activate object) 以你的例子来说: function countMethod(){ // 进入新的函数,当前activate object及作用域链为: activate object = (scope1) -> global // (scope1)引用数为1, global引用数+1 var object1 = new Object(); // 新建的对象(以o1称它)被当前作用域引用:(scope1).object1 = o1 // o1引用数为1 var object2 = new Object(); // 新建的对象(以o2称它)被当前作用域引用:(scope1).object2 = o2 // o2引用数为1 object1.method1 = object2; // object1 计数器 -1,object2 计数器 +1 // 创建o2引用: o1.method1 = object2 // o2引用数为2 object2.method2 = object1; // object1 计数器 +1,object2 计数器 -1 // 创建o1引用: o2.method2 = o1 // o1引用数为2 // 离开countMethod,当前activate object恢复调用前,即: activate object = global // (scope1)引用数为0,释放它,同时它的两个属性object1与object2引用也释放,导致o1 o2引用计算-1 // o1引用数为1,o2引用数为1 <-- 这是最简单的循环引用 } // 定义完countMethod方法(以f1称它),产生两个引用: global.countMethod = f1, f1.[[scope]] = global // f1引用数为1, global引用数+1 // 当前activate object及作用域链为: activate object = global countMethod(); 如果有闭包,根据引用计数就是这样子 function f1(){ // activate object = (scope1) -> global // (scope1)+1=1, global+1 var a = {}; // (scope1).a = o1 // o1+1=1 return function f2(){} // (scope1).f2=f2, f2.[[scope]]=(scope1) // f2+1=1; (scope1)+1=2 // activate object=global // (scope1)-1=1 <-- 还有一个引用,是f2造成的,函数定义会存储当前的作用域 } // global.f1 = f1, f1.[[scope]] = global // f1+1=1, global+1 var b = f1(); // global.b=f2 // f2+1=2 b = null; // global.b=null // f2-1=1 // 此时也是有循环引用,(scope1).f2 = f2, f2.[[scope]] = (scope1) 以上是纯理论上的最简单的引用计数,但是可以看到,非常脆弱。现在不管什么浏览器都是几种方法结合,JS内部的循环引用都能识别。 我们常说的循环引用无法释放是指JS与DOM两套体系之间的循环引用 LZ的说法不太赞同 的确,现在在浏览器中应用计数法来释放系统垃圾的方法很少用了,一般时候完全可以忽略这种方法。但是万恶的IE中的 BOM 和 DOM 中的对象是使用 C++ 以 COM 对象的形式来实现的,而 COM 的垃圾收集机制就是采取了这种引用计数的方式。因此,即使 IE 的 Javascript 引擎是使用标记清除策略来实现的,但 javascript 访问的 COM 对象依然是基于引用计数策略的。研究的时间虽然不多,但是毕竟只要这种方式的存在,就要大概明白其原理。 |
|
返回顶楼 | |
发表时间:2011-10-17
确保程序始终使用最少的内存可以让页面获得更好的性能,加快页面的反应速度。优化内存最好的方式就是让运行中的代码只保存必须数据。一旦数据不再使用,就释放其引用。可以通过 null 来实现。这个做法适用于大多数全局变量和全局对象的属性。局部变量在离开其运行环境时就被释放了。 |
|
返回顶楼 | |
发表时间:2011-10-17
楼主这个引用不完整,原文第69-70页说了,Netscape Navigator 3.0使用引用计数器策略后很快就意识到了循环引用的问题,所以在4.0中放弃了引用计数的方式。
关于 标记法中的 进入环境、离开环境,也要通过前一节“执行环境及作用域” 来理解。
firefly_zp 写道
经常使用 Javascript 的人会琢磨其垃圾收集机制,Javascript 并不像 C,C++ 那样需要开发者手动去清除垃圾,在编写 Javascript 程序是,开发者无需关心内存使用问题,所需内存分配以及无用内存(垃圾)的回收完全实现了自动管理。究其根源,主要是程序收集那些不再使用的变量,并且释放其占用的内存。因此,垃圾收集机制会按照固定时间间隔,周期性反复的执行这一操作。 举例来说,局部变量只存在于函数内部,程序会为局部变量在栈内存或堆内存中分配对应的存储空间,当函数运行结束,局部变量所占用的内存就没有存在的必要了,这时程序会释放局部变量所占用的内存供其他变量使用。这是程序最简单释放内存的方法,但是很多时候,程序中变量会一直被使用,此时垃圾收集机制必须跟踪变量并且判断其是否被使用,是否可以释放其内存空间。
垃圾收集机制主要判断变量释放内存空间的方法有两个:其一是标记清除法,其二是引用计数法。
标记法,每个变量都有其运行环境,变量创建后会在某种环境中运行,比如创建一个局部变量,局部变量会运行在函数体内。当函数运行时,会标记局部变量为“进入环境”,当函数体运行结束后,意味着变量脱离了其运行环境,此时则将变量标记为“离开环境”。对于“离开环境”的变量,垃圾收集机制会进行相应记录,并且在下一个回收周期时将其释放。
引用计数法,跟踪记录每个值的被引用次数。声明一个变量并将一个引用类型值赋给该变量时,这个值得引用次数就是 1。如果同一个值又被赋给另外一个变量,则该值的引用次数加 1。相反,如果包含对这个值的引用的变量又取得另外一个值,这个值得引用次数减 1。当这个值得引用次数为 0 时,则说明没有办法再访问到此值,因此就可以将其占用的内存空间回收。当垃圾收集器在下一个周期运行时,会释放引用次数为零的值所占用的内存空间。(原文解释参考:Javascript 高级程序设计 - 第二版)
举个例子来说:
function countMethod(){
var object1 = new Object(); // 声明变量,计数器由 0 变为 1
var object2 = new Object(); // 声明变量,计数器由 0 变为 1
object1.method1 = object2; // object1 计数器 -1,object2 计数器 +1
object2.method2 = object1; // object1 计数器 +1,object2 计数器 -1
}
此函数运行退出后,object1 的计数器读数为 1,object2 的计数器度数为 1。所以两个变量都不会被销毁。如果大量的这样的程序存在于函数体内,就会导致大量的内存被浪费而无法回收,从而导致内存的泄露。
上述问题解决方法,手动释放 object1 object2 所占用的内存。即:
object1.method1 = null;
object2.method2 = null;
对比上面的例子,举一个正常情况下的例子。
function countMethod(){
var object1 = new Object(); // 声明变量,计数器由 0 变为 1
var object2 = new Object(); // 声明变量,计数器由 0 变为 1
object1.method1 = "This is object1"; // object1 计数器 -1,object1 读数变为0
object2.method2 = "This is object2"; // object2 计数器 -1,object2 读数变为0
}
通过上例看出,正常情况下,当函数运行结束后,object1 object2的读数均为 0,在下一个垃圾收集周期时,会被回收并且释放其所占用的内存。
|
|
返回顶楼 | |