他是一种文件格式
简单说,就是能被JVM虚拟机识别、加载、并执行的 文件格式
而且除了java语言,还有很多其他语言也可以编译出class文件,当然还有kotlin
上图摘抄自 【深入Java虚拟机】之二:Class类文件结构 很简单 javac hello.java
记录 一个类文件 里的所有信息,记住是 一个类文件 ,而且是 所有信息
详细的可参考 【深入Java虚拟机】之二:Class类文件结构
这里简要说一下:
Class文件是一组以8位字节为基础单位的 二进制流 ,各个数据项目 严格按照顺序紧凑地 排列在Class文件中,中间 没有添加任何分隔符 ,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。
下表列出了Class文件中各个数据项的具体含义:
每个Class文件的头4个字节称 为魔数(magic) ,它的唯一作用是 判断该文件是否为一个能被虚拟机接受的Class文件 。它的值固定为0xCAFEBABE。
紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。 常量池是由一组constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。 常量池计数器constant_pool_count 的值 =constant_pool表中的成员数+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count时才会被认为是有效的。
这个是使用一个工具来查看class文件的内容
能被DVM虚拟机识别、加载、并执行的文件格式
在build-tools里面找到dx.bat
要使用dx命令,记得配置环境变量
javac hello.java
dx --dex --output hello.dex hello.class
adb push hello.dex /storage/emulated/0
adb shell
注意dex文件必须在Andriod手机执行,因为手机里才有DVM虚拟机
dalvikvm -cp /sdcard/hello.dex hello
一个class文件只是记录一个Java类的所有信息
但是一个dex记录所有类文件的信息,是 整个工程的信息
上图中的文件头部分,记录了dex文件的信息,所有字段大致的一个分部;
索引区部分,主要包含字符串、类型、方法原型、域、方法的索引;
索引区最终又被存储在数据区,其中链接数据区,主要存储动态链接库,so库的信息。
一张图理解dex
当java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右
class与dex异同之处纪传体通过记叙人物活动反映历史事件的体裁,通过记叙人物活动,反映历史事件。 如:《秦始皇本记.class》《项羽本纪.class》《高祖本纪.class》
编年体是中国传统史书的一种体裁,它是以年代为线索编排有关历史事件。编年体史书以时间为中心,按年、月、日顺序记述史事。因为它以时间为经,以史事为纬,比较容易反映出同一时期各个历史事件的联系。 例如:《春秋.dex》《左传.dex》《资治通鉴.dex》。
内存里存储class文件的不同部分,对应内存空间里的不同部分
jvm的classloader与Android里的classloader区别较大
下图为jvm的类加载器,
Android的类加载器是热修复的核心,接下来会专门说
每个方法从调用到执行完成,就是对应一个栈帧在虚拟机从入栈到出栈的过程
栈帧里包含局部变量表、栈操作数、动态链接、方法入口
A方法调用B方法,就会在调用B方法代码时,java虚拟就就会创建一个保存B方法的栈帧,然后压入栈区,当B方法执行完后,这个栈帧就会弹出栈区,这就是使我们经常说的,栈内存不需要我们管理,局部变量会在方法调用结束后,自动回收。
另外,从这里可以看出,每个方法对应一个栈帧,如果递归方法嵌套太深,当栈的深度大于jvm所允许的最大深度时候,会引起Stack Overflow,栈溢出,所以递归慎用,
为native方法服务的,也是通过栈帧实现对本地方法的调用
存储虚拟机加载的类信息、常量、静态变量、及时编译器编译后的数据 这块区域,永远占据内存,知道退出进程
所以常量、静态变量生命周期很长,只有App退出,才会被回收,所以,很多内存泄漏都是不合理使用静态变量引起的
所有通过new创建的对象的内存都在堆区分配 是虚拟机中最大的一块内存,是GC要回收的部分
新生代与老生代,简单说,刚刚创建的对象会存在新生代里,当新生代对象越来越多,内存不足时候,jvm会通过自己的一套算法,把对象从新生代移动到老生代,这样新生代就会多出一部分空间了,还能接受新的对象。当新生代和老生代的内存都满了,再来对象就会oom
为什么要分新生代+老生代
这是为了让开发者动态调整新生代和老生代的大小,例如在做即时通讯时,临时的消息对象创建的比较多,就可以把新生代这块区域调整大一些,便于新对象的分配
引用计数器:被引用+1,引用销毁-1,为0,则可以被销毁
循环引用的时候,此算法失效
被GCRoot直接或者间接引用的对象,就不可销毁
强软弱虚
弱引用的创建与使用
好处:不需要让对象进行移动,仅需要对不存活的对象进行处理,在 存活对象较多 时候,执行效率高效,但是内存碎片很多
好处:当 存活的对象比较少 时,较为高效,但是需要另外一块空间,用于管理移动
以上三种算法各有优缺点,虚拟机根据不同情况,采用不同算法,进行垃圾回收
jvm的方法调用是就栈的,前面说的栈帧 Dalvik是基于寄存器的,寄存器是比内存更快的存储介质
虽然Dalvik虚拟机已经不错了,但是google工程师研发了ATR虚拟机,更加高效
app每次运行都会把字节码转换为机器码,再去执行,退出应用,在进入app,又会再次把字节码转为机器码,效率很低的
在app安装时候,就把字节码转为本地机器码,存在本地,因此,只要app启动,直接执行机器码,而不是每次转换。
但是采用ART预编译技术,app安装时间快比较长,而且在手机里占用空间多 空间换时间
加载framework层的字节码文件
加载安装到系统里的app的class文件
加载指定目录的class文件
PathClassLoader和DexClassLoader的父类
其实一个app最少需要BootClassLoader和PathClassLoader才能正常运行
我们打印下app里的classlodaer
// 打印所有的ClassLoader var classLoader = classLoader if (classLoader != null) { Log.e("cjx", "ClassLoader---$classLoader") while (classLoader.parent != null) { classLoader = classLoader.parent Log.e("cjx", "ClassLoader---$classLoader") } } 复制代码
由此可见,一个字节码文件被任意一个classLoader加载过,就不会被其他classLoader加载了,提高了加载效率,也带来了另外特性
一个字节码文件一旦被顶层classLoader加载过,就会被整个继承体系所共有
不同继承路线的classLoader加载的类,肯定不是同一个类,防止被冒充
例如String这个类,肯定在顶层的classLoader里会把它加载,这样就避免,你自己写个classLoader来篡改string这个类的加载过程
什么样的类才能叫做是同一个类呢
同一个包名+同一个类名+同一个类加载器加载的类,才叫同一个类
如果都找不到,会走findClass方法,看一下这个方法
源码目录
间接子类:DexClassloader
DexClassloader源码查看
上面的第二个参数很重要,这个路径是系统内部的路径,就是因为这个参数,才能去把未安装到app里的dex文件,加载进来
间接子类:PathClassLoader
PathClassLoader源码查看
其实这两个间接地子类,什么也没做,只是一个能加载外部的dex文件,一个只能加载apk内部的文件,主要逻辑还是他们的父类BaseDexClassloader实现的,我们接着看BaseDexClassloader的findClass方法,看看是如何加载dex文件的
直接子类:BaseDexClassloader
BaseDexClassloader源码查看
发现直接调用的是DexpathList的findclass方法
DexPathList源码
Element是类DexPathList的一个内部类,它其中重要的一个变量就是DexFile,就是dex文件。
看看这个Element[]是怎么实现的
来到makePathElement方法
makePathElements方法核心作用就是 将指定路径 中的 所有文件转化成DexFile 同时 存储到到Element[]这个数组 中。nativeLibraryDirectories 就是lib库了。 最终在findclass方法中实现。
接着看看dexFile的loadClassBinaryName方法,我们进入DexFile这个类
DexFile源码查看
回顾一下,我们的源码解析经历了些什么