Java的内存模型以及GC算法,javascript内存机制

  Java的内存模型以及GC算法,javascript内存机制

  本文为大家带来一些关于javascript的知识,主要介绍对JavaScript内存管理和GC算法的深入理解,主要讲解JavaScript的垃圾收集机制和常见的垃圾收集算法;还讲解了V8引擎中的内存管理,希望对大家有所帮助。

  【相关推荐:javascript视频教程,web前端】

  

前言

   JavaScript在创建变量(数组、字符串、对象等)时是自动进行了分配内存。),并会在不使用它们的时候“自动”释放分配的内容;JavaScript不同于其他底层语言,比如C语言。它们提供了内存管理的接口,比如malloc()用于分配所需的内存空间,free()用于在释放内存空间之前分配内存空间。

  我们提供释放内存的过程称为垃圾回收这种类似JavaScript的高级语言,自动内存分配,自动内存回收,因为这种自动化导致很多开发者不关心内存管理。

  即使高级语言提供了自动内存管理,我们也需要对内部管理有一个基本的了解。有时候自动内存管理会出现问题,我们可以更好的解决,或者用代价最小的方法解决。

  

内存的生命周期

  其实不管是什么语言,内存的声明周期大致分为如下几个阶段:

  下面我们对每一步进行具体说明:

  内存分配:当我们定义一个变量时,系统会自动为它分配内存。它允许在程序中使用这个内存。当对变量执行内存使用:时,出现读写内存回收:使用后,自动释放不需要的内存,即垃圾回收机制自动回收未使用的内存

JavaScript中的内存分配

  为了保护开发人员的头发,JavaScript在定义变量时就自动完成了内存分配,示例代码如下:

  let=123//为数值变量分配内存

  Str=一碗周//为字符串分配内存

  让obj={

  名称:“一碗周”,

  年龄:18,

  }//为对象及其包含的值分配内存

  //为数组及其包含的值分配内存(类似于对象)

  设arr=[1,null, abc]

  函数fun(a) {

  返回一个2

  }//为函数(可调用对象)分配内存

  //函数表达式也可以分配一个对象。

  Element.addEventListener(

  点击,

  事件={

  console.log(事件)

  },

  假的,

  )有些时候并不会重新分配内存,如下面这段代码:

  //为数组及其包含的值分配内存(类似于对象)

  设arr=[1,null, abc]

  设arr2=[arr[0],arr[2]]

  //这里不会重新分配内存,而是直接存储原来的内存

在JavaScript中使用内存

   JavaScript中使用值的过程,其实就是读写分配内存的操作。这里的读写可能是写变量,读变量的值,写对象的属性值,传递参数给函数等。

  

释放内存

   JavaScript中的内存释放是自动的。释放的时机是当一些值(内存地址)不再被使用时,JavaScript会自动释放它所占用的内存。

  其实大部分内存管理问题都在这个阶段。这里最难的任务是找到那些不必要的变量。

  虽然现在的高级语言都有自己的垃圾收集机制,虽然垃圾收集算法很多,但是并不能智能收集所有的极端情况,这就是为什么我们要学习内存管理和垃圾收集算法。

  接下来,我们来讨论JavaScript中的垃圾收集和常见的垃圾收集算法。

  

JavaScript中的垃圾回收

  我们前面说过,JavaScript中的内存管理是自动的,在创建对象时会自动分配内存。当对象不再是引用或不能从根上访问时,它将被作为垃圾回收。

  JavaScript中可到达的对象只是可以访问到的对象。不管是通过引用还是作用域链,只要能被访问就叫可达对象。可达对象的可达性有一个标准,即能否从根找到;这里的根可以理解为JavaScript中的全局变量对象,意思是浏览器环境下的窗口,节点环境下的全局。

  为了更好的理解引用的概念,看下面这一段代码:

  let person={

  名称:“一碗周”,

  }

  让男人=人

  人员=null图解如下:

  根据上图我们可以看到,这个{name:一碗周 }最后不会被当做垃圾回收,因为它也有一个参考。

  现在我们来理解一下可达对象,代码如下:

  函数groupObj(obj1,obj2) {

  obj1.next=obj2

  obj2.prev=obj1

  返回{

  obj1,

  obj2,

  }

  }

  设obj=groupObj({ name:大明 },{ name:小明 })调用groupObj()函数的结果是一个包含两个对象的对象,其中obj.obj1的下一个属性指向obj.obj2;obj.obj2的prev属性指向obj.obj2.最后形成了一个无限娃娃。

  如下图:

  现在来看下面这段代码:

  删除obj.obj1

  删除obj.obj2prev我们删除obj对象中obj1对象的引用和obj.obj2中prev属性对obj1的引用。

  图解如下:

  这时候obj1就被当垃圾回收了。

  

