Java内存机制以及Android内存优化

Java内存机制

1. 虚拟机运行时数据区

基本概念

虚拟机
模拟某种计算机体系结构,执行特定指令集的软件。包括进程虚拟机和系统虚拟机(VMWare)

  • 进程虚拟机:JVM、Adobe Flash Player、FC模拟器
    • 高级语言虚拟机:JVM、.NET CLR、P-Code
      • Java语言虚拟机:JVM、Apache Harmony
        • Java(TM)虚拟机
          Java(TM)虚拟机并不是只能执行Java程序
          三大商用JVM:Oracle Hotspot、Oracle JRockit Vm、IBM J9 VM
          • Oracle HotSpot虚拟机
            Oracle JDK自带的虚拟机。HotSpot命名来自它的“热点代码探测”技术。
            每一个Java程序都对应一个Java虚拟机实例。

公有设计,私有实现
《Java虚拟机规范》(JVMS)定义了概念模型,不约束虚拟机的具体实现。
Java虚拟机运行时数据区
在《Java虚拟机规范》中定义了若干种程序运行期间会使用到的存储不同类型数据的区域。
有一些区域是全局共享的,随着虚拟机启动而创建,随着虚拟机退出而销毁。有一些区域是线程私有的,随着线程开始和结束而创建和销毁。
是所有Java虚拟机共同的内存区域概念模型。

既然虚拟机作为一个虚拟的计算机, 来执行我们的程序, 那么在执行的过程中, 必然要有地方存放我们的代码(class文件); 在执行的过程中, 总会创建很多对象, 必须有地方存放这些对象; 在执行的过程中, 还需要保存一些执行的状态, 比如, 将要执行哪个方法, 当前方法执行完成之后, 要返回到哪个方法等信息, 所以, 必须有一个地方来保持执行的状态。 上面的描述中, “地方”指的当然就是内存区域, 程序运行起来之后, 就是一个动态的过程, 必须合理的划分内存区域, 来存放各种数据。 所以, 在本文中, 将会详细介绍JVM的运行时数据区。

运行时数据区的划分


分为程序计数器、Java堆、Java虚拟机栈、本地方法栈、方法区

  • 线程私有的数据区包含程序计数器、虚拟机栈、本地方法栈。
  • 全局共享的是Java堆、方法区(包括常量池)和直接内存
  • 需要自动内存管理(GC)的区域:Java堆、方法区(不规定但实现了GC)、直接内存
  • 可能出现OOM的区域:Java堆、Java虚拟机栈、本地方法栈、方法区、直接内存
  • 可能出现StackOverFlow的区域:Java虚拟机栈和本地方法栈

程序计数器

这是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行Java方法,指数器记录的是正在执行的虚拟机字节码指令的地址;如果是native方法,指数器为空。
这是唯一一个没有OOM规定的区域。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法(不包括native方法)在执行时都会创建一个栈帧,用于存储局部变量,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行结束的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

在Java虚拟机规范中,对该区域内存规定了两种异常状况:

  • StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
  • OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出

本地方法栈

本地方法栈则为虚拟机使用到的Native方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如非常典型的Sun HotSpot虚拟机。

和虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。

它被用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。

在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。

一个完整的栈帧包括:局部变量表、操作数栈、动态连接信息、方法正常完成和异常完成信息

  • 局部变量表
    是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。其大小以slot为最小单位(32位)。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。它存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用,以及returnAddress类型(指向了一条字节码指令地址)
    局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型的值和对象的引用。
  • 操作数栈
    操作数栈也常被称为操作栈,它是一个后入先出栈,由若干个Entry组成。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的max_stacks数据项中。
    在方法执行过程中,栈帧用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果

举例

Java堆

  1. 全局共享
  2. 通常是Java虚拟机中最大的一片内存区域
  3. 是Java对象的存储区域
  4. 需要实现自动内存管理,即GC,但不限制实现方式
  5. 可能出现OOM异常

Java堆、栈的关联过程:两种方式

 


方式一的优点是访问速度更快,因为方式二需要两次指针定位。方式二的优点是,Java堆中的对象经常会变化(GC),此时只需要修改句柄池中的引用即可,比较方便。目前第一种方式比较流行。

方法区和运行时常量池

  1. 全局共享
  2. 作用是存储类的结构信息
  3. JVMS不要求进行内存管理,但商用Java虚拟机都实现了自动内存管理
  4. 运行时常量区是方法区的一部分,作用是存储Java类文件常量池中的符号信息
  5. 可能出现OOM异常

