Android知识点总结(五):Java 虚拟机原理

164 阅读20分钟

1、描述JVM类加载过程

加载 验证 准备 解析 初始化,五虎上将。当然有特殊情况,java运行时绑定,解析在初始化之后

  • 加载
    • 类加载器从文件系统或者网络读取.class文件
    • 相当于顺丰快递员收件
  • 验证
    • JVM验证被加载类是否符合规范
    • 快递小哥验包裹能不能邮寄
  • 准备
    • 为静态变量分配内存并初始化,比如public static int a = 8初始化后依旧是0
    • 小哥给快递准备个盒贴个条准备邮寄
  • 解析
    • 根据常量池中的符号引用转为直接引用(比如物理内存地址指针)
    • 根据地址寄给收件人
  • 初始化
    • 执行类构造器()主要是给静态变量和常量赋值,并执行静态代码块
    • 包括签收,能被收件人使用了

类的二进制形式就被加载到Java虚拟机中,就可以被Java虚拟机使用

2、请描述new一个对象的流程

2.0 顺序描述

  • 类加载-》检查加载-》分配内存-》内存空间初始化-》设置-》对象初始化

2.1 类加载

  • 上一个问题已经说了

2.2 检查加载

  • 检查指令参数能否在常量池中定位到一个类的符号引用,并且检查类是否已经被加载、解析和初始化过。
  • 类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址。

2.3 分配内存

  • 检查完之后,虚拟机将新生对象分批额内存,就是在java堆中划一个道,确定大小的内存从java堆中划分出来。
2.3.1 指针碰撞
  • 如果java堆内存绝对规整,用过在一边,没用过另外一边,分配内存就是把指针向空闲的那边挪一段于对象大小相等的距离。
2.3.2 空闲列表
  • java堆中已使用和未使用内存相互交错,没办法简单指针碰撞,那就jvm维护一个表,记录上哪些内存块可以用,分配的时候挑一块大的划拉,并更i性能记录。这就是空闲列表分配
2.3.3 并发安全
  • 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
  • 分配内存控件用的CAS保证操作更新原子性
  • TLAB,ThreadLocalAllocationBuffer,每个线程都有一块自己的本地线程分配缓存。避免线程冲突。
  • 具体来说,当线程需要分配对象时,首先检查TLAB是否有空间,如果有空间,就用CAS保证TLAB的访问和修改时原子的,CAS回先比较TLAB的头部指针和当前线程指针头部是否一致,不一致说明其它线程修改过TLAB。重试或者抛出异常,如果一致就TLAB的头部指针指向下一个可用空间,并将对象分配到TLAB中。当一个TLAB用满时,JVM会为该线程申请一个新的TLAB,以继续进行对象的分配。TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。

2.4 内存空间初始化

  • 内存分配完,jvm把分配到空间的东西都初始化为零值,比如int a;你用它就是0,boolean b;是false。

2.5 设置

  • 设置就是,对象是哪个类的实例,如何才能找到类额元数据信息、对象哈希码、对象GC分代年龄信息,这些信息存到对象头。

2.6 对象初始化

  • 调用构造方法把值都整好,程序员才能 用。

3、Java对象会不会分配到栈中

  • 当一个对象被jvm逃逸分析,出了这个方法就凉了,不可逃逸,就会被jit优化到栈上分配。

4、GC的流程是怎样的?介绍下GC回收机制与分代回收策略

GC就是垃圾回收器,怎么判定是垃圾呢?有两种,一种是计数法,但是有两个垃圾相互引用,所以会有问题。我们着重介绍下可达性分析。

4.1、可达性分析

就通过一组名为“GC Root”的对象作为起点,从这些节点向下搜素,搜索走过的路径称为引用链,最后判断对象是否可达来是否可被回收 ,如果可达就不能被回收。
  • GC Root有哪些。
    • Native方法中 的JNI引用对象
    • 存活的线程对象
    • 方法区的常量引用对象
    • 方法区中静态应用指向的对象
    • 正在运行方法中的局部 变量所引用的变量。
  • 分为两组 存活的:线程、java虚拟机栈中的对象 一直在的:方法区中的常量、静态变量。JNI饮用的 对象。
  • 优点,不会向引用计数一样有循环引用的问题。
  • 缺点:在多线程情况下造成误报漏报。比引用为null没及时更新,就没被垃圾回收掉。严重的是漏报,已经从null引用变有gcroot引用了,但还是被回收了。不过GC前都有安全点,避免了这个问题。但我们还是要注意。一般有以下几种方式: - 安全点,默认的 - 比

