Android 面试 - JVM 知识

128 阅读9分钟

JVM的定义

  • JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
  • 引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

JVM执行程序的过程

  1. 加载.class文件
  • Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。
  1. 管理并分配内存
  • 程序计数器、虚拟机栈、本地方法栈、Java堆、方法区。
  1. 执行垃圾收集
  • 分代收集算法、复制算法、标记-清除算法、标记-整理算法。

Java类的加载过程

类加载.jpg

Java 类加载器

  • 启动类加载器(Bootstrap Classloader):负责将 <JAVA_HOME>/lib 目录下并且被虚拟机识别的类库加载到虚拟机内存中。
  • 扩展类加载器(Extention Classloader):负责加载JVM扩展类,jar包位于 <JAVA_HOME>/lib/ext 目录中。
  • 系统类加载器(Application Classloader):它负责加载用户路径(ClassPath)上所指定的类库,也就是说,我们自己编写的代码以及使用的第三方的jar包都是由它来加载的。
  • 自定义加载器(Custom Classloader):通常是我们为了某些特殊目的实现的自定义加载器,通过继承ClassLoader实现。

双亲委派机制

若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。 只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

类的加载过程

  1. 加载过程
  • 通过全类名获取定义此类的二进制字节流。
  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  • 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。
  1. 链接过程
  • 验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
  • 准备:为类的静态变量分配内存,并将其初始化为默认值。
  • 解析:虚拟机将常量池中的符号引用替换为直接引用的过程。
  1. 初始化
  • 初始化过程就是调用类初始化方法,为类的静态变量赋予正确的初始值。

JVM内存区域划分

内存模型.jpg

运行时数据区被分为线程私有数据区和线程共享数据区两大类。

  • 线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈
  • 线程共享数据区包含:Java堆、方法区

程序计数器

  • 是当前线程所执行的字节码的行号指示器。
  • 如果线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址。 如果线程正在执行的是一个Native方法,那么计数器的值则为空。
  • 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此它是线程私有的内存。
  • 在Java虚拟机规范中,是唯一一个没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

  • Java方法执行的内存模型
  • 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 是线程私有的内存,与线程生命周期相同。
  • 一般把Java内存区分为堆内存(Heap)和栈内存(Stack),其中指的是虚拟机栈,指的是Java堆。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状况
  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果虚拟机栈可动态扩展且扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。

本地方法栈

  • 是虚拟机使用到的Native方法服务。
  • 在虚拟机规范中,对这个区域无强制规定,由具体的虚拟机自由实现。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆

  • 用于存放几乎所有的对象实例和数组。
  • 被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 是垃圾收集器管理的主要区域,也被称做“GC堆”。
  • 是Java虚拟机所管理的内存中最大的一块。
  • 可处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
  • 在Java虚拟机规范中,如果在堆中没有内存完成实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

方法区

  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 与Java堆一样,是各个线程共享的内存区域。
  • 和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可选择不实现GC。
  • 在Java虚拟机规范中,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

Java四种引用类型

强引用(StrongReference)

  • 具有强引用的对象不会被GC;
  • 即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。

软引用(SoftReference)

  • 只具有软引用的对象,会在内存空间不足的时候被GC,如果回收之后内存仍不足,才会抛出OOM异常;
  • 软引用常用于描述有用但并非必需的对象,比如实现内存敏感的高速缓存

弱引用(WeakReference)

  • 只被弱引用关联的对象,无论当前内存是否足够都会被GC;
  • 强度比软引用更弱,常用于描述非必需对象。

虚引用(PhantomReference)

  • 仅持有虚引用的对象,在任何时候都可能被GC;
  • 常用于跟踪对象被GC回收的活动;
  • 必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

对象存活判定算法

1、引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

  • 然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题。

2、可达性分析法

通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

可作为GC Roots的对象:
  • 虚拟机栈中引用的对象,主要是指栈帧中的本地变量
  • 本地方法栈中Native方法引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

垃圾收集算法

分代收集算法

根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。新生代(少量存活)用复制算法,老年代(对象存活率高)使用标记—清理算法或者标记—整理算法

复制算法

  • 把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象复制到另外一块上面,再将这一块内存空间一次清理掉。
  • 优点:每次都是对整个半区进行内存回收,无需考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 缺点:每次可使用的内存缩小为原来的一半,内存使用率低。

标记-清除算法

  • 首先标记出需要回收的对象,在标记完成后统一清除掉所有的被标记对象。
  • 缺点:效率问题和空间问题(标记清除后会产生大量的不连续内存碎片,内存碎片过多可能会导致程序需要分配较大对象时找不到足够大的连续内存空间而不得不提前触发另一次垃圾回收动作)

标记-整理算法

  • 首先标记出所有需要回收的对象,然后进行整理,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
  • 优点:即没有浪费50%的空间,又不存在空间碎片问题,性价比较高。一般情况下,老年代会选择标记-整理算法。