[安卓开发日记] 内存模型和GC
一 、JAVA内存主要分5块:堆 - 方法区 - 程序计数器 - 本地方法栈 - 虚拟机栈
-
堆 和 方法区 这两块内存是共享的。 程序计数器 + 本地方法栈 + 虚拟机栈 这3快内存加起来是组成了线程栈。每个线程都由自己的线程栈,自然线程栈就是私有的了,一个进程的内存中随着线程数量的增加,会有多个线程栈内存出现。
-
程序计数器 - 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
-
本地方法栈 - 原生方法就是用 Java 调非 Java 代码的接口,方法的实现由非 Java方法实现,比如C和C++。
-
Java虚拟机栈 - 线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息
-
堆内存 - 内存中最大的一块,被所有线程共享,存放的是对象实例,堆内存若是细分的话还可以细分为 新生代、老年代,新生代里还可以细分为:Eden、From Survivor、To Survivor,这是因为 GC 自动回收机制回收的就是堆内存,划分的这么清楚就是为了提高堆内存回收效率
-
方法区 - 线程共享的,线程安全的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,常量池和静态池也在方法区内。
二、堆内存和GC
堆内存可以分为新生代、老年代,新生代里还可以细分为:Eden、From Survivor、To Survivor。大对象会直接分配到老年区。
可达性算法
通过一系列的"GC ROOTS" 的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。
GC ROOTS包含
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
- Thread - 活着的线程
- Stack Local - Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
- Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
GC算法
新生代区域 Eden 、From Survivor、To Survivor 内存比例为1:1:8
新创建对象一般加入Eden区,经历第一次GC存活后对象,加入Survivor区,Survivor区每次GC后年龄加一,到达15岁后,被放入老年代
如果创建对象过大会直接放入老年代
- 复制算法
所有的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。
其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。由于年轻代中的对象生命周期往往很短,所以当需要进行GC的时候就将当前90%内存中存活的对象复制到另外一块Survivor中,原来的Eden与Survivor将被清空。但是这就有一个问题,我们无法保证每次年轻代GC后存活的对象都不高于10%。所以在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即无法复制到Survivor中的对象将移动到老年代。 - 标记清除算法
算法分为2个阶段:1.标记需要回收的对象,2.回收被标记的对象。一般用于老年代 - 标记整理算法
第一步通过标记需要回收的对象,第二 布将存活的对象向内存一端移动,其余的对象回收,整理出一块较大的连续内存空间。一般在老年代没有办法找出连续空间存放一个对象,但是总剩余空间又足够放下该对象的时候,需要进行标记整理算法