我想问一个关于Java脚本中标记和清除的问题,我将在下面提供该代码
var user = "mina";
var user = null;
console.log(user);
在该代码中,如果我们实现标记并清除“ var user =“ mina”“,则会被视为垃圾,因为它不再可访问了,这是正确的
答案 0 :(得分:5)
免责声明:我在Microsoft期间使用Chakra JS引擎。
首先:您的问题以ECMAScript(JavaScript规范)为前提,要求实现使用标记清除垃圾收集器,相反:它不是:
ECMAScript规范不需要垃圾收集,实际上,根据我对规范的解释,永远不会释放内存并最终无法分配更多内存并崩溃的实现仍然是有效的实现。即使语言规范确实需要GC(例如.NET的通用语言规范),他们也没有规定必须使用“标记并扫描”策略。
历史说明:我相信JScript和VBScript的Active Scripting引擎(早于Chakra的JIT引擎)使用COM对象和环境内在函数的引用计数,而JavaScript值(字符串和JS对象)使用某种形式的标记扫描,但我可能是错的,因为我从未在该引擎上工作过。 Chakra使用了更高级的技术,包括按值传递小字符串,我在这里不做详细介绍,但是Chakra的源代码可在GitHub上找到:https://github.com/Microsoft/ChakraCore
关于使用string
值的特定问题:由于字符串文字是不可变的,因此在代码甚至被编译或解释之前,脚本解析器通常会将它们interned包含在内,因此当字符串丢失引用其字符串数据(字符数组)将不会被收集或重新分配,而只是保留在内存中。如果它是运行时创建的字符串,那么它将最终释放,并且不一定是在丢失引用的确切时刻(与琐碎的引用计数实现一样)。
关于您的 exact 代码:因为“mina”
值实际上从未使用过,因此体面的代码优化程序会完全删除该行,因此将没有任何可用空间(假设字符串没有被拦截)。</ p>
忽略这些技术上的“对象丢失所有引用后会发生什么”,然后取决于GC的类型或JavaScript引擎使用的自动内存管理:
让我们使用以下示例:
function doSomething() {
var nihilist = {
name: 'Friedrich Nietzsche',
dob: new Date( '1844-10-15' )
};
console.log( nihilist.name );
nihilist = null;
}
不同的JavaScript或ECMAScript引擎可以自由地实现内存管理,但是它们认为合适(严格来说,包括什么都不做)。因此,让我们考虑一下在某些常见情况下会发生什么:
A smart compiler that analyses the lifetime of objects and their references (并且知道console.log
没有副作用)会看到为nihilist
创建的对象从未被该函数公开(即不是return
的对象,没有分配给某种输出参数,没有隐藏的async
状态机或对象唯一引用的闭包捕获),因此不会需要在免费存储中分配nihilist
对象(无论是堆还是竞技场等)and could put it on the call-stack,因此重新分配nihilist = null
时,原始对象值仍然存在堆栈,但是将在doSomething
返回时释放(当然,假设console.log
没有存储对name
字符串的引用)。
nihilist
对象X
。X
被分配给nihilist
引用。nihilist = null
后,X
不会发生任何事情(除了丢失其最后一个引用)。doSomething
返回,并且堆栈指针移至上一个堆栈帧,并且当或如果调用堆栈将更多帧推入堆栈时,包含X
的内存将被覆盖。通过另一个函数调用)。X
。其参考计数为零。内部name
string
的引用计数设置为1(假设它没有被interned)。X
被分配给nihilist
引用,其计数增加到1
。console.log
被称为传递nihilist.name
,这将Y
的引用计数增加到2
,然后在1
返回时返回到console.log
(假设console.log
没有对其进行任何新引用)。nihilist = null
,并且X
的引用计数降至零。nihilist = null
之后立即释放了内存。有多种方法可以实现跟踪收集器-这些策略之一就是您提到的朴素的标记扫掠策略:
要记住的有关跟踪垃圾收集的重要一点是,运行时既需要某种方式已经知道 在内存中分配的对象,又需要一种遵循对象之间的引用的方式。在JavaScript中,引用不是简单的32或64位原始指针,而是包含大量元数据的较大C struct
对象,这很容易,并且所有对象分配都可以存储在“对象表”中以进行简单迭代(这称为“精确垃圾收集”或“精确垃圾收集”);其他方法包括启发式扫描原始内存以查找类似于指针的值。
要注意的另一个重要事项是,跟踪GC通常不在程序中的特定点运行或直接由程序调用,而是在后台线程中运行,并在需要时冻结程序执行 < / em>(这被称为“停止世界”,通常是为了响应增加的内存使用情况(可能也是在计时器间隔)),然后执行其收集,并在完成后才恢复其他线程。这是无法预料的,这就是为什么在硬实时环境中无法使用Tracing GC系统的原因。
在这种情况下,我们假设示例GC JavaScript跟踪环境使用精确垃圾收集(我注意到Chakra主要使用内存扫描技术)。
X
是在免费商店中创建的。它的内存地址和大小将添加到运行时中的已知对象列表中。
1.1。它也引用了name
字符串(我们假设它是 interned 不可变字符串Y
)。
1.2。 dob: new Date
对象将按值存储在X
内部(在原始内存中,有一个“标记”告诉rutime它是Date
存储的按值存储,但是可能是稍后更改为“日期”参考。var nihilist = X
分配后,X
与代表函数局部变量(因为变量本身不是对象)的特殊GC根相关联,即“ X
是可访问的”。如果X
被另一个对象Z
引用,则该对象将由根引用,并且X
将是2个分离度,但仍然可以访问。console.log
内将临时引用Y
,该引用在console.log
返回时结束。由于console.log
未引用X
,因此意味着如果console.log
did 长期引用Y
,则X
仍然可以安全地销毁。nihilist = null
后,X
就再也无法访问了,但是什么也不会立即发生:X
占用的内存和有关X的分配元数据保持不变。在将来的某个时间点(可能是立即发生,甚至可能是几分钟甚至几小时的离开),GC都将冻结程序执行并开始进行标记扫掠:
5.1。首先,迭代其根对象(包括表示局部变量的特殊根)并将其注释为仍然有效(存储注释的内存可以就位(例如,每个对象都有一个元数据头,用于存储其无效/有效)状态),也可以在步骤1)中提到的已知对象列表中,例如:
function checkObject( allocatedObject ) {
if( allocatedObject.status == UNKNOWN ) {
allocatedObject.status = ALIVE;
foreach( reference in allocatedObject.references ) {
reference.destination.status == ALIVE;
checkObject( reference.destination );
}
}
}
foreach( root in allRoots ) {
foreach( reference in root.references ) {
checkObject( reference.destination );
}
}
5.2。然后,它会遍历非根对象(allNonRootObjects
)的列表,并检查它们是否还存在:
foreach( allocatedObject in allNonRootObjects ) {
if( allocatedObject.status == UNKNOWN ) {
deallocate( allocatedObject );
}
}