GC算法

   GC是垃圾收集的简称,即垃圾回收。当垃圾收集器工作时,它可以找到内存中的垃圾,并释放和回收空间。回收后方便我们后续使用。

  GC中的垃圾,包括程序中不在需要使用的对象程序中不能再访问到的对象,都会被当作垃圾处理。

  GC算法就是工作时查找和回收所遵循的规则,常见的GC算法有如下几种:

  引用计数:用一个数字记录引用的次数,通过判断当前数字是否为0来判断对象是否为垃圾。标记清除:在工作中给一个物体加一个标记,判断它是不是垃圾。标记整理:类似于标记清除。V8中使用的分代回收:垃圾收集机制。

引用计数算法

  引用计数算法的核心思想是设置一个引用计数器来判断当前引用数是否为0,从而确定当前对象是否为垃圾,从而垃圾收集机制开始工作,释放这个内存。

  引用计数算法的核心是引用计数器,不同于其他GC算法。

  引用计数器的变化会随着引用关系的变化而变化。当引用计数器变为0时,对象将被视为垃圾回收。

  现在我们通过一段代码来看一下:

  //{name:一碗周 }的参考计数器1

  let person={

  名称:“一碗周”,

  }

  //添加了另一个引用,引用计数器1

  让男人=人

  //取消引用,引用计数器-1

  人员=空

  //取消引用并引用counter-1。这时候{name:一碗周 }的记忆就会被当做垃圾收集起来。

  man=null引用计数算法的优点如下:

  一发现垃圾就回收;尽量减少程序挂起,发现垃圾立即回收,从而减少程序因内存满而被迫停止的现象。缺点如下:

  无法回收循环引用的对象;就比如下面这段代码:

  fun() {

  const obj1={}

  const obj2={}

  obj1.next=obj2

  obj2.prev=obj1

  还‘一碗周’

  }

  在fun()上面的代码中,函数体的内容在函数执行完成后是没有用的。但是,由于对obj1和obj2的引用不止一个,所以两者都不能回收,造成了空间和内存的浪费。

  时间成本高,因为引用计数算法需要不断监测引用计数器的变化。

标记清除算法

  标签清除算法解决了引用计数算法的一些问题,实现比较简单,会用到V8引擎中。

  标记清除算法时,不会立即收集未引用的对象。相反,垃圾对象会一直累积,直到内存耗尽。当内存耗尽时,程序将被挂起,垃圾收集将开始。当所有未被引用的对象都被清除后,程序将继续执行。该算法的核心思想是将整个垃圾收集操作分为标记和清除两个阶段。

  第一阶段是遍历所有对象,标记所有可达对象;第二阶段是遍历所有对象,清除未标记的对象。同时,所有被标记的对象都将被擦除,以方便下一步工作。

  为了区分可用对象和垃圾对象,我们需要记录每个对象中对象的状态。因此,我们为每个对象添加一个特殊的布尔字段,称为marked。默认情况下,对象是在未标记状态下创建的。因此,标记字段被初始化为false。

  垃圾回收后,把回收的内存放在空闲链表中,供我们后续使用。

  标记算法最大的优点是解决了引用计数算法无法回收循环引用对象的问题。例如,下面的代码:

  fun() {

  const obj1={},

  obj2={},

  obj3={},

  obj4={},

  obj5={},

  obj6={}

  obj1.next=obj2

  obj2.next=obj3

  obj2.prev=obj6

  obj4.next=obj6

  obj4.prev=obj1

  obj5.next=obj4

  obj5.prev=obj6

  返回obj1

  }

  Const obj=fun()当函数结束时,obj4的引用不为0,但是使用引用计数算法无法将其作为垃圾回收,使用标签清除算法可以解决这个问题。

  但是标签清除算法存在一些缺点,会导致内存碎片和地址不连续。还有就是标签移除算法的使用,即使发现垃圾对象,也可以立即移除,需要第二次移除。

  

