JS内存管理之隐藏类和删除操作


内存管理篇

①Chrome浏览器引擎

②内存泄漏

③静态分配与对象池

1.Chrome浏览器的V8 JavaScript引擎

在初始化对象的时候,V8引擎会创建一个隐藏类,随后在程序运行过程中每次增减属性,就会创建一个新的隐藏类或者查找之前已经创建好的隐藏类。每个隐藏类都会记录对应属性在内存中的偏移量,从而在后续再次调用的时候能更快地定位到其位置。

 

比如如下代码:

function Article(){
  this.title='Inauguration Ceremony Features Kazoo Band'  ;
}
let a1=new Article();
let a2=new Article();

V8会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享的同一个构造函数和原型。假设之后又添加了下面这行代码:

a2.author='Jake';

  此时两个Article实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。

  当然,解决方案就是避免JavaScript的“先创建再补充”式的动态属性赋值,并在构造函数中一次性声明所有的属性,如下图所示:

function Article(opt_author){
  this.title=''Inauguration Ceremony Features Kazoo Band'';
  this.author=opt_author;    
}
let a1=new Article();
let a2=new Article('Jake');

这样,两个实例基本上就一样了(不考虑hasOwnProperty的返回值),因此可以共享一个隐藏类,从而带来潜在的性能提升。不过要记住,使用delete关键字会导致生成相同的隐藏类片段。看一下这个例子:

function Article(){
  this.title='Inauguration Ceremony Features Kazoo Band';
  this.author='Jake''    
}
let a1=new Article();
let a2=new Article();
delete a1.author;

代码结束之后,即使两个实例使用了同一个构造函数,它们也不再共享同一个隐藏类。动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:

function Article(){
  this.titile='Inauguiration Ceremony Features Kazoo Band '
  this.author='Jake';  '  
}

let a1=new Article();
let a2=new Article();

a1.author=null;

2.内存泄漏

1)意外声明全局变量

function setName(){
  name='Jake';  
}

2)定时器操作造成的内存泄漏

let name='Jake';
setInterval(()=>{console.log(name)},100);

只要定时器一直运行,回调函数中引用的name就会一直占用内存。

3)闭包造成的内存泄漏

let outer =Function(){
  let name='Jake';
  return function(){
      return name;          
}    
}

这回导致分配给name的内存被泄漏。以上代码创建了一个内部闭包,只要outer函数存在就不能清理name,因为闭包一直再引用着它。假如name的内容很大,那就可能是个大问题。

3.静态分配与对象池

浏览器判断何时进行垃圾回收程序的一个标准就是对象更替的速度。如果又很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样会影响性能。比如下方例子:

functio addVector(a,b){
  let resulttant=new Vector();
  resultant.x=a.x+b.x;
  resultant.y=a.y+b.y;
  return resultant;          
}

调用这个歌函数会在堆上创建一个新对象,然后修改它,最后再返给调用者。如果这个矢量对象的生命周期很短,那么它很快就会失去对它的引用,成为可以被回收的值。假如这个矢量加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排垃圾回收。

  该问题的解决方法是不要动态创建矢量对象,比如可以修改上面的函数让他使用一个已有的矢量对象:

functio addVector(a,b,resultant){
  resultant.x=a.x+b.x;
  resultant.y=a.y+b.y;
  return resultant;          
}

不过这个行为是在其他地方实例化矢量参数resultant,但这个函数的行为没有变。那么在哪里创建矢量可以不让垃圾回收调度程序盯上呢?

其中一种策略是使用对象池,在初始化的某一时刻,可以创建一个对象池,用来管理一组可以回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它。然后再操作完成后再把它还给对象池。由于没有发生对象初始化,垃圾回收探测就不会发现有对象更替,,因此垃圾回收程序就不会那么频繁地运行,下面是一个对象池的伪实现:

//vectorPool是已有的对象池
let v1=vectorPool.allocate();
let v2=vectorPool.allocate();
let v3=vectorPool.allocate();

v1.x=10;
v1.y=5;
v2.x=-3;
v2.y=-6;


addVector(v1,v2,v3);
console.log([v3.x,v3.y]);  //[7,-1]

vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);


//如果对象有属性引用了其他对象
//则这里也需要把这些属性设置为null
v1=null;
v2=null;
v3=null;

如果对象池只按需分配矢量 (在对象不存在时创建新的,对象存在时候则复用存在的),那么这个实现本质上是一个贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择。不过,使用数组来实现,必须留意不要招致额外的垃圾回收。比如下面例子:

let vectorList=new Array(100);
let vector=new Vector();
vectorList.push(vector);

 

原创文章,作者:kepupublish,如若转载,请注明出处:https://blog.ytso.com/268306.html

(0)
上一篇 2022年6月19日
下一篇 2022年6月19日

相关推荐

发表回复

登录后才能评论