阅读 216

【拒绝一问就懵】之你多少要懂点内存回收机制

背景介绍

  • Java优势之一就是其具有垃圾回收机制。在大部分情况下,JVM的GC(垃圾回收器)能够帮助我们回那些不可到达的对象(就是未被引用的对象)。
  • 当然,在一些情况下,我们仍然需要自己去释放内存(就是把对象引用置null,把容器、数组清空),否则就会引起内存泄漏,内存泄漏严重时将容易引发OutOfMemoryError,详情见 【拒绝一问就懵】之不可忽视的内存泄露
  • 此外,由于GC会停止所有的线程,包括UI线程,所以频繁的GC必然会导致画面卡顿(Android中每16ms为一帧),因此还应避免GC的频繁发生。一个导致GC频繁发生的原因就 是【拒绝一问就懵】之没听说过内存抖动吧,点击链接看详情。
    GC时线程被停止示意图
  • 所以,理解Java的内存机制,有助于帮助我们在写代码的过程中避免内存泄漏。

走进内存模型

Java内存层级

JVM运行时内存划分

JVM运行时内存划分-详细
;

程序计数器

程序计数器是线程私有的内存区域,这个区域是Java虚拟机中唯一一个没有限制OutOfMemoryError的内存区域。之所以需要它是因为Java的多线程机制是通过轮流切换分配处理器执行时间来实现的,所以会涉及到线程的暂停和重启,而在一个线程中如果正在执行Java方法的话,这个计数器就回去记录当前正在执行的虚拟机字节码,一旦被暂停,恢复只需要从程序计数器记录的为止继续执行就可以。

但是,如果线程中执行的是一个Native方法,那么程序计数器是不会去记录的,所以此时的程序计数器为空。

虚拟机栈Stack

Java虚拟机栈也是线程私有的。一条线程启动就会为它建立一个虚拟机栈。

在线程中每有一个Java方法被调用就会创建一个 “栈帧” 。每个 “栈帧” 会保存执行该方法所需的局部变量表(一般Java程序员喜欢用这个部分来代表栈)、操作数栈、动态链接以及方法出口等信息。

如果一个线程中有过多的 “栈帧” 要入到虚拟机栈中,即短时间内调用了过多的方法,就会造成 -- 栈益处 -- ,即 StackOverflowError 错误。

在这个内存区域中,如果虚拟机需要扩展内存,但没有申请到足够的内存,就会抛出 OutOfMemoryError 错误。

本地方法栈

和虚拟机栈有些类似,但它是为Native方法提供服务的。

Java堆Heap

Java的堆内存是Java虚拟机所管理的内存中最大的一块。它是所有线程所共享的,用于存放对象实例和数组,Java虚拟机的GC主要就发生在这个地方。因此这块区域也叫做"GC堆"。

Java堆的内存可以按照垃圾回收算法【分代回收】分为【新生代区】和【老年区】,进一步的,【新生代区】可以分为【Eden区】、【From Survivor区】和【To Survivor区】。

从内存角度来说,Java堆内存又被划分为线程共享的内存区域和每个线程私有的内存区域。

在Java堆区域中,如果没有内存分配给要创建的实例,并且堆也不能够再扩展,就会抛出OutOfMemoryError错误。

回收算法

Java8之后,Heap Segment真正意义上的是由Young GeneriationOld Generiation组成的。对象在其中是标记复制算法来判定一个对象是否应该被清理掉。
Heap Segment中发生的GC称为Major GC,只会影响Heap Segment区。

内存图

Young Generiation中的GC变化 — 复制算法

这个区域发生的GC称为Minor GC

  • 当对象被创建后,首先会被加入eden区。当eden区满了之后,就会触发一次GC,存活下来的对象会被复制到survivor区。
  • 当不为空的Survivor区满了,同样会触发一次GC。
  • 当短时间内有大量对象创建和释放同样会造成内存抖动,会触发CG。
  • 如图所示,survivor有两个区域,其中一个总是保持为空。
  • 现假设两个Survivor区分别为S0,S1,并且首次GC时,eden区中存活的对象被复制到S0中。当再次发生GC时,S0和eden中仍然存活的对象就会被复制到空的S1中,此时S0为空;再次发生GC时,S1和eden中存活的对象将被复制到S0中,此时S1为空;再次发生GC...就是这样进行的。当一个对象被来回复制转移的次数达到阀值(默认为15次,可以通过使用-XX:MaxTenuringThreshold该命令来调整阀值)时,这个对象将被复制到Old Generiation区中,此时该对象将会变的相对安全,因为Old Segment区的GC频率相对较低。

Old Segment中的GC变化

这个区域发送的GC成为Full GC

  • 该区域满了之后会触发一次GC,在该次GC中,一些年龄较大的对象会被清理掉。
  • 若多次触发GC后,该区域仍然处于满的状态,则会抛出OutOfMemoryError
  • 以两种情况下,新建对象会被直接复制到该区域中:
    • 当新建对象所需要的内存大于1/2的单个survivor区内存时。比如一些很长的对象;
    • 当新建对象被该区中的对象引用时,或者引用了该区域中的对象。

方法区

