JVM内存管理笔记

Posted iblade

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM内存管理笔记相关的知识,希望对你有一定的参考价值。

郑重声明:
本文属于个人学习笔记,严重参考姜新星大佬在拉勾教育平台的《android工程师进阶34讲》。严重参考警告,侵删。


JVM 的运行时内存结构中一共有两个“栈”和一个“堆”,分别是:Java 虚拟机栈和本地方法栈,以及“GC堆”和方法区(两栈一器,一堆一区)。除此之外还有一个程序计数器,但是我们开发者几乎不会用到这一部分。
JVM 内存中只有堆和方法区是线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

JVM 是基于栈的解释器执行的,DVM 是基于寄存器解释器执行的。
上面这句话里的“基于栈”指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧。
其中程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭;不存在垃圾回收问题。

可达性分析算法是从离散数学中的图论引入的。

在 Java 中,有以下几种对象可以作为 GC Root:

  • Java 虚拟机栈(局部变量表)中的引用的对象。
  • 方法区中静态引用指向的对象。
  • 仍处于存活状态中的线程对象。
  • Native 方法中 JNI 引用的对象。

何时回收:

  • Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  • System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。


Java 能够实现"一次编译,到处运行”,原因是,.class文件(字节码文件),有了字节码,无论是哪种平台(如:Mac、Windows、Linux 等),只要安装了虚拟机都可以直接运行字节码。
这种字节码可以不来自Java,还可以是Groovy、JRuby、Jython、Scala 等,因为这些语言经过编译之后也可以生成能够被 JVM 解析并执行的字节码文件。而虚拟机并不关心字节码是由哪种语言编译而来的。

编译插桩:
需求背景:记录每一个页面的打开和关闭事件,并通过各种 DataTracking 的框架上传到服务器,用来日后做数据分析。
方案①BaseActivity生命周期里处理,缺点是对第三方依赖库中的界面则无能为力,因为我们没有第三方依赖库的源码。
方案②Application.ActivityLifecycleCallbacks,在其回调生命周期里处理。
第三种方案:编译插桩——就是在代码编译期间修改已有的代码或者生成新代码。实际上,Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。

从上图可以看出,我们可以在 1、2 两处对代码进行改造。

1.在 .java 文件编译成 .class 文件时,APT、AndroidAnnotation 等就是在此处触发代码生成。(如Butterknife就是APT时期生成代码)

2.在 .class 文件进一步优化成 .dex 文件时,也就是直接操作字节码文件。

重点介绍第二种:

使用场景:①日志埋点;②性能监控;③动态权限控制;④业务逻辑跳转时,校验是否已经登录;⑤甚至是代码调试等。

目前市面上主要流行两种实现编译插桩的方式:
①AspectJ 是老牌 AOP(Aspect-Oriented Programming)框架。J2EE 中最常用,成熟稳定,使用者也不需要对字节码文件有深入的理解。
②通过 ASM 可以修改现有的字节码文件,也可以动态生成字节码文件,并且它是一款完全以字节码层面来操纵字节码并分析字节码的框架。汇编?

ASM 实现思路:
①遍历项目中所有的 .class 文件(要创建一个单独的 Gradle Plugin,并在 Gradle Plugin 中使用自定义 Transform 找出所有的 .class 文件。)
②遍历到目标 .class 文件 (Activity)之后,通过 ASM 动态注入需要被插入的字节码。

Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。

ASM 是一套开源框架,添加依赖即可,

 // ASM 相关依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

参考文章:https://www.jianshu.com/p/16ed4d233fd1

ClassLoader:负责将 .class 文件加载到 JVM 中。

在 Java 程序启动的时候,并非一次性加载程序中所有的 .class 文件,而是在程序的运行过程中,动态地加载相应的类到内存中。

通常情况下,Java 程序中的 .class 文件会在以下 2 种情况下被 ClassLoader 主动加载到内存中:①调用类构造器;②调用类中的静态(static)变量或者静态方法。

JVM 中自带 3 个类加载器:
①启动类加载器 BootstrapClassLoader
②扩展类加载器 ExtClassLoader (JDK 1.9 之后,改名为 PlatformClassLoader)
③系统加载器 APPClassLoader

以上 3 者在 JVM 中有各自分工,但是又互相有依赖。
①AppClassLoader 主要加载系统属性“java.class.path”配置下类文件,也就是环境变量 CLASS_PATH 配置的路径。因此 AppClassLoader 是面向用户的类加载器,我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
②ExtClassLoader 加载系统属性“java.ext.dirs”配置下类文件;
③BootstrapClassLoader 同上面的两种 ClassLoader 不太一样。它并不是使用 Java 代码实现的,而是由 C/C++ 语言编写的,它本身属于虚拟机的一部分。因此我们无法在 Java 代码中直接获取它的引用。如果尝试在 Java 层获取 BootstrapClassLoader 的引用,系统会返回 null。

双亲委派模式(Parents Delegation Model)
既然 JVM 中已经有了这 3 种 ClassLoader,那么 JVM 又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式。
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。

AppClassLoader 传入的 parent 就是 ExtClassLoader,而 ExtClassLoader 并没有传入任何 parent,也就是 null。

“双亲委派”机制只是 Java 推荐的机制,并不是强制的机制。我们可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。

自定义ClassLoader
如果我们想加载其他特殊位置下的 jar 包或类时(比如,我要加载网络或者磁盘上的一个 .class 文件)自带classLoader无法满足,需要自定义。

自定义 ClassLoader 步骤

①自定义一个类继承抽象类 ClassLoader。
②重写 findClass 方法。
③在 findClass 中,调用 defineClass 方法将字节码转换成 Class 对象,并返回。
如本地电脑磁盘写一个类:

    @Override
    protected Class<?> findClass(String name) {
        //寻找字节码
        byte[] classBytes = findCodeClassFromSomePath(name);
        //根据字节码组装Class对象,并返回
        return this.defineClass(name, classBytes, 0, classBytes.length);
    }


上述动态加载 .class 文件的思路,经常被用作热修复和插件化开发的框架中,包括 QQ 空间热修复方案、微信 Tink 等原理都是由此而来。客户端只要从服务端下载一个加密的 .class 文件,然后在本地通过事先定义好的加密方式进行解密,最后再使用自定义ClassLoader 动态加载解密后的 .class 文件,并动态调用相应的方法。

Android虚拟机不认识.class需要把.class转成.dex 。加载是在BaseDexClassLoader 中,而我们一般只使用它的两个子类:PathClassLoader 和 DexClassLoader。

一个 class 文件被加载到内存中需要经过 3 大步:装载、链接、初始化。

①装载:指查找字节流,并根据此字节流创建类的过程。装载过程成功的标志就是在方法区中成功创建了类所对应的 Class 对象。
②链接:指验证创建的类,并将其解析到 JVM 中使之能够被 JVM 执行。
③初始化:则是将标记为 static 的字段进行赋值,并且执行 static 标记的代码语句 。

对象的初始化顺序如下:

静态变量/静态代码块 -> 普通代码块 -> 构造函数

  1. 父类静态变量和静态代码块;

  2. 子类静态变量和静态代码块;

  3. 父类普通成员变量和普通代码块;

  4. 父类的构造函数;

  5. 子类普通成员变量和普通代码块;

  6. 子类的构造函数。

以上是关于JVM内存管理笔记的主要内容,如果未能解决你的问题,请参考以下文章

JVM内存管理笔记

JVM内存管理笔记

JVM学习笔记------内存管理和垃圾回收

JVM笔记-运行时内存区域划分

JVM内存管理和JVM垃圾回收机制

JVM内存管理和JVM垃圾回收机制