4.2 垃圾回收算法

- 标记清除算法
    过程分为标记和清除。实现简单不需要移动对象。缺点,多内存碎片
- 标记整理算法
    多了一个移动,优点是避免内存碎片。但是所了一个移动也就是压缩操作,效率低
- 复制算法
    优点:按顺序分配内存,简单效率高,不用考虑内存碎片
    缺点:可能的内存大小缩小二原先的一半,存活率高时,会进行频繁复制。

4.3 分代回收策略

image.png image.png

- 新对象生成会 放到eden区域。
- 如果满了就会触发minor gc,把活着的对象放到survivor to里,把survivor from里活着的也拷到survivor to里,然后fromto换个名字。这样to 又是空的了。每次minor gc过后,或者的年龄+1;
- 默认15岁时,再经过minor gc就从Survivor 去老年代享福去了。如果时Survivor 的to满了,也会因为不到15岁就去了老年代。
- 老年代满了就会触发Major GC

4.4 总结

  • JVM一般是可达性分析来判定对象是否可回收的。对象会在哪个代际回收采用的不一样的垃圾回收算法。年轻代是复制算法,老年代是标记清除或者标记整理算法,因为java版本和垃圾回收器有所不同。
  • 之所以用分代策略:不同的生命周期的对象 可以 采用不同的手机方式,提高回收效率。有些对象活得短,有些活得长,如果一视同仁,那么每次都遍历整个堆,太费时间了。

5、Java对象如何晋升到老年代?

  • 新生代中eden满了会触发垃圾回收,此时会将from和eden中存活的对象放到to区,年龄加1。当年龄15岁,遇到minor gc就被送去老年代
  • 如果to满了,那么新从eden或者from来的对象会被送往老年代
  • 大对象直接进入老年代:多大由JVM参数 -XX:PretenureSizeThreshold=x 决定;
  • Survivor空间中相同年龄的所有对象总和大于Survivor一半的时候,年龄大于或者等于该年龄的对象直接进入老年代。

5.1空间分配担保

  • 空间分配担保是指,老年代进行空间分配担保。
  • 当MinorGC的时候,JVM会检查老年代的连续可用空间是否大于新生代所有对象的总空间。
    • 大于,则此次MinorGC是安全的
    • 小于,则再看HandlePromotionFailture
      • true 表示,检查历史晋升到老年代对象的平均大小,如果大于或者等于,则尝试一次MinorGC,但依旧又风险。
      • false或者小于。则进行Full GC。会标记整理。年龄会增加,年龄超过15会去老年代。

5.2总结

- 大对象直接进入老年代
- 年龄超过阈值
- 动态对象年龄判定
- 年轻代空间不足        

6、判断对象是否被回收,有哪些GC算法,虚拟机使用最多的是什么算法?

- Java利用GC让开发者不必再像C/C++手动回收内存,但不是有GC就万事大吉,再不了解GC机制算法的情况下,很容易内存泄漏,改释放的内存没释放,然后OOM。
- 有引用计数法,一次引用记一个数,没有引用就表示可被回收,但是呢,无法避免2个垃圾相互引用(循环引用)。所以被淘汰了。
- 可达性分析法,一个对象GCROOT可达就行,不会被回收。
    - GCroot有 运行中的虚拟机栈,存活的线程。本地方法栈、方法区中的常量、静态变量。

7、Class会不会被回收?用不到的Class怎么回收?

理论上会回收,但提条件比较苛刻。

  • 所有实例被回收,Java堆中不存在该类及其子类实例。
  • 该类的类加载器已经被回收。
  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类。

jvm允许堆满足上述三个条件的无用类进行回收,但并不是必然回收,至于是否回收,HotSpot虚拟机提供了 -Xnoclassgc参数禁用类的垃圾收集。

