一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。
加载
这个阶段是类加载的第一阶段,有两种时机会触发类加载:
预加载:虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载(提前将程序运行常用的类提前加载好)。
运行时加载: 虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。其实加载阶段做了有三件事情:
-
获取.class文件的二进制流
-
将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
-
在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象是放在方法区中的。
简单说,该阶段就是把.class文件通过类加载器加载到内存中生成java.lang.Class对象(类加载器角色充当搬运工);
链接
链接这个阶段可分为验证、准备、解析三个阶段
-
验证
校验.class文件是否符合虚拟机的要求,验证包含:文件格式验证、元数据验证、字节码验证、符号引用验证。
-
准备
准备阶段是正式为类变量(被static修饰的变量)分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。有两个地方需要注意:
-
这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
-
这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值(也可以是指定值);另外一次在初始化阶段,赋予程序员定义的值。因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。
-
解析
将常量池内的符号引用替换为直接引用的过程。
符号引用和直接引用都是用于表示类、接口、字段和方法的引用,不同之处在于符号引用是一种编译时的引用,以符号的形式描述引用的目标,而直接引用是指向具体内存地址的引用,可以直接使用。
初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,为类的静态变量赋予正确的初始值,,或者可以从另个一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
类的初始化是指类加载过程中的初始化阶段对类变量按照程序员的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。
类加载器有哪些?
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器 (user class loader),用户通过继承 java.lang.ClassLoader类的方式自行实现的类加载器。
什么是双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
为什么要用双亲委派机制?
保证应用程序的稳定有序。可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
例如类java.lang.Object,它存放在rt.jar之中,通过双亲委派机制,保证最终都是委派给处于模型最顶端的启动类加载器进行加载,保证Object的一致。反之,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类。
如何打破双亲委派
两个典型的方法:
- 自定义类加载器,重写loadClass方法
- 使用线程上下文类加载器
类加载机制
- 全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。
- 父类委托(双亲委派)
先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。
- 缓存机制
缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。
如何分配内存
内存分配有两种方式,指针碰撞(Bump The Pointer)、空闲列表(Free List)。
- 指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
- 空闲列表:如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
JVM 里 new 对象时,堆会发生抢占吗?JVM是如何保证线程安全的?
会发生抢占,堆抢占和解决方案
- 采用CAS分配重试的方式来保证更新操作的原子性
- 每个线程在Java堆中预先分配一小块内存,也就是本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),要分配内存的线程,先在本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
创建对象方式
- 使用
new
关键字创建对象 - 使用Class类的
newInstance
方法,Constructor
类的newInstance
方法(反射机制) - 使用
Clone
方法创建对象 - 使用(反)序列化机制创建对象
<clinit>()
方法和<init>()
方法区别。
-
<clinit>()
方法发生在类初始化阶段,会执行类中的静态类变量的初始化和静态代码块中的逻辑,执行顺序就是语句在源文件中出现的顺序。 -
<init>()
方法发生在类实例化阶段,是默认的构造函数,会执行普通成员变量的初始化和普通代码块的逻辑,执行顺序就是语句在源文件中出现的顺序。
一个实例变量在对象初始化的过程中最多可以被赋值几次?
那我们就试试举例出最多的情况,其实也就是每个要经过的地方都对实例变量进行一次赋值:
- 1、
对象被创建时候
,分配内存会把实例变量赋予默认值,这是肯定会发生的。 - 2、
实例变量本身初始化的时候
,就给他赋值一次,也就是int value1=100。 - 3、
初始化代码块的时候
,也赋值一次。 - 4、
构造函数中
,在进行赋值一次。
Java类初始化过程
静态代码块初始化
静态成员变量初始化
代码块初始化
实例变量初始化
构造函数初始化
new一个对象的详细过程
1. 类加载检查
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2. 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
3. 初始化零值
内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值(但不包括对象头),如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
4. 设置对象头
接下来,Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5. 执行<init>()方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始—— 构造函数,即Class文件中的 <init>()方法 还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。