面试官问:简略描绘一下一个类被加载,创立目标,到调用目标办法被履行的流程。
背八股文的都知道:知识点是类的生命周期,以及目标的创立进程。
但是你有真正了解根据JVM的内存结构,一个类的生命周期吗?这次让你不再只听过没见过了,只要见过了才会浮光掠影。
为了不让答复太干吧,首要脑回路能够想一下思路:
- jvm 内存结构
- 类的生命周期
- 目标的创立进程
- 额定补充创立目标的几种办法。
一、JVM内存结构
JVM 内存结构也是java的一种标准,需求注意的是要和JMM(java内存模型)区别。JVM的内存结构,也叫运转时区域,如图所示,首要有以下几个部分:
- 办法区(Method Area): 也称为类区,这儿存储了每个类的结构信息,例如运转时常量池、字段和办法数据、结构函数和一般办法的字节码内容以及一些特别办法(如初始化器)的字节码内容。在JVM标准中,办法区也经常被称为非堆(Non-Heap)的一部分。
- 堆(Heap): 这是JVM内存中的首要工作区域,它被一切线程同享。堆首要用于寄存目标实例和数组。堆的巨细能够动态调整,GC(废物搜集器)首要作用的区域便是堆。堆内存进一步能够分为年青代(Young Generation)、老时代(Old or Tenured Generation)以及在某些情况下的持久代(Permanent Generation,但在HotSpot JVM中已被元空间Metaspace替代)。
- 虚拟机栈(Stack Area): 每个线程运转时都会创立自己的线程栈。这块区域寄存局部变量、操作数栈、办法调用和返回地址。每个办法调用都会创立一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接信息和办法返回时需求的信息。栈内存是线程私有的。
- 程序计数器(Program Counter Register): 这是一小块内存区域,它能够看作是当时线程所履行的字节码的行号指示器。在恣意时刻,每个线程都要履行一个办法,即使是一个本地的(Native)办法,程序计数器会正常运转,每个线程都有自己独立的程序计数器,称为“线程阻隔”(Thread-Local Storage)。
- 本地办法栈(Native Method Stack): 和Java栈类似,本地办法栈服务于本地办法调用(即非Java办法调用)。本地办法一般指的是用其他言语(如C或者C++)编写的办法。当某个线程调用一个本地办法时,它会在本地办法栈中登记该办法的状况。
一个 Java 程序,首要经过 javac 编译成 .class 文件,然后 JVM 将其类加载到办法区,履行引擎将会履行这些字节码。履行时,会翻译成汇编代码(操作系统相关的函数)。履行引擎在需求履行某个办法的时分,将对应办法区的函数指令放到CPU的高速缓存,CPU直接读取然后运转指令。程序计数器会记录履行指令的地址的行号。
进程如下:Java 文件->编译器>字节码->JVM->机器码->CPU。
JVM 全称 Java Virtual Machine,也便是咱们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完结咱们想要的操作。 咱们平常说的JVM其实更多说的是HotSpot,还有其他的TaobaoVM(淘宝有自己的VM,它实际上是Hotspot的定制版),LiquidVM(针对硬件的虚拟机,它下面是没有操作系统的,运转效率比较高),毕昇JDK(华为内部OpenJDK定制版Huawei JDK的开源版别(gitee.com/openeuler/b…)。
二、类的生命周期
字节码转换为机器码是Java虚拟机(JVM)的履行引擎(Execution Engine)的功能之一,即解释(AOT)履行或者即时编译(JIT)履行,此外还支撑类加载(Class Loading),指令优化,废物收回等。类的生命周期也贯穿其间,而Java类的生命周期指的是从类被加载到Java虚拟机(JVM)中开端,到类被卸载出虚拟机为止的进程。这个生命周期包括以下几个首要阶段:
1.类加载
类加载进程也便是JVM履行引擎经过类加载器读取字节码文件的进程。通常类加载器有如下图所示:
在Android的源码中art/runtime/class_linker.cc,我看看类怎样加载的。源码链接
这个klass
目标是什么?能够了解为加载一个类今后,生成这个类信息的目标,每个类的字节码对应一个。当示例一个目标的时分,这个类信息也会一向存在在这个目标中。
2.链接
类加载完结今后开端链接阶段,这阶段包括验证,准备,解析:
- 验证(Verification): 确保加载的类符合JVM标准,不会造成安全失败。
- 准备(Preparation): 为类变量分配内存,并设置默认初始值。
- 解析(Resolution): 将类、办法、特点的符号引证转换为直接引证。 在上面DefineClass办法里,LoadClass履行完结就有一个LinkClass办法。
3.初始化
- 履行类结构器
clinit()
办法的进程,这个办法由编译器主动搜集类中一切类变量的赋值动作和静态句子块(static{}
块)中的句子兼并发生。 - 父类会先于子类初始化,静态变量依照声明顺序初始化。
- 一个类只会被初始化一次。
履行引擎会调用klass的初始化办法,openjdk源码链接
new 关键字对应的办法,会先找到Klass,然后对类进行类的初始化。
这个办法包括以下几个进程:
- 分配类变量并进行默认初始化;
- 履行静态句子块中的句子,包括赋值和履行办法调用;
- 解析和链接类中的一切相关的符号引证;
- 把符号引证转换为直接引证;
- 对类变量进行显现初始化;
- 最终履行其他的类的
<clinit>()
办法
4.运用
创立类的实例目标、调用类的办法、或拜访类的变量等活动。这个日常开发写的代码都是怎样运用类。就不赘述了。
5.卸载
当满意一个类的一切实例目标都现已被收回,并且class目标klass没有被引证的地方条件下,类才会被卸载,比如该类的ClassLoader被收回,并且此类的一切实例都现已被收回等情况下,JVM会认为该类现已不再需求了,类会被卸载。不过这个条件相对比较难以达到。
三、目标中的类信息klass
当咱们实例化一个类的目标的时分,会给目标增加一个klass目标的引证(Android SDK 21以上),klass带着类的全部信息。如图所示:
如图是一个没有成员变量和办法的类,展开发现是一些threadId,classLoader,classsize,objectsize等状况信息。此时发现classSize的巨细是224(可能不同Android版别会有不同的巨细)
当我加一个办法的时分,增加到232,说明一个办法引证巨细为8个字节。 此外咱们还发现了一个shadow$_monitor_的字段。咱们检查Object的源码
看注释的说明现已很明白了,klass
是Class类信息,monitor_
是关于Monitor和hashcode信息,Object的java源码中的只要在hashCode有运用shadow$_monitor_
,如图:
identityHashCodeNative是一个native办法,咱们检查Android源码,结合java代码和c代码,hashcode应该有5种条件生成,所以获取目标的hashCode()
的时分,明白他不代表目标的地址了。
假如想知道Monitor是什么?重视我,我马上组织 @~@ 。
四、从字节码看一个目标的创立
下面办法是简略的创一个目标:
public static void fun2() {
Object object = new Object();
}
转换成java字节码指令集:
0: new #3 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_0
8: return
new 表明办法调用,dup表明复制到作用句柄,invokespecial表明调用结构办法 履行引擎会调用klass的初始化办法,源码链接
new 关键字对应的办法,会先找到Klass,然后对类进行类的初始化,然后请求堆内存实例目标。
Android中字节码是dex的字节码,相同转换成指令集看看:
0000: new-instance v0, java.lang.Object // type@0003
0002: invoke-direct {v0}, java.lang.Object.<init>:()V // method@0005
0005: return-void
new-instance创立一个新的实例,invoke-direct直接调用,咱们找到art履行引擎运转时的对应办法源码
所以简略来看,一个目标的创立,首要需求判别类是否被加载,然后请求内存,不行的话会触发GC,假如内存请求成功,就调用init()
,即结构办法处理初始化类的成员信息。这需求区别类的初始化,类的初始化只要一次,<clinit>()
并不是程序员在Java代码中直接编写的办法,而是Javac编译器的主动生成物。
五、目标被实例化的办法
目标被实例化的办法有哪些:
- 运用new关键字
- 运用反射
Class<?> clazz = Class.forName("com.demo.MyDemo"); MyDemo myObject = (MyDemo) clazz.newInstance(); //结构函数反射 Class<?> clazz = Class.forName("com.demo.MyDemo"); Constructor c = clazz.getConstructor(null); MyDemo myObject = (MyDemo)c.newInstance();
- 运用Java 8+的结构函数引证
class MyDemo{
MyDemo(String s){
}
}
Function<String, MyDemo> constructorReference = MyDemo::new;
MyDemo myObject = constructorReference.apply("some parameter");
- 克隆clone() 类实现接口
public class MyDemo implements Cloneable{
@Override
protected MyDemo clone() throws CloneNotSupportedException {
// 调用super.clone()来履行浅拷贝
MyDemo cloned = (MyDemo) super.clone();
return cloned;
}
}
- 序列化 先经过ObjectOutputStream进行序列化,然后经过ObjectInputStream进行反序列化,能够实现创立一个目标。
六、总结
前文从源码层面加深了解类加载和目标的创立进程,再来丰富咱们的对开头面试官的答复。
答: 首要jvm内存结构有办法区,堆,虚拟机栈(程序计数器,本地办法栈),类经过类加载器加载,链接这两个阶段会生成一个klass的类目标存储在类表中,然后类的初始化阶段会将将常量和静态变量初始化存储,办法代码存在办法区,当运用关键字new
实例化一个目标时分,先判别类是否被初始化,然后请求内存,可能会触发GC,内存请求成功将目标存在堆区。然后调用目标的结构办法初始化目标的成员变量,当调用一个目标的办法时,办法被履行的时分,经过klass的类信息中的method办法表,找到对应的办法地址,然后履行引擎将办法区的指令加载到虚拟机栈开端履行。办法区的指令会封装成一个栈帧,每个线程都有一个虚拟栈维护这些办法栈,最终放到CPU高速缓存被CPU履行,程序计数器也会一向记录办法指令履行到哪一个行。最终直到办法指令被履行完,该办法出栈,持续下一个办法。