标记整理算法

  标记整理算法可以看作是标记清除算法的增强,其步骤分为标记和清除两个阶段。

  但是在标记排序算法的清除阶段,会先对对象进行排序,移动对象的位置,最后清除对象。

  步骤如下图:

  

V8中的内存管理

  

V8是什么

   V8是主流的JavaScript执行引擎。现在Node.js和大部分浏览器都采用V8作为JavaScript的引擎。V8的编译功能采用即时编译,也称为动态翻译或运行时编译,是一种执行计算机代码的方法。这种方法包括在程序执行期间(在执行期间)而不是在执行之前编译程序。

  V8引擎对内存有上限,64位操作系统下1.5G,32位操作系统下800MB。至于为什么设置内存上限,主要是因为内容V8引擎主要是为浏览器准备的,不适合占用太多空间;还有一点就是这种规模的垃圾收集非常快,用户几乎感觉不到,从而增加了用户体验。

  

V8垃圾回收策略

   V8发动机采用分代回收的思想,主要是将我们的内存按照一定的规则分为两类,一类是新生代存储区,一类是老一代存储区。

  新一代的对象是那些存活时间短的,简单来说就是新产生的,通常只支持一定的容量(64位操作系统32MB,32位操作系统16MB),而老一代的对象是那些存活事件长或者内存驻留的,简单来说就是在新一代的垃圾收集中存活下来的,容量通常比较大。

  下图展示了V8中的内存:

  V8引擎会根据不同的对象采用不同的GC算法,V8中常用的GC算法如下:

  逐代回收空间复制标记清除标记整理标记增量

新生代对象垃圾回收

  如上所述,生存时间短的物体是新生代储存的。新一代对象回收过程采用复制算法和标记排序算法。

  复制算法将我们的新一代内存区域划分为两个大小相同的空间。我们把处于当前使用状态的空间称为从状态,把处于空间状态的空间称为状态。

  如下图所示:

  我们将所有活动对象存储在From空间中,当空间快满时,将触发垃圾收集。

  首先,需要对来自太空的新一代活动物体进行标记和整理。标记和排序后,已标记的活动对象将被复制到目标空间,未标记的对象将被回收。最后,从空间和到空间被交换。

  还有一点要说的是,复制对象时,新一代对象会移动到老一代对象。

  这些被移动的对象是具有指定条件的,主要有两种:

  经过一轮垃圾回收,存活下来的新一代对象会被移到老一代对象,To空间的占用率超过25%,这个对象也会被移到老一代对象(25%的原因是怕影响后续的内存分配)。因此,新一代对象的垃圾收集方案是以空间换时间。

  

老生代对象垃圾回收

  老一代区存储的对象是生存时间长,占用空间大的对象。正是由于其生存时间长,占用空间大,复制算法无法使用。如果使用复制算法,会导致时间和空间的浪费。

  老一代对象一般采用标记清除、标记排序和增量标记算法进行垃圾收集。

  在清除阶段,标记清除算法主要用于回收。一段时间后,会产生大量不连续的内存碎片。当碎片太多无法分配足够的内存时,会使用标记排序算法来整理出我们的空间碎片。

  老生代对象的垃圾回收会采用增量标记算法来优化垃圾回收的过程,增量标记算法如下图所示:

  因为JavaScript是单线程的,所以只能同时运行一个程序执行和垃圾回收,这样会导致程序在垃圾回收时卡死,肯定会给用户不好的体验。

  因此,提出了增量标记。程序运行时,程序运行一段时间,然后进行初步标记。这个标记可能只标记直接可达的对象,然后程序继续运行一段时间,进行增量标记,也就是标记哪些间接可达的对象。如此反复,直到结束。

  

Performance工具

  由于JavaScript没有为我们提供操作内存的API,所以我们只能管理它自己提供的内存,而不知道实际的内存管理是什么样的。有时我们需要留意内存的使用情况。性能工具提供了多种监控内存的方式。

  