Java的方法区和Java的堆内存一样是被线程所共有的。它主要存放虚拟机加载的类信息、常量、静态变量、即时编译产生的代码等。

一些地方会将方法区合并到Java堆中一起去说。把它作为“永久代”。这在Hot-Spot虚拟机而言成立,但是一般来说是不成立的。

Java的方法区如果内存不够分配的话,也是会抛出OutOfMemoryError错误的。也就是如果加载过多类到方法区的话,可能会造成方法区内存益处。

对象的可到达性

在GC检查对象的是否可以回收时,是根据对象是否可到达引用练顶端的GC Roots对象来判断的。GC Roots对象一般是虚拟机栈中变量表中引用的对象、类静态属性引用的对象、常量对象、JNI传到底层的对象。就是说,一个对象如果溯源不到这几种类型的对象的话,就认为它是无法到达的,那么它将会在GC时被回收。

新的引用类型

在JDK 1.2之后,Java扩充了4种引用类型定义:

强应用类型

即我们平时通过new关键字创建出来的的对象的引用,只要强引用还存在,那么这些对象就一定不回被回收,即使时抛出OutOfMemoryError。什么时候强引用会不存在呢?当一个方法执行完,栈帧中的变量表将会被清理,在该方法中创建使用的临时强引用就会被清理掉,之后,原本它指向的对象就被变的不可到达。

软引用类型

用来描述一些有用但不是必须的对象,即通过SoftReference创建的对象,它们将会在原本确定要发生内存溢出前的一次GC中被回收,如果回收完内存还是不够,Java堆就会抛出OutOfMemoryError错误。就是说,在触发内存溢出发生前,这些对象是和强引用一样,只要引用还在,就不会被回收。

弱引用类型

用来描述一些不必须的对象,即通过WeakReference创建的对象。弱引用对象的生命周期只有一次GC。

虚引用类型

一个对象的存在与否完全不受虚引用的影响,它唯一的用处就是可以用来监测一个对象是否被回收。

方法区中的-运行时常量池

运行时常量池主要存放类中编译时期生成的常量,当然也可以动态的往里面添加。

比如:

"abc".intern();
复制代码

这个方法首先会检查运行时常量池中是否有这个字符串,有的话取出来用,没有的话生成一个并存到常量池中。

再比如,运行过程中生成通过static修饰的String时,也会加入到常量池中。对于String而言,常量 + 常量 生成的也是常量,但是常量 + 变量 生成的就是变量了。

关于Dalvik虚拟机

Dalvik虚拟机是Google按照JVM虚拟机规范定制的虚拟机,它更符合移动设备的环境要求。与标准虚拟机不同:

  • Dalvik编译生成的是.dex文件,这种格式的文件体积更小。而JVM规范的是.class文件。
  • Dalvik虚拟机是基于寄存器的,而JVM规范是基于栈的,所以速度方面会有优势。比如上面的说的标准Java虚拟机中,它的虚拟机栈就为线程的运行提供了服务。而Dalvik虚拟机中,使用寄存器去储存运行指令,同时寄存器也提供了程序计数器。寄存器是处理器的一部分哦!
  • Dalvik虚拟机允许在内存中创建都个实例,以隔离不同的应用程序。这样,当一个应用程序在自己的进程中崩溃后,不会影响其它进程的运行。

关于ART虚拟机

ART虚拟机在Android 5.0以后是被默认开启的,此时Dalvik已经被Google放弃维护了。它与Dalvik虚拟机的不同:

  • ART虚拟机在应用程序安装时就会把字节码通过dex2oat工具直接转成机器码储存,这个过程叫做AOT(Ahead-Of-Time)。而Dalvik是在每次启动应用程序时,通过传统的JIT(JUST IN TIME)模式将字节码转成机器码。显然,这样速度会慢不少。当然,ART虚拟机的占用内存也会更大些。
  • ART虚拟机在进行GC时采用了并法的模式。
    • 在传统的GC模式下,当虚拟机触发一次GC,会先暂停所有线程,然后检查所有对象,将符合回收条件的对象进行标记,然后进行回收,最后再恢复线程,这样的话gc速度会快些,但是遇到内存抖动,就会卡顿了。同时,传统的GC算法导致了【内存碎片化】严重,在一次回收后,很多内存块都会出现不连续的情况,这样会导致寻址变得困难,从而拖慢程序运行速度。
    • 而ART虚拟的垃圾回收算法允许GC时对对象的标记和一些对象的清理工作并发进行。同时,ART引入了【移动垃圾回收器】技术,使得碎片化内存能够被对齐,从而能稍微节约一些内存空间。

总结

  • Heap Segment被划分为两块:Young GeneriationOld Generiation
  • Young Genertiation中又被划分为Eden区和两个Survivor区,对象在其中采用标记复制算法来判定一个对象是应该清理还是移到Old Generiation中。该内存区域发生GC的频率较高。
  • Old Generiation发生GC的频率相对较低。当有大对象被创建,或者和该区域有关的对象被创建时,它将会被直接移动到该区域中。

看到这里的童鞋快奖励自己一口辣条吧!

想要看CoorChice更多的文章,可以加个关注哦!

关注下面的标签,发现更多相似文章
评论