众所周知,GC、内存泄漏、OOM都是Android面试的考点,我参照着深入理解Java虚拟机做一个笔记,以防以后需要。

内存区域

 1.程序计数器(Program Counter Register)

  这里和深大的“计系三连”联系起来;

  PC,在MIPS里下一条执行的指令,在JVM里对应的是下一条需要执行的字节码指令,分支(Branch)、循环(Loop)、跳转(Jump)、异常处理(Exception)、线程恢复等基础功能都需要依靠PC(这里响起了wy老师的话“这里很重要,考试百分之百会考,你懂我意思吧”)

  PC这类内存区域是“线程私有的”:因为JVM的多线程是通过线程切换并分配处理器执行时间的方式实现的,所以在一个时刻,处理器只能处理一条线程中的指令,为了能确保线程切换后能恢复到正确的位置,每一个线程都应该拥有一个PC,告诉处理器下一条指令的位置。

 2.Java虚拟机栈(Java Virtual Machine Stacks)

  虚拟机栈也是线程私有的,同时它的声明周期和线程生命周期相同;

虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧,用于储存局部变量表、操作数栈、动态链接、方法出口等信息,每个方法的调用到执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。

  这深入理解JVM里对虚拟机栈的描述和我们当时学计算机系统2时对X86-64里C语言的机器级表示里的栈帧结构十分相似,一个方法里申请的基本类型的局部变量的空间大小在编译阶段就可以确定的,所以在调用一个方法的时候,就知道这个方法对应的栈帧需要多大的局部变量空间(局部变量表确定),在方法执行的时候,局部变量表的大小是不会改变的。


Java & Android 内存管理
LG老师的计系2PPT

  StackOverflowError:在JVM里,如果线程请求的栈深度大于虚拟机栈所允许的深度,就会跑出StackOverflowError异常(常见于递归深度过大,别问我为什么知道......也是LG老师的算法课教我做人);
  OutOfMemoryError:在可以动态拓展的虚拟机栈拓展时,无法申请到足够的内存,就会抛出OutOfMemoryError异常;

 3.本地方法栈(Native Method Stack)

  本地方法栈和虚拟机栈的作用非常相似,区别是虚拟机栈是为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用的Native方法服务......(这里暂时没搞懂Native方法是什么,mark一下,老师说过“最好的学习方法不是一开始就什么都去弄懂,而是先把东西记下来”)

 4.Java堆(Java Heap)

  一般来说Java Heap是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,唯一的目的是存放对象实例;

Java虚拟机规范的描述:所有对象的实例以及数组都要在堆上分配;

  Java堆是垃圾回收回收的主要区域,所以也被叫做“GC堆”(Garbage Collected Heap),垃圾回收可以说是一个很重要的点了,放在后面再介绍;如果堆没有内存来完成实例分配,就会抛出OutOfMemoryError异常;

 5.方法区(Method Area)

(对应X86-64的只读段?)


Java & Android 内存管理
引用“深入理解Java虚拟机”
 6.运行时常量池(Runtime Constant Pool)
Java & Android 内存管理
引用“深入理解Java虚拟机”
 7.直接内存(Direct Memory)
Java & Android 内存管理
引用“深入理解Java虚拟机”

垃圾回收机制(GC - Garbage Collection)

“深入理解Java虚拟机”里明确指出:当需要排查内存溢出和内存泄漏问题,就必须了解GC和内存分配,所以在这里先mark一下GC,再讲内存泄漏;

首先,Garbage Collection得确定什么是Garbage,书里列举了两种方法:

  1. 引用计数算法
    原理:每个对象都有一个引用计数器,记录着有多少地方在引用它,当引用计数器为0的时候,这个对象就已经“死”掉了(正如寻梦环游记:真正的死亡是世界上再也没有人记住你)
    优点:实现简单、高效,FlashPlayer、Python使用
    缺点:无法克服循环引用,如a.obj = b.obj = a;
  1. 可达性分析算法
    原理:通过一系列成为“GC Root”的对象作为根节点,往下搜索,搜索的路径称为“引用链”,在这个引用链上的节点是“活”的,GC Root不可达的对象就是“死”的;
    GC Root:
      虚拟机栈中引用的对象
      方法区中静态属性引用的对象
      方法区中常量引用的对象
      本地方法栈中JNI引用的对象

知道了哪些对象是Garbage之后,就得明确垃圾回收算法了,这里列举四类:

  1. 标记-清除算法
     标记清除算法是最基础的回收算法,后面的算法都是对它的改进;
     第一阶段:利用上面的算法来确认哪些是需要回收的对象
     第二阶段:标记完后,统一回收所有被标记的对象
     缺点:
      1. 效率低,无论是标记还是回收的效率都不高
      2. 内存碎片化,标记清除后,会出现大量的不连续的内存碎片,很有可能在后面申请较大空间的时候找不到一块连续的内存空间,从而出发新一轮的垃圾回收
  1. 标记-整理算法
     标记-整理算法的是对标记-清除算法的一种改良
     第一阶段:和标记-清除算法一样,都是标记
     第二阶段:整理,将仍存活的对象统一往一端移动,然后清除掉端边界外的内存
  1. 复制算法
     将内存分为相同大小的两块,每次只使用其中的一块,当这一块的内存用完了,就将这一块里存活的对象复制到另一块里,然后将这一块一次性清除
     优点:这样做每次只需要对整个半区进行垃圾回收,也能解决内存碎片化的问题
     缺点:将内存缩减为原先的一半
      IBM公司研究表明,新生代中的对象98%都是“朝生暮死”,所以不需要按照1:1来划分内存空间,取而代之的是划分一个比较大的Eden空间和两个比较小的Survivor空间,每当回收时,将Eden和一块Survivor里的存活对象复制到另一块Survivor里,然后将Eden和第一块Survivor清空,HotSpot虚拟机里默认Eden和Survivor的大小比例是8:1,所以被浪费的内存只占10%;
  1. 分代收集算法
     当代商用的虚拟机都采用“分代收集”算法,根据对象存活的周期将内存分为几块,一般把Java堆分为新生代和老年代,各个年代各自采取合适的算法:
      新生代对象存活率低,每次回收都发现大量对象死去,所以选用复制算法
      老年代对象存活率高,而且没有额外的空间为它担保,所以只能选用标记-清除算法或者-标记整理算法

常见的内存泄漏

1.静态全局变量持有对象的强引用
2.非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,这些类做耗时操作,且超出Activity声明周期,则Activity内存泄漏
3.系统服务在Activity销毁的时候没有注销,如传感器、广播接收器

  1. 有待发现。。。。。

相关文章: