Android性能调优之需要掌握的JVM知识(1)

  1. Java的GC机制

注意的是,Android中的Dalvik和ART并不属于JVM。

2.JVM执行流程

==========================================================================

当我们执行一个Java程序时,它的执行流程如图所示:

在这里插入图片描述

图中可以看出,JVM执行流程分为两个部分,分别是编译时环境和运行时环境,当一个Java文件经过Java编译器编译后会生成一个 .class文件,这个 .class会交由JVM来处理。

Jvm和Java语言没有什么必然的联系,它只跟特定的二进制文件 Class文件有关。所以任何语言只要能编译出 .class文件,就能被JVM识别且执行。

3.JVM结构体系

==========================================================================

这里讲的结构,并不是JVM物理上的结构,而且是其实现逻辑,是抽象层面上的结构。

我说我是个车轮,是因为我走路的时候把自己当成车轱辘来滚,而不是我真的是个轮子。

按照Java虚拟机规范,抽象的JVM如图所示:

在这里插入图片描述

可以看出Java虚拟机包括 运行时数据区域执行引擎本地库接口本地方法库。类加载子系统并不时JVM的内部结构。

在这些区域里,像 方法区、Java堆、本地库接口,垃圾回收器、即时编译器都是线程共享的。

3.1 Class文件格式


Java文件被编译后生成了 Class文件,这种二进制格式的文件不依赖与特定的硬件和操作系统。

每一个class文件都对应着唯一的类或接口的定义信息,但是类或者接口并不一定定义在文件中,比如类可以通过类加载器直接生成。之前说过,任何语言只要能编译成Class文件,就可以被Java虚拟机识别并且执行,Class文件的重要性可见一斑。

下面我们来学习Class文件格式:

ClassFile {

u4 magic; // 魔数,表明当前文件是.class文件,固定0xCAFEBABE

u2 minor_version; // Class文件的副版本号

u2 major_version; //Class文件主版本号

u2 constant_pool_count; // 常量池计数

cp_info constant_pool[constant_pool_count-1]; // 常量池内容

u2 access_flags; // 类/接口访问标识

u2 this_class; // 当前类索引

u2 super_class; // 父类索引

u2 interfaces_count; // 接口计数器

u2 interfaces[interfaces_count]; // 接口表

u2 fields_count; // 字段计数器

field_info fields[fields_count]; // 字段表

u2 methods_count; // 方法计数器

method_info methods[methods_count]; // 方法表

u2 attributes_count; // 属性计数器

attribute_info attributes[attributes_count]; // 属性表

}

其中:uX 代表 X字节的无符号类型。比如u4就是4字节的无符号类型

3.2 类的生命周期


一个Java文件被加载到JVM内存中到从内存中被卸载的过程被称为类的生命周期。

类的生命周期包括的阶段分别是:加载、链接、初始化、使用和卸载。其中链接包括验证、准备和解析。因此类的生命周期被分为了7个阶段,顺序如下所示。

  1. 加载

查找并加载Class文件

  1. 链接

包括验证、准备和解析

(1) 验证:确保被导入类型的正确性

(2)准备:为类的静态字段分配字段,并使用

(3)解析:虚拟机将常量池内的符号引用替换为直接引用

  1. 初始化

将类变量初始化为正确初始值

  1. 使用

  2. 卸载

其中前三个阶段称为类的加载阶段。

在《深入理解JVM》中,上述第一点,加载阶段(非类加载阶段)主要做了3件事情:

  • 根据特定名称查找类或接口类型的二进制字节流

  • 将这个二进制字节流所代表的静态存储结构 转化成 方法区的运行时数据结构

  • 在内存中生成了一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

其中第一件事情就是由Java虚拟机外部的类加载子系统来完成的。

3.3 类加载子系统


类加载子系统通过多种类加载器来查找和加载Class文件到JVM中,JVM有两种类加载器,分别是系统加载器自定义加载器。之前对类加载机制做过理解:Java ClassLoader总结

这里就复制其中比较关键的东西吧:

  • Bootstrp ClassLoader(引导类加载器)

Bootstrp加载器是由C++语言编写的,它是在JVM启动后初始化的,主要负责加载%JAVA_HOME%/jre/lib-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。

因为其是由C++写的,所以不能被Java代码访问到,但是可以查询某个类是否被引导类加载器加载过。

  • Extensions ClassLoader(拓展类加载器)

Bootstrp Loader加载ExtClassLoader,并且设置其父加载器(是父加载器而不是父类哦)为自己,这个ExtClass Loader是java编写的,它主要加载%JAVA_HOME%/jre/lib/ext这个路径下所有的classes目录以及java.ext.dirs系统变量指定路径中的类库。

  • Application ClassLoader(应用程序类加载器)

Bootstrp Loader加载完ExtClassLoader之后会加载AppClassLoader,并指定其父加载器为ExtClassLoader,它的作用是加载当前应用程序Classpath目录,以及系统属性java.class.path所指定位置的类或者jre文档,它也是Java的默认加载器。

关于ClassLoader的学问我们后边再写一篇,加深理解

4 运行时数据区域

==========================================================================

Java的内存不仅仅是堆内存和栈内存。

1.程序计数器

为了保证程能够连续的执行下去,处理器必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用。

程序计数器也叫PC寄存器,是一块较小的内存空间。在虚拟机概念模型中,字节码解释器的工作时就时通过改变程序计数器来选取下一个条需要执行的字节码指令的。

JVM的多线程是通过轮流切换并分配处理器执行时间的方式来实现的。在一个确定的时刻只有一个处理器执行一条线程中的指令。为了在线程切换后能恢复到正确的执行位置,每个线程都会有一个独立的程序计数器,因此程序计数器是私有的

如果线程执行的方法不是native方法,则程序计数器保存在正在执行的字节码指令地址,否则程序计数器的值为空。程序计数器是JVM规范中唯一没有任何OOM情况的数据区域

2.Java虚拟机栈

每一条Java虚拟机线程都有一个线程私有的Java虚拟机栈。它的生命周期与线程相同。

Java虚拟机栈存储线程中Java方法调用的状态,比如局部变量、参数、返回值以及运算的中间结果等。

一个Java虚拟机栈包含了多个栈帧,一个栈帧用来存储局部变量、操作数栈、动态链接、方法出口等信息。当线程调用一个Java方法时,虚拟机就压入一个新的栈帧到该线程的Java虚拟机栈中,在该方法执行完成后,这个栈帧就从Java虚拟机栈中弹出。

Java虚拟机规范中定义了两种异常情况:

  1. 如果线程请求分配的栈容量超过Java虚拟机所允许的最大容量,Java虚拟机就会抛出 StackOverflowError,即爆栈

  2. 如果JVM栈可以动态扩展,但是扩展时无法申请到足够的内存,或者在创建新的线程时,没有足够的内存去创建对应的JVM,就会抛出 OutOfMemoryError异常,即OOM

因为大部分JVM都是可以扩展的,所以相比于爆栈,我们见到OOM的情况更多。

3.本地方法栈

JVM可能要用到C Stacks来支持Native语言,这个C Stacks就是本地方法栈。

它与JVM栈类似,只不过本地方法栈是用来支持Native方法的,如果Java虚拟机不支持Native方法,并且也不依赖于C Stacks,可以无需支持本地方发展。Jvm可以自由的实现本地方法栈,比如 HotSpot VM将本地方发展和Java虚拟机栈合二为一。

本地方法栈也会抛出 StackOverflowError和OutOfMemoryError的异常。

4.Java堆

Java堆是被所有线程共享的运行时内存区域。Java堆用来存放对象实例。

几乎所有的对象实例都在这里分配内存。Java堆存储的对象被垃圾收集器管理,这些受管理的对象无法显式的销毁。

从内存回收的角度来分,Java堆可以粗略的分为新生代和老年代

从内存分配的角度来分,Java堆中可能划分出多个线程私有的分配缓冲区。

Java虚拟机规范中定义了一种异常情况:如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展式时,也会抛出OutOfMemoryError异常。

5.方法区

方法区是被线程共享时的内存区域,用来存储已经被Java虚拟机加载的类的结构信息。包括运行时常量池、字段和方法信息、静态变量等数据。方法区是Java堆的逻辑组成部分,它一样在物理上不用连续,并且可以选择在方法区中不实现垃圾收集。

方法区并不等同于永久代,只是因为HotSpot VM使用永久代来实现方法区,对于其他的JVM,比如J9和JRockit等,并不存在永久代等概念。

如果方法区不满足内存分配需求时,JVM也会抛出OOM异常。

6.运行时常量池

并不是运行时数据区域的一份子,而是方法区的一部分。

在前面的Class文件结构中我们看到了,Class文件不仅包含类的版本号、接口、字段等,还包含常量池。

它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。

运行时常量池可以理解为是类或接口的常量池的运行时表现形式。

5.对象的创建

========================================================================

对象的创建是我们经常要做的事情,通常是通过new指令来完成一个对象的创建,当虚拟机接收到一个new指令时,它会做如下的操作:

  1. 判断对象对应的类是否加载、链接和初始化

  2. 为对象分配内存

类加载完成后,接着会在Java中划分一块内存分配给对象。内存分配是根据Java堆是否规整。有两种方式:

(1)指针碰撞,如果Java堆的内存是规整的,即所有用过的内存放在一边,而空闲的内存放在一边。分配内存时将位于中间的指针指示器向空闲的内存一动一段与对象大小想等的距离,这样便完成分配内存的工作

(2)空闲列表,如果Java堆的内存是不规整的,则需要由虚拟机维护一个列表来记录哪些内存是可以用的。

这样在分配的时候可以从列表中查询足够大的内存分配给对象。

Java堆的内存是否规整根据所采用的垃圾收集器是否带有压缩整理功能有关。

  1. 处理并发安全问题

创建对象是一个非常频繁的操作,所以需要解决并发的问题,有两种方式:

(1)对分配内存空间的动作进行同步处理,比如在虚拟机采用CAS算法并配上失败重试的方式保证更新操作的原子性

(2)每个线程在Java堆中预先分配一小块内存,这块内存成为本地线程分配缓冲,线程需要分配内存时,就在对应线程的TLAB上分配内存,当线程的TLAB用完并且被分配到了新的TLAB时,这时候才需要同步锁定。通过 -XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。

  1. 初始化分配到的内存空间

将分配到的内存,除了对象头外都初始化为零值

  1. 设置对象的对象头

将对象的所属类、对象的HashCode和对象的GC分代年龄等数据存储在对象的对象头中。

对象头的知识后面会梳理

  1. 执行init方法进行初始化

执行init(),初始化对象的成员变量、调用类的构造方法,这样一个对象就被创建出来的。

PS:单从上面就可以知道,创建一个对象其实也会造成一定的COST,所以看了这些东西,你还会轻易的去new对象吗?你还会再onDraw() 里面去new对象吗?所以也请把对象的创建看成是一个轻微级的操作来看!

4.1 对象的堆内存布局


我们已经知道对象被创建了,堆又给对象分配了空间,那么对象在堆内存是如何进行布局的呢,它长的是什么样的呢?就是上一节所讲的,对象头是啥?

以HotSpot VM为例,对象在堆内存的布局分为三个区域:

  1. 对象头

对象头包括两部分信息,分别是 Mark Word元数据指针

(1)Mark Word:用于存储对象运行时数据,比如 Hash Code、锁状态标志、CG分代年龄,线程持有的锁

(2)元数据指针:用于指向方法区中的目标类的元数据,通过元数据可以确定对象的具体类型。后面会细讲

  1. 实例数据

用于存储对象中的各种类型的字段信息(包括父类继承来的)

  1. 对齐填充

对齐填充不一定存在,起到了占位符的作用,没有特别的含义。

Mark Word在HotSpot中的实现类为markOop.hpp,markOop被设计成一个非固定的数据结构,这是为了在极小的空间中存储尽量多的数据。

32位虚拟机的markOop格式如下:

//32位的markOop

hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)

JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)

size:32 ------------------------------------------>| (CMS free block)

PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

数据的解释为:

  1. hash

对象的哈希码

  1. age

对象的分代年龄

  1. biased_lock

偏向锁标识位

  1. lock

锁状态标志位

  1. JavaThread*

持有偏向锁的线程Id

  1. epoch

偏向时间戳

Mark Word经常被用于研究锁的状态,我之前在做关于对象锁的理解时,也有写过这种东西,只是角度不同,是从锁追溯到Mark Word,而这里是从Mark Word追溯到锁,这里对锁就不多做细讲了,这里有两篇:Java中的几种锁机制

Synchronized的锁优化

这里小结一下:

一个进程的启动就能产生一个JVM,一个JVM上有多个线程。在程序运行的时候,JVM上堆会分配了很多个对象的内存空间。

当一个线程想要使用堆上的某一个对象时,会先去访问这个对象的Mark Word,看看这个锁,这个类锁、这个对象锁是不是能用(就是是不是被别的线程使用了),如果可以用,那就用,如果用不了,就根据锁的状态进行 自旋/等待 or …

4.2 oop-klass模型


oop-klass是用来描述Java对象实例的一种模型,它分为两个部分:

  1. OOP(Ordinary Object Pointer)

指的是普通对象指针,用来表示对象的实例信息。

  1. klass

klass用来描述元数据

在HotSpot中就采用了 oop-klass模型,oop实际上是一个家族,JVM内部会定义很多oop类型,如下所示:

typedef class markOopDesc* markOop; //oop标记对象

typedef class oopDesc* oop; //oop家族的顶级父类

typedef class instanceOopDesc* instanceOop; //表示Java类实例

typedef class arrayOopDesc* arrayOopDesc*; //数组对象

typedef class objArrayOopDesc* objectArrayOopDes //引用类型数组对象

typedef class typeArrayOopDesc* typeArrayOop; //基本类型数组对象

其中oopDesc是所有oop的顶级父类,arrayOopDesc是objArrayOopDesc和typeArrayOopDesc的父类。

instanceOopDesc*和arrayOopDesc都可以用来描述对象头。

还定义了 klass家族:

class Klass; //klass家族的父类

class InstanceKlass; //描述Java类的数据结构

class InstanceMirrorKlass; //描述java.lang.Class实例

class InstanceClassLoaderKlass; //特殊的InstanceKalss,不添加任何字段

class InstanceRefKlass; //描述Java.lang.ref.Reference的子类

class ArrayKlass; //描述Java数组信息

class ObjectArrayKlass; //描述Java中引用类型数组的数据结构

class TypeArrayKlass; //描述Java中基本类型数组的数据结构

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

[外链图片转存中…(img-rFnFToKo-1715421994199)]

[外链图片转存中…(img-RuPW7Ial-1715421994200)]

[外链图片转存中…(img-IG9MkQ47-1715421994201)]

[外链图片转存中…(img-IpLYraEG-1715421994202)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值