8、Java 有几种引用关系?

  • 强引用 - 就是我们平常用的 Object obj = new Object();
  • 软引用 - GC的时候不会释放,系统内存不足时会释放,一般用于缓存设计
  • 弱引用 - GC一来就释放,比如ThreadLocal,多数情况帮我们解决内存泄漏问题,比如Handler使用不当的内存泄漏问题,用静态内部类+弱引用。因为如果不这样子搞,handler直接用外部acitvity的变量。其中jvm中,是会把acitiviy传进来,那么msg持有acitivtiy,messagequeue持有msg,looper持有messagequeue,threadlocal持有looper,当前线程持有threadlocal。 那么就是gcroot可达 。gcroot包括 正在运行的方法中的局部变量、正在运行的线程。本地方法栈JNI引用的对象,方法区中的常量,静态变量。所以这样会造成内存泄漏。用了弱引用,当AMS对activity的强引用没了,那么handler对acitivty的弱引用,一gc就回收了。就不会让activity内存泄漏。
  • 虚引用 - GC回收改应用时可以得到一个通知,改应用不能直接引用,但可以在引用队列中观察到GC回收的对象 ,可以用于监听GC回收通知。

9、描述JVM内存模型

  • java时一次编写导出运行,只要在不同操作系统上装了对应的虚拟机,同一份代码就可以随意移植,写完java代码时,生成.java文件,java编译器编译为.CLASS文件通ClassLoader把类信息加载到 JVM当中,最后JVM再去调用操作系统。所以JVM只要正确执行.CLASS文件,就能实现跨平台。
  • jvm以线程为最小单位运行。class文件-》类加载器-》jvm。
    • jvm有线程共享区
        • 存放对象实例和数组。基本上都在这。当然有些栈上分配另说。堆也是JVM垃圾收集器管理的主要区域,堆中有新生代,老年代。比例1:2、新生代中有EDEN,survivor form和survivor to,比例为8:1:1。当堆没有可分配内存时,就会抛出OOM异常。
      • 方法区
        • 存储运行时常量池,已经被jvm加载的类信息、常量("hello"这样的字符串也是常量)、静态变量、即时编译器编译后的代码。也就是字节码和常量池。JDK8以前,HotSpot使用“永久代”来实现方法去的,其它虚拟机不存在这个概念。这样方法去可以和Java堆一样被HotSpot的垃圾回收器所管理,不需要单独处理。
    • 常量池
    • java文件被编译成class文件之后,
    • 包含类的版本、字段、方法、接口描述信息
    • class文件常量池,存放编译器生成的各种字面量
      • 存放编译器生成的各种字面量(字面量用于存储类加载时的类信息和运行时的动态常量,例如类名、成员变量、方法等信息,以及在运行时创建的动态常量)和符号引用
  • 运行时常量池
    • 当类加载到内存中
    • jvm就会把class文件常量池
    • 放到运行时常量池 - 可以通过-XX:MaxPermSize来设置永久代大小,如果用永久代来实现方法去,则有内存泄漏的风险。所以取消了永久代用元空间实现了方法区。元空间最大的区别就是,永久代时堆的一部分,和新生代、老年代地址是连续的,元空间不在虚拟机中,属于NativeMemory(本地内存),因此元空间大小仅受本地内存限制。 - 要知道常量池和运行时常量池的区别。常量池,就是Class文件常量池。是class文件的一部分呢,Java文件被编译成class文件之后,除了包含了类的版本、字段、方法、接口描述信息,还有一项信息叫class文件常量池。存放编译器生成的各种字面量(字面量用于存储类加载时的类信息和运行时的动态常量,例如类名、成员变量、方法等信息,以及在运行时创建的动态常量)和符号引用。运行时常量池就是,当类加载到内存中,jvm就会把class文件常量池中的内容(字面量和符号引用)存放到运行时常量池中。java的常量并不是在编译器才可以产生,运行期也可以产生新的常量并放入池中。

image.png