直接内存

  1. 并非JVMS定义的标准内存区域
  2. 随JDK1.4加入的NIO引入,目的是避免在Java堆和Native堆中来回复制数据带来的性能损耗
  3. 全局共享
  4. 能被自动管理,但检测手段比较简陋
  5. 可能出现OOM异常

2. 对象判定和回收算法

可回收对象的判定方法

1. 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,引用计数器的值加1;引用失效时,值减1;计数器为0的对象可以被回收。

缺点:循环引用
OC语言的解决方式:强引用和弱引用。弱引用不会增加引用计数。
2. 可达性分析算法
由于引用计数算法的缺陷,Java虚拟机通常使用这种判定方法。

通过一系列称为“GC Root”的对象作为起始点,从这些起点向下搜索,搜索路线称为“引用链”,如果一个对象到GC Roots没有任何引用链,则判定不可用。


Java中GC Roots:

  1. 虚拟机栈中(本地变量表中)引用的对象
  2. 方法区的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI引用的对象

垃圾收集算法

1. 标记-清除算法
首先标记出所有需要回收的对象,标记完成后统一清除。
缺陷:产生大量不连续的内存碎片


2. 复制算法
将可用内存分为相等的两块,每次只使用其中一块,当一块内存用完了,就将还活着的对象复制到另一块内存中,再把这块内存中的所有对象清除掉。
缺陷:将内存缩小了一半、复制开销
改进:不一定需要分为大小相等的两块,可以适当提高可用内存比例。


3. 标记-整理算法
标记过程与“标记-清理算法”一样,然后让所有存活对象向一端移动,最后清理掉边界以外的对象。
缺陷:系统停顿时间更长(移动消耗)


4. 分代算法
当今商用Java虚拟机共同采用的算法。

根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,然后在不同内存区域采用不同的垃圾收集算法。比如新生代,会产生大量垃圾对象,适合采用复制算法(需要复制的对象较少),而老年代可以采用标记-清理或者标记-整理算法。

HotPot虚拟机的算法实现

GC Roots枚举


安全点和安全区

Android内存优化

1. Android内存管理机制

Android系统内存分配和回收

  • 一个APP通常就是一个进程对应一个虚拟机
  • GC只有在Heap剩余空间不够时才触发垃圾回收
  • GC触发时,所有线程都会被暂停(可能导致内存抖动)

APP内存限制机制

  • 不同设备分配的内存限制不同
  • 吃内存大户:图片

切换应用时后台APP清理机制

  • APP切换的LRU cache:最近使用的APP最不可能被清理
  • onTrimMemory()回调

监控内存的方法

  • Android方法
  • AS monitor工具

2. Android内存优化方法

数据结构优化

  1. 字符串拼接使用StringBuilder:时间和空间性能都极大优于String
  2. ArrayMap、SparseMap替换HashMap:时间和空间优化
  3. 内存抖动:重复大量申请对象
  4. 再小的class会耗费0.5kb
  5. HashMap一个entry需要额外占用32b

对象复用

  1. 复用系统自带的资源
  2. listview、GridView复用
    3.避免在onDraw中创建对象(因为onDraw经常会调用,如果创建对象耗时、耗内存,会造成卡顿)

避免内存泄漏
内存泄漏会导致可用内存越来越少,频繁造成GC

  1. 单例模式使用了Activity的context(单例对象常驻内存),应该使用Application的context
  2. 静态变量引用Activity、view等对象没有及时释放(静态变量常驻内存)
  3. 非静态内部类和匿名内部类会持有外部类(Activity)对象,因此内部类里不能有静态引用或耗时任务。否则不能用这两种内部类。
  4. Handler:message可能等待很长时间,在处理之前,message都持有handler引用,而handler持有Activity引用。导致泄漏。解决方式是将handler声明为静态,并将Activity的弱引用传给它。或者在Activity结束时同时移除message。
  5. 资源未关闭造成的内存泄漏
    对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。参考http://waylenw.github.io/Android/android-bitmap-memory-yh/

3. OOM优化

当APP申请的内存空间大于系统为APP分配的空间时会出现OOM

  1. 调整图像大小后再放入内存
  2. 采用强引用+软引用2级缓存,提高加载性能
  3. 及时回收图像
  4. 不要创建过多的静态变量

参考文章

  • Java运行时数据区

你可能感兴趣的:(Android,Java,读书笔记)