算法
背包问题 - 找出背包中装了哪些东西
const knapsack = (weights:number[], values:number[], total:number) => {
var n = weights.length;
var f = [[]];
f[-1] = new Array(total+1).fill(0);
for(var i=0; i<n; i++){
f[i] = new Array(total).fill(0);
for(var j=0; j<=total; j++){
if(j < weights[i]){
f[i][j] = f[i-1][j];
}else{
f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]] + values[i]);
}
}
}
console.log(f)
var totalWeight = 0,
j = total;
for(var i=n;i--;){
if(f[i][j] > f[i-1][j]){
console.log(`选择物品:${i};重量为:${weights[i]};价值为:${values[i]}`);
totalWeight += weights[i];
j -= weights[i];
}
}
console.log(`背包最大承重:${total};实际重量:${totalWeight};最大价值:${f[n-1][total-1]}`)
}
var a = knapsack([2,2,6,5,4],[6,3,5,4,6],14)
内存
以下讨论的内存模型为 node V8 的内存模型
内存管理模型
- 代码区:存放即将执行的代码片段
- 栈:存放局部变量
- 堆:存放对象、闭包上下文
- 堆外内存:不通过 V8 分配,也不受 V8 管理的内存,Buffer 对象的数据就存放于此。

代码区内存回收
这部分内存几乎不回收,存储的大部分是 AST,代码通过解释器引擎解析出来的结果。
栈区内存回收
栈(Stack)的分配与回收非常直接,当程序离开某作用域后,其栈指针下移(回退),整个作用域的局部变量都会出栈,内存收回。
堆外内存回收
堆外内存由系统管理,内存回收也只收到系统级别的调度。
堆内存回收
堆区内存结构
首先 V8 引擎内存由两部分组成:新生代、老年代。
新生代:年轻的新对象,未经历垃圾回收或仅经历过一次
老年代:存活时间长的老对象,经历过一次或更多次垃圾回收的对象
两个不同的内存结构回收机制也不同,大小也不同,老年代大约是新生代的 40 倍左右。

新生代垃圾回收
新生代由于大部分的对象是段时间存在的,所以在 GC 时大部分的对象都会是被删除,少部分是存活的,所以在新生代采用的是复制算法,这样的话性能会更好,但是对空间的利用率更低,因为需要拿出一部分来作为复制。
Scavenge 算法将新生代的总空间一分为二,只使用其中一个,另一个处于闲置,等待垃圾回收时使用。使用中的那块空间称为 From,闲置的空间称为 To。

垃圾回收开始时 From 空间所有应该存活的对象都复制完成后,原本的 From 空间将被释放,成为闲置空间,原本 To 空间则成为使用中空间,两个空间进行角色翻转。

新生代内存晋升老年代
对象一开始进来的时候都会先进入新生代,但是由于内存有限,所有的对象不可能一直存活在新生代,所以会有对象从新生代晋升到老年代的过程。
-
对象此前已经经历过一次新生代垃圾回收,这次依旧应该存活,则晋升至老年代。
-
To空间已经使用了25%,则将此对象直接晋升至老年代。
标记清除
顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep)
老年代的内存回收为标记清除,想要了解老年代的回收内存,首先就会先想到如何标记哪些是会需要回收的,哪些是不需要的。
标记的方案是从根节点开始向下遍历,js 是基于原型链的,根对象就那么几个(BOM,DOM,CSSOM)是一个深度遍历的过程,所有被引用的对象存在引用关系的就是活着的对象,这个时候会记录为可达对象,就是能访问到的对象。
这个时候会对堆内存从头到尾进行线性的遍历,如果发现对象没有被标识为可达对象,就会将其回收。下图就是一个堆内存区被标记并且清除过后的内存现状,呈碎片化。

标记整理
上述的标记清除之后会留下一堆不连续的内存段,这些内存段不完整意味着如果有大的对象过来,发现每个小格子都放不下,但是他们把空格子合并起来是可以放下的,这就会造成空间浪费。
那就又想到一种方法,标记整理,将实体的对象都合并起来,然后放到一起,重新整理内存,合并地址,然后将大的内存块腾出来,使空间变得紧凑重新让空间利用起来。
但是标记整理会非常耗费时间,并且在整理的过程中必须是所有的运行都会停下来,等整理完成之后才会恢复,所以标记整理发生的时机就变成了,当有大对象进来已经放不下的时候。下图是整理之后的结果

增量标记
上述的方案能完成整个垃圾回收了,但是标记清楚是需要全停顿的,所以一旦进行 GC 的时候,浏览器就卡住了,在我们的视觉上,60 fps 左右的帧频是最接受的,所以我们采取了增量标记的方案,如果触发 GC,那我们会把每个整个 GC 分成多个步骤,每执行一个步骤,停歇一下,把内存还给浏览器,执行完浏览器的操作,GC 再进行下一个步骤。这样交替执行就提高了程序流畅性,一定程度上避免了长时间的卡顿。
内存泄露
前端的内存泄露的情况会一般发生引用计数。这也是垃圾回收的机制。但是引用计数只是标记的阶段。
引用计数是指一个值的引用次数是 0,就表示这个值将不再用到,因此可以将这块内存释放。
内存泄露发生于这个值不再需要了,但是引用计数却不为 0,垃圾回收机制并不能回收这块内存,从而导致内存泄露。
比如说 DOM:如果一个 js 对象对一个 DOM 持有引用,删除了 DOM,但是这个时候 js 对 DOM 有引用,所以 DOM 的引用计数是 1,所以内存并不会将 DOM 进行回收。
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
下面是几个常见的内存泄露实例:
function foo(arg) {
bar = "this is a hidden global variable";
}
理论上讲弹栈之后,内存就会被回收的,但是由于 bar 变成了全局对象,此时内存并不会回收 bar,如果不手动清除,bar 就永远不会清楚了
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
当 node DOM 节点被删掉了,但是 setInterval 会不断的执行,但是处理程序一直处于活跃,所以并不能回收 someResource 也会导致内存泄露。
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
闭包对于环境变量的持有,由于 unused 和 someMethod 共享了闭包上下文环境,所以两者没有都被标记为不用时,一直不会回收