-jvm有线程独享区
    - 虚拟机栈
        - 储存方法运行时所需要的数据,成为栈帧
            -   **局部变量表**:用于存储方法中的局部变量。这些变量在方法执行期间被创建并存储在栈帧中,以便在方法执行期间随时访问。
            -   **操作数栈**:这是一个栈结构,用于存储计算过程中的中间结果。当需要进行计算或操作时,计算结果会被压入操作数栈中,以便后续的指令可以继续使用或计算。
            -   **方法返回地址**:当方法执行完毕后,需要返回调用者。这个返回地址会被压入栈帧中,以便在方法执行完毕后能够正确地返回到调用者。
            -   **动态连接**:这部分用于实现Java的动态绑定机制。在方法调用时,JVM会根据方法的实际类型来决定调用哪个方法,这个过程是通过动态连接来实现的。
    - 本地方法栈
        - 为JVM所调用的Native,及本地方法(JNI)服务
    - 程序计数器
        - 记录当前线程所执行到的字节码的行号

image.png

image.png

10、StackOverFlow与OOM的区别?分别发生在什么时候,JVM栈中存储的是什么?,堆中存储的是什么?

**每个线程在创建时都会创建一个本地方法栈、程序计数器、虚拟机栈,其虚拟机栈内部保存一个个的栈帧(stack Frame)**

  • StackOverFlow是栈空间不够造成的,主要是单个线程运行过程中调用方法过多或者是 方法递归操作 是申请栈帧使用空间草果了单个栈申请的储存空间,一般是1M.java 6.0以前是256kb。一般来说申请大对象会直接去
  • OOM主要是堆申请空间不够出现的,比如单次申请空间超过了堆中连续的可用空间。
  • 栈帧中有局部变量表、操作数栈、方法返回地址、动态连接。存一些对象引用和临时对象(小对象且没逃逸出这个栈帧)、基本数据类型。堆中主要用来存对象。

11、StringBuffer与StringBuilder在进行字符串操作时的效率。

  • 两者都是创建 引用指向对象,对象指向方法区中的常量池。但是StringBuffer中的方法都被synchronized,线程安全,因为有锁,所以效率第一点。多线程用StringBuffer,单线程用Stringbuilder效率高。
  • 用的 时候要注意。
 String  s = "hello";
 for(int  i = 0; i <10;i++){
     
                             //这里的意思是 s = new StringBuilder().append(s).append("k");频繁创建于回收会内存抖动,可能卡顿或者oom。
                             //应改为下述
                             s = s+"k";
    }
                             
  StringBuilder result = new StringBuilder("hello ");

for(int i = 0; i < 100; i++){

    //就不用频繁创建新对象了 
result.append("K");

}                           
    

12、JVM DVM 和ART的区别 (回想以下android apk打包流程)

12.1 JVM

  • JVM是基于栈的虚拟机,对于基于栈的虚拟机来说 ,没一个线程都是独立的栈,栈中记录了方法调用的历史,每一次方法调用,栈中便会多出一个栈帧。最顶部的栈帧表示当前帧,器代表了当前执行的方法,基于栈的虚拟机通过操作舒展进行所有操作。

image.png jvm 中执行字节码,把java代码。

int a = 1;

int b = 2;

int c = a + b;

编译为字节码,得到的指令为:

    ICONST_1 #将int类型常量1压入操作数栈

ISTORE 0 #将栈顶int类型值存入局部变量0

ICONST_2

ISTORE 1

ILOAD 0 #从局部变量表加载0int类型数据

ILOAD 1

IADD #执行int类型加法

ISTORE 2

数据会不断在操作数栈和局部变量表之间移动

