类和对象的生命周期

一个完整的类的生命周期如下:

加载 --> 准备-->解析-->初始化-->使用-->卸载

注意: 加载并不是类加载,类加载包括加载到初始化的五个阶段。而加载是第一步。

1、类加载

  1. 加载:将类的二进制流加载进VM,储存在方法区,并且生成java.lang.Class对象
  2. 验证:验证文件格式(发生在还没进入内存时)、元数据、字节码、符号引用(发生在解析阶段将符号引用转换为直接引用时)
  3. 准备:为类变量分配内存并且赋予初始量(赋值0)
  4. 解析:将符号引用转换为直接引用,符号引用包括
    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符
  5. 初始化:调用类构造器的()方法。对类变量进行赋值;按顺序执行static静态初始块

2、使用

2.1对象实例化

例:Student s = new Student();
当检测到 new 指令时,虚拟机首先会区常量池中检查是否存在 Student 这个类的符号引用,并且检查这个类是否被加载,如果没有则执行加载过程。
之后执行如下过程:

  1. 在栈内为 s 分配空间。
  2. 在堆中为Student对象分配空间。
  3. 对学生对象的成员变量进行默认初始化和显示初始化。
  4. 学生对象初始化完毕,将地址赋值给 s变量。

2.1.1 为Student对象分配空间

对象需要分配多大的内存在类加载过程中就已经确定了。为对象分配内存的方式根据堆是否规整为分两种:

  1. 指针碰撞
    当堆上的内存比较规整的时候,未分配的内存和已经分配的内存各方一边,这种情况下为对象分配内存只需要将指针从已分配的内存向空闲内存进行移动。

  2. 空闲列表
    当堆上的内存不规整的时候,虚拟机许需要维护一个列表,记录哪些内存是可用的,再分配的时候从列表寻找一块内存进行分配,并且更新列表。

堆上的内存是否完整和虚拟机是否带有压缩整理功能有关,这又涉及到GC过程。

在为新生对象分配内存时,还需要考虑线程安全问题,由于堆内存线程共享,所以多线程情况下可能出现并发问题。解决方案:

  1. 为分配内存的动作进行同步处理
  2. 为每个线程事先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB),哪个线程需要分配空间,就在哪个线程的TLAB上执行。只有当某一线程的LTAB用完了才执行同步锁定。

当对象分配内存结束后,虚拟机会为该对象设置信息:对象是哪个类的实例、对象的元数据、对象的哈希码、对象的GC年代等,并将这些信息放入对象头。这时,对象的字段值都为0,接着执行< init >指令,把对象按照指令初始化。这样一个完整的对象创建出来了。

2.1.2对象的内存布局

  1. 对象头(Mark Word)

32位系统下,占8字节(32bit);64位系统下,占16字节(64bit),开启压缩指针后 12字节。
markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间

  1. 实例数据

存放的是对象的各种数据,包括从父类继承的和自己本身的。
分配策略:相同宽度的字段放在一起,例如double和long

  1. 填充数据

无实际意义,用来填充位数以达到8的倍数倍

2.1.3对象的访问定位

java通过JVM栈上的reference找到对象,有如下两种方法访问

直接访问

reference中直接存放对象地址,而指向方法区,说明对象类型数据的指针存放在对象中。
优点:快,省区指针定位的开销

句柄访问

在堆中划分出一块地址,称为句柄池,refrerence中存放句柄池的地址,句柄池存放两个指针分别位具体的对象示例数据指针和指向方法区的对象类型数据指针。
优点:栈中存储的是稳定的句柄地址,在对象被移动时(垃圾收集时,对象经常需要移动)仅需要改变句柄中示例数据的指针

2.2、垃圾收集

方法区和堆是需要GC回收的区域,而堆是重点回收区域

2.2.1、回收区域、何时回收

java堆是GC回收的重点区域。因为堆中存放者几乎全部对象

java堆被分为两类:
1.新生代(Young Generation)新生代中又有Eden和两个Survivor区域。
i:Eden区域:对象优先分配在该区域,大对象直接进入老年代。同时TLAB也分配在该区域。
ii:Survior区域:当Eden区域内存不足时会触发Minor GC,也成为新生代GC,在Minor GC中存活的对象会被复制到Survior中,可以避免过早发生Full GC
2.老年代(Old Generation)老年代放置长生命周期的对象,通常从Survior区域拷贝过来。有个别大对象直接创建在老年代。老年代满时会触发Full GC

2.2.1.1 回收内容(可达性分析算法)

通过两种方法找到需要回收的对象:

  1. 引用计数法:给对象添加引用计数器,被引用就+1,引用失效时减一。当为0时清除。(java不采用这种方法)
  2. 可达性分析法。从GC root开始搜索,搜索走过的路径称为引用链。如果发现一个对象无法通过GC root到达,则标记这个对象。第一次被标记的对象,如果有必要执行finalize()方法,则将其放置在F-Queue队列中。之后交由虚拟机自动生成的finalize线程去执行。之后会对F-Queue队列中的对象进行二次标记。如果finalize()方法并没有拯救这个对象。则该对象被回收。注意,一个对象的finalize()方法只会被执行一次。

GC roots包括以下几种:
1.虚拟机栈(栈帧中的局部变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(Native方法)引用的对象

可达性分析的引用有如图四种

2.2.2、如何回收(垃圾清除算法)

三大垃圾清除算法

  1. 标记/清除算法(基础)
  2. 复制算法
  3. 标记/整理算法

jvm采用“分代收集算法”,不同区域采用不通多垃圾清除算法

  1. 新生代使用复制算法。
    新生代中98%的对象朝生夕死。有Survior空间。
  2. 老年代使用标记/清除算法、标记/整理算法
    老年代中对象存活率高,且没有额外空间担保。

垃圾清除算法对比

算法 内容 优点 缺点
标记/清除算法 标记出所有需要被回收的对象,回收被标记的对象所占用的空间 基础、容易实现 容易产生内存碎片
复制算法 将可用内存按容量划分为两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理。 简单高效、不产生内存碎片 浪费内存空间
标记/整理算法 先标记,标记之后不直接清理,而是将存活对象向一端移动,之后清理边界外的对象 充分利用内存空间、不产生碎片 实现复杂

3、类卸载

类的生命周期走到了最后一步,在Java虚拟机中类的生命周期和对象的生命周期很相似。虚拟机创建并初始化对象,使程序使用对象,然后在对象变得不再被引用后可选地进行垃圾收集。同样,虚拟机装载、连接并且初始化类,使程序能使用类,当程序不再引用他们的时候可选的卸载它们。
如何判断无用的类呢?需要满足以下三个条件

  1. 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。

  2. 加载该类的ClassLoader已经被回收。

  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足以上三个条件的类可以进行垃圾回收,但是并不是无用就被回收,虚拟机提供了一些参数供我们配置。

你可能感兴趣的:(类和对象的生命周期)