java对象从创建到回收过程

333 阅读8分钟

生命周期

java类完整的生命周期:加载,连接,初始化,使用,卸载

初始化方式:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
  • 通过反射方式执行以上三种行为
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)
  • 除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。
  • 克隆

卸载

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

类初始化

  • 父类静态成员和静态初始化快,按在代码中出现的顺序依次执行。
  • 子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
  • 父类的实例成员和实例初始化块,按在代码中出现的顺序依次执行。
  • 执行父类的构造方法。
  • 子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
  • 执行子类的构造方法
public class Person {
    {
        System.out.println("parent中的初始化块");
    }
    static{
        System.out.println("parent中static初始化块");
    }

    public Person(){
        System.out.println("parent构造方法");
    }


}


public class Child extends Person implements InitializingBean {

    {
        System.out.println("son中的初始化块");
    }

    static{
        System.out.println("son中的static初始化块");
    }

    public Child(){
        System.out.println("son构造方法");
    }



    public static void main(String[] args){
        Child child = new Child();
        System.out.println("################################");
        Child child1 = new Child();
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("初始化完成");
    }
}

执行结果:
parent中static初始化块
son中的static初始化块
parent中的初始化块
parent构造方法
son中的初始化块
son构造方法
################################
parent中的初始化块
parent构造方法
son中的初始化块
son构造方法

初始化面试题

public class TestBean {
    public static int k = 0;

    public static TestBean t1 = new TestBean("t1");

    public static TestBean t2 = new TestBean("t2");

    public static int i = print("i");

    public static int n = 99;

    public int j = print("j");

    static {
        print("静态代码块");
    }

    {
        print("构造块");
    }


    public TestBean(String str) {
        System.out.println((++k) + ":" + str + " i= " + i + " n=" + n);
        ++i;
        ++n;
    }

    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i= " + i + " n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String[] args) {
        TestBean init = new TestBean("init");
    }


}

模拟过程:

  • 静态变量分配内存 k=0,t1=null,t2=null,i=0,n=0,如下图

image.png

  • 赋值 k=0;t1=new TestBean("t1"),t1原本为null,现在把new出来的对象赋值给t1,于是就要构建一个Test对象,构建对象时,要把所有的非static部分初始化一份,放入堆内存

image.png

image.png 实例变量和代码执行顺序由代码位置决定

接下来是构造方法实例化 image.png

t2实例化过程和t1一致

  • 小结:Java本身也是代码从上往下走的,只不过静态部分和非静态部分在两个次元里。Java的成员有分配空间,赋默认值两个过程,且首先为全体成员申请空间,然后由上至下逐一赋值。

java子类隐藏父类变量和覆盖父类方法

父类子类结果备注
静态方法静态方法隐藏
实例方法实例方法覆盖
实例变量实例变量隐藏
静态变量静态变量隐藏
public class Base {

    int x = 1;

    static int y = 2;

    String name(){
        return "mother";

    }

    static String staticname(){
        return "static mother";

    }
}
public class Subclass extends Base{

    int x = 4;

    int y = 5;

    String name(){
        return "baby";

    }

    static String staticname(){
        return "static baby";

    }


    public static void main(String[] args){
        Subclass s = new Subclass();

        System.out.println(s.x+" "+s.y+" "+s.name()+" "+s.staticname());

        Base s1 = s;

        System.out.println(s1.x+" "+s1.y+" "+s1.name()+" "+s1.staticname());

    }



}
执行结果:
4 5 baby static baby
1 2 baby static mother

java对象在内存中的数据结构

内存布局如下图

image.png

对象头
MarkWord
  • MarkWord(标记字段) :哈希码、分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。Mark Word被设计成了一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例外:如果是数组的话,还需要有一块区域存放数组大小,因为没办法从元数据确认数组大小,所以要存储到对象头的MarkWord中。
  • MarkWord是根据对象的状态区分不同的状态位,从而区分不同的存储结构。例如下图:

image.png 其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

无锁状态:对象的hashCode + 对象分带年龄 + 状态为001

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

枷锁过程:

  • 1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。

  • 2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

  • 3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

  • 4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

  • 5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。

  • 6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。

  • 7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

对象指针

该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。 Java对象的类数据保存在方法区。

数组长度

只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。

对齐填充

因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能

jvm内存回收

对象存活判断

  • 引用计数法 计算对象引用的次数,java不使用,因为有循环引用的问题
  • 可达性分析 维护一个GC roots,从GC roots进行引用链计算,如果没有被GC roots引用到,则认为对象可以回收。常见的GC roots
  • 1、虚拟栈中引用的对象
  • 2、方法去的静态变量常量
  • 3、本地方法栈中引用的对象
  • 4、jvm内部引用,如各种异常对象
  • 5、sync持有的对象

四种引用类型

  • 1、强引用,直接new的对象,存活不能回收
  • 2、软引用,softReference,发生oom前,及时存活依然可以被回收,一般用再缓存
  • 3、弱引用,weakReference,发生gc就能被回收
  • 4、虚引用,phantomReference,随时可能被回收,日常开发很少被使用

对象分配策略

  • 1、优先分配在堆eden区
  • 2、大对象直接进入old区
  • 3、长期存活的对象进入old区
  • 4、开启逃逸分析后,如果分析认为对象的生命周期只在方法内,则进行栈生分配,减少gc压力