12.2 DVM 也就是Dalvik vm

  • DVM是google专门为Android平台开发的虚拟机,它运行在Android运行时库当中。
  • Dvm是基于寄存器的虚拟机。上文提到jvm运行代码的时候, - 就是把数据压入操作数栈,讲栈顶数存入局部变量表,再把数据压入操作数栈,把栈顶数存入局部变量表,然后把他们从局部变量表中取到操作数栈中计算,再存到局部变量表当中。他们就不停往返在操作数栈和程序变量表之间。 - DVM不用来回跑.它没有操作数栈和局部变量表。但是又很多虚拟寄存器。所以无需来回跑,旨在一个寄存器里跑,所以指令更少。这些寄存器也存放在运行时栈中,本质上就是一个数组。
  • 字节码不一样
    • Dvm是APK包,dex文件,包含class文件。Jvm是JAR包,class文件。
    • .JAR文件包含多个.class文件每个.class文件包含了该类的常量池、类信息、属性。JVM按需加载,需要时再加载,比较慢。而dvm是预加载,dex讲多个类信息整合到仪器,多个类复用常量池等区域。那么使用时就比较快了,更符合移动端的需求。

12.3 ART

  • Android 4.4发布的,代替了Dalvik。4.4默认DVM,可以选择开启ART。Android 5.0起就默认用ART了。
  • DVM在安装过程中,会执行一次优化,把dex字节码优化为ODEX(提高启动和运行速度防反编译、但是增加空间)
  • ART能兼容Dalvik的字节码执行。但是引入了一个AOT(AHEAD OF TIME),在4.4~7.0以下的设备安装应用时,ART会使用DEX2OAT编译应用,把应用中的dex编译成本地机器码,但是这个过程会让应用安装变慢。
  • 在Android 7.0及之后的版本中,引入了JIT和AOT混合编译的机制。
    • 在安装应用时,为了提高安装速度,不会进行AOT编译,而是在运行时进行解释执行。对于经常执行的方法,JIT编译器会对其进行编译,并将编译结果保存到Profile配置文件中。当设备处于闲置或充电状态时,编译守护进程会运行,根据Profile文件中对常用代码进行AOT编译成本地机器码。这样,下次运行时就可以直接使用本地机器码,从而提高应用的性能。
    • 假设Object中有两个方法,A和B。如果A常用被JIT标记,然后被AOT编译为机器码。那下次Object编译为机器码的时候,只要将B方法编译为机器码。A方法不用编译成机器码了,因为A方法早就被编译了。

13、APK安装都讲了顺便说一下APK打包流程吧。

  • 安卓资源文件(如图片、音频、XML等)通过AAPT工具进行处理,生成对应的R.java文件和二进制资源文件。AAPT工具负责对资源文件进行打包,生成可用于应用程序加载和使用的二进制格式文件。 同时,对于.aidl文件,AIDL工具将其转换为一个Java接口文件,该接口文件定义了进程间通信的接口和方法。

  • 在准备好应用程序的其他Java文件后,这三类文件将一起送入Java编译器进行编译,生成对应的.class文件。

  • 随后,dx工具将把这些.class文件以及第三方库的.class文件合并成一个dex文件。这个过程是将Java字节码转换为Dalvik字节码的过程,以便在Android设备上运行。

  • 接着,APKBuilder工具将使用这些dex文件以及其他资源文件(如AndroidManifest.xml、res资源文件等)打包成APK文件。在打包过程中,还可以进行签名和压缩等操作。

  • 最后,jarsigner工具将对APK进行签名,以验证其完整性和真实性。签名后的APK可以被上传到Google Play Store或其他应用市场供用户下载安装。在发布之前,还可以使用zipalign工具对APK进行优化,确保其在Android设备上的正确加载和运行。

  • 总之,APK打包流程涉及多个步骤,包括资源文件的准备、AIDL处理、Java代码编译、DEX生成、APK打包和签名等。开发者需要合理配置和管理这些步骤,以确保生成的APK文件能够正常运行并提供预期的功能。同时,还需要注意各个步骤之间的依赖关系和顺序,以确保整个流程的顺利进行。

简单说

  • 安卓资源文件(图片、音频、XML等等)被AAPT(Android Asset Package Tool)生成对应的R.java文件和二进制资源文件。

  • .aidl文件被AIDL整成一个java接口文件

  • 准备App其它java文件

  • 这三类文件。被java编译器 编译成.class文件。

  • dex再把class文件和第三方库和第三方class编成一个dex文件。

  • apkbuillder把dex文件把其它资源文件和资源二进制文件打成apk

  • 再jarsigner给签名

  • 后再zipalign对对齐

  • 最终给出apk

image.png