Performance使用步骤

  首先我们打开Chrome浏览器(这里我们用的是Chrome浏览器,其他浏览器也可以),在地址栏输入我们的目标地址,然后打开开发者工具,选择【性能】面板。

  选择性能面板后,开启录音功能,然后访问特定界面,模仿用户进行一些操作,然后停止录音。最后,我们可以在分析界面中分析记录的内存信息。

  记忆问题的体现

  出现内存的问题主要有如下几种表现:

  当页面延迟加载或频繁暂停时,其底层伴随频繁的垃圾回收的执行;为什么垃圾收集频繁?可能是有些代码直接导致内存满,需要立即垃圾回收。关于这个问题我们可以通过内存变化图进行分析其原因:

  页面有一个持续性的不佳表现,也就是说在我们使用的过程中,页面给我们的感觉就是一直不好用。我们一般认为页面最底层会有内存膨胀。所谓内存扩展,就是当前页面为了达到某个速度,请求的内存比它需要的多得多。请求的内存超过了我们的设备本身可以提供的大小,此时我们可以感知到持续的低性能体验。导致内存扩展的问题可能是我们代码的问题,也可能是设备本身很差。如果要分析、定位、解决,需要在多台设备上进行反复测试。

  页面的性能随着时间的延长越来越差,加载时间越来越长。出现这个问题的原因可能是由于代码出现内存泄露。要检测内存是否泄露,可以通过内存总视图监控我们的内存。如果内存持续上升,则可能存在内存泄漏。

  

监控内存的方式

  在浏览器中监控内存主要有以下几种方式:

  浏览器提供的任务管理器,Timeline时序图堆快照,搜索并分离DOM,判断是否有频繁的垃圾收集。接下来,我们将分别解释这些方式。

  

任务管理器监控内存

  在浏览器中按【Shift】 【ESC】键,打开浏览器提供的任务管理器。下图显示了Chrome浏览器中的任务管理器。我们来解读一个。

  在上图中,我们可以看到【掘金】标签上的【内存占用】所指示的浏览器中,该页面的DOM所占用的内存。如果一直增加,说明正在创建一个新的DOM下面的【JavaScript使用的内存】(默认不打开,需要右键打开)表示JavaScript中的堆,括号内的大小表示JavaScript中所有可达对象。

  

Timeline记录内存

  上面介绍的浏览器中提供的任务管理器,只能用来帮助我们判断页面是否存在问题,而不能定位页面上的问题。

  Timeline是性能工具中的一个小标签,以毫秒为单位记录页面中的情况,可以帮助我们更简单地定位问题。

  

堆快照查找分离DOM

  堆快照很有针对性的发现当前接口对象中是否存在一些分离的DOM。分离的DOM的存在意味着存在内存泄漏。

  首先我们先要弄清楚DOM有几种状态:

  第一,DOM对象存在于DOM树中,属于正常的DOM。然后,它不存在于DOM树中也没有JS引用,属于垃圾DOM对象。最后,它不存在于DOM树中但是有JS引用。这是DOM的分离,需要我们手动释放。找到分离的DOM的步骤:打开开发者工具【内存面板】【用户配置】【获取快照】在【滤镜】中输入Detached,找到分离的DOM。

  如下图所示:

  找到创建的分离DOM后,我们找到DOM的引用,然后释放它。

  

判断是否存在频繁的垃圾回收

  因为应用在GC工作的时候是停止的,如果当前垃圾回收工作频繁,时间过长,就会对页面不友好,导致应用假死,说“我的状态”。用户在使用的时候会感知到应用卡顿。

  我们可以通过如下方式进行判断是否存在频繁的垃圾回收,具体如下:

  从Timeline时序图判断,监视当前性能面板中的内存趋势。如果它频繁地上升和下降,就会发生频繁的垃圾收集。这时候就要定位代码,看看执行的时候是什么原因造成了这种情况。用浏览器任务管理器会比较容易。在任务管理器中,主要是数值的变化,数据频繁的瞬间增减也是频繁的垃圾收集。【相关推荐:javascript视频教程,web前端】以上是深入了解JavaScript内存管理和GC算法的细节。更多请关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: