JVM内存划分

StackOverflowError发生在虚拟机栈中。
OutOfMemoryError可能发生在虚拟机栈、堆、方法区。
GC Root 对象
在 Java 中,有以下几种对象可以作为 GC Root:
1.Java 虚拟机栈(局部变量表)中的引用的对象。(局部变量引用的对象)
2.方法区中静态引用指向的对象。(全局静态变量,全局变量不是GC root)
3.仍处于存活状态中的线程对象。
4.Native 方法中 JNI 引用的对象。
垃圾回收时机
1.Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,会触发一次 GC。
2.System.gc():主动调用此 API 来请求一次 GC。
垃圾回收算法
1.标记清除算法(Mark and Sweep GC)

优点:实现简单,不需要将对象进行移动。
缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
2.复制算法(Copying)
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。


优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制
3.标记-压缩算法 (Mark-Compact)
需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。

优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
分代回收策略
1.新生代(Young Generation)
新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。绝大多数刚刚被创建的对象会存放在 Eden 区。

当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1是空的。

下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden和 S0区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0变为空。

如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

老年代(Old Generation)
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。
GC Log 分析
为了让上层应用开发人员更加方便的调试 Java 程序,JVM 提供了相应的 GC 日志。在 GC 执行垃圾回收事件的过程中,会有各种相应的 log 被打印出来。其中新生代和老年代所打印的日志是有区别的。
新生代 GC:这一区域的 GC 叫作 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。
引用类型

软引用隐藏问题
需要注意的是,被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。
编译插桩
1.在 .java 文件编译成 .class 文件时,APT、AndroidAnnotation 等就是在此处触发代码生成。
2.在 .class 文件进一步优化成 .dex 文件时,也就是直接操作字节码文件。这种方式功能更加强大,应用场景也更多。但是门槛比较高,需要对字节码有一定的理解。(AspectJ,ASM)

一般情况下,我们经常会使用编译插桩实现如下几种功能:
日志埋点;
性能监控;
动态权限控制;
业务逻辑跳转时,校验是否已经登录;
甚至是代码调试等。
类加载
通常情况下,Java 程序中的 .class 文件会在以下 2 种情况下被 ClassLoader 主动加载到内存中:
1.调用类构造器
2.调用类中的静态(static)变量或者静态方法
双亲委派机制
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。
注意:“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。
Android 中的 ClassLoader
在 Android 虚拟机里是无法直接运行 .class 文件的,Android 会将所有的 .class 文件转换成一个 .dex 文件,并且 Android 将加载 .dex 文件的实现封装在 BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。
1.PathClassLoader 用来加载系统 apk 和被安装到手机中的 apk 内的 dex 文件。
2.DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。
Class初始化过程

装载
装载是指 Java 虚拟机查找 .class 文件并生成字节流,然后根据字节流创建 java.lang.Class 对象的过程。
这一过程主要完成以下 3 件事:
1)ClassLoader 通过一个类的全限定名(包名 + 类名)来查找 .class 文件,并生成二进制字节流:其中 class 字节码文件的来源不一定是 .class 文件,也可以是 jar 包、zip 包,甚至是来源于网络的字节流。
2)把 .class 文件的各个部分分别解析(parse)为 JVM 内部特定的数据结构,并存储在方法区。
在这里 JVM 会将这些 .class 文件的结构转化为 JVM 内部的运行时数据结构。这点同 JSON 解析过程有点类似:如果你做过 Android 开发,应该都使用过 GsonFormat 将后端开发返回的 JSON 结构转化为一个运行时 Bean 类,当程序运行时使用这个 Bean 类去解析处理 JSON 数据。
3)在内存中创建一个 java.lang.Class 类型的对象:
接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个 Class 类型的类对象是提供给外界访问该类的接口。
加载时机
隐式装载:在程序运行过程中,当碰到通过 new 等方式生成对象时,系统会隐式调用 ClassLoader 去装载对应的 class 到内存中;
显示装载:在编写源代码时,主动调用 Class.forName() 等方法也会进行 class 装载操作,这种方式通常称为显示装载。
验证
文件格式检验:检验字节流是否符合 class 文件格式的规范,并且能被当前版本的虚拟机处理。
元数据检验:对字节码描述的信息进行语义分析,以保证其描述的内容符合 Java 语言规范的要求。
字节码检验:通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。
符号引用检验:符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
准备
为类中的静态变量分配内存,并为其设置“0值”。
Java 中基本类型的默认”0值“如下:
基本类型(int、long、short、char、byte、boolean、float、double)的默认值为 0;
引用类型默认值是 null;
解析
把常量池中的符号引用转换为直接引用,也就是具体的内存地址。
初始化
这一阶段是执行类构造器<clinit>方法的过程,并真正初始化类变量。
在准备阶段 value 被分配内存并设置为 0,在初始化阶段 value 就会被设置为 具体值。
初始化的时机
对于装载阶段,JVM 并没有规范何时具体执行。但是对于初始化,JVM 规范中严格规定了 class 初始化的时机,主要有以下几种情况会触发 class 的初始化:
1.虚拟机启动时,初始化包含 main 方法的主类;
2.遇到 new 指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作;
3.当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作;
4.子类的初始化过程如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
5.使用反射 API 进行反射调用时,如果类没有进行过初始化则需要先触发其初始化;
6.第一次调用 java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。
在初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有 static 关键字修饰的信息,而没有 static 修饰的语句块在实例化对象的时候才会执行。
上述的 6 种情况在 JVM 中被称为主动引用,除此 6 种情况之外所有引用类的方式都被称为被动引用。被动引用并不会触发 class 的初始化。
最典型的就是在子类中调用父类的静态变量,则不会初始化 Child 类。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类 Child 来引用父类 Parent 中定义的静态字段,只会触发父类 Parent 的初始化而不会触发子类 Child 的初始化。
class 初始化和对象的创建顺序
静态变量/静态代码块 -> 普通代码块 -> 构造函数
- 父类静态变量和静态代码块;
- 子类静态变量和静态代码块;
- 父类普通成员变量和普通代码块;
- 父类的构造函数;
- 子类普通成员变量和普通代码块;
- 子类的构造函数。
架构基于寄存器&基于栈堆结构
JVM 的指令集是基于栈结构来执行的;而 Android 却是基于寄存器的,不过这里不是直接操作硬件的寄存器,而是在内存中模拟一组寄存器。Android 字节码和 Java 字节码完全不同,Android 的字节码(smali)更多的是二地址指令和三地址指令。基于寄存器的指令明显会比基于栈的指令少,虽然增加了指令长度但却缩减了指令的数量,执行也更为快速。用一张表格来对比基于栈和基于寄存器的实现方式如下:

DVM 内存管理与回收
Dalvik 虚拟机中的堆被划分为了 2 部分:Active Heap 和 Zygote Heap。
为什么要分 Zygote 和 Active 两部分?
Android 系统的第一个 Dalvik 虚拟机是由 Zygote 进程创建的,而应用程序进程是由 Zygote 进程 fork 出来的。
Zygote 进程是在系统启动时产生的,它会完成虚拟机的初始化,库的加载,预置类库的加载和初始化等操作,而在系统需要一个新的虚拟机实例时,Zygote 通过复制自身,最快速的提供一个进程;另外,对于一些只读的系统库,所有虚拟机实例都和 Zygote 共享一块内存区域,大大节省了内存开销。

网友评论