对象的创建和分配

对象的创建

创建方式

1、 new 关键字直接创建。 new ObjectName()。

2、通过 Class 反射对象的 newInstance() 方法。ObjectName  obj  =  ObjectName.class.newInstance()。

3、通过 Class 反射对象获取 Constructor 类,再调用其 newInstance() 方法。 ObjectName obj = ObjectName.class.getConstructor.newInstance()。

4、在类实现 Cloneable 接口的前提下,使用对象的 clone() 方法。ObjectName obj = obj.clone()。(如果内部有自定义类属性,并且想要实现深克隆(新创建的对象和原有的对象不是同一个),那么就需要让该属性类也实现 Cloneable 接口。

5、使用反序列化。(为了避免属性丢失,需要让类实现 Serializable 接口)

public static void main(String[] args){
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FilePath))
            ObjectName obj = ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

 

对象的内存布局

在对象身上,存储了关于这个对象的所有信息。

 

创建过程

1、根据创建对象的信息去内存中存放类信息的常量池中寻找是否存在要加载的类信息,如果存在直接创建对象;如果不存在就先进行该类的加载。

2、为对象分配空间。这里涉及到线程位置分配的安全和效率,比较复杂,会在下面详细来说。

3、初始化分配到对应的位置。

4、设置对象的对象头。

5、执行 init 方法(执行非静态代理块和实例属性的初始化以及执行实例构造方法)

 

对象的内存分配

分配方式

1、指针碰撞:如果 Java 堆内存是规整的,也就是对象的创建位置都是紧挨着的,这样的话直接将指针指示器向空闲方向移动要创建对象大小的距离就可以了。

2、空闲列表:如果 Java 堆内存是不规整的,那么就需要维护一个空闲列表来记录哪些位置是空闲的以及多大。在分配时就在列表上查询,找到合适的位置分配。

 

并发安全

由于在堆的线程共享的,所以对象的创建分配的空间可能同时也是另外一个线程对象创建的分配位置,这就导致了并发问题,所以为了保证对象创建的并发安全,可以有下面两种方式:

1、在分配空间时进行同步处理(采用 CAS +回旋锁的方式来保证)

2、TLAB:新的线程创建时会在堆中划分一块区域给该线程,后面该线程创建的对象都会在该位置存放,当空间不足时才使用第一种方式。(HotSpot 使用)。

 

代码优化

1、栈上分配。通过逃逸分析判断创建的对象是否逃逸出方法(也就是这个对象是否在当前方法的外部被调用),如果没有逃逸出方法,那么就有可能直接在栈上分类空间来保存。

2、同步省略。JIT 在编译时会判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他的线程。如果没有,那么 JIT 编译器在编译这段代码时就会取消这段代码的同步。

3、分离对象(标量替换)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在栈中。

标量:无法再被分解的数据。如一个类的基本数据类型属性。

聚合量:还可以被分解的数据。如一个类的自定义属性。

 

逃逸分析的不成熟性

关于逃逸分析目前还是处于不稳定的阶段,因为无法保证逃逸分析的性能消耗一定高于其节省的性能。简单来说就是可能执行了逃逸分析,结果发现都是逃逸出方法的对象,这样逃逸分析并没有提高性能,同时执行逃逸分析也消耗了一定的性能,造成得不偿失。所以,逃逸分析在 JVM 中没有实现 栈上分配的功能的,但是其还是在 JIT 中起到了优化作用。所以可以说对象都是创建在堆上的。而我们一般所说的对象创建在栈上,实际情况是因为标量替换的作用。

 

实际的对象空间分配过程

首先会判断是否可以进行标量替换,如果可以直接使用标量替换,然后结束。不可以的话再尝试在当前线程划分的区域创建,如果区域不够再尝试使用 CAS+ 自旋锁在其他位置划分,失败就再次尝试,直到成功。

 

对象的访问

Java 程序通过栈上的引用访问堆中的对象。对象的访问方式取决于 JVM 虚拟机上的实现,目前主流的访问方式是句柄和直接指针。

句柄

句柄相当于一个中间表,存储着对应实例对象的地址以及实例数据所对应类信息的地址。

优势:比较稳定,当对象被移动后(垃圾回收时移动对象是非常常见的事)时只需要改变句柄中的指针就可以了。句柄本身不需要改变。

 

直接指针

引用直接指向实例对象,在对象上保存对应的类信息所在的地址。

优势:查找快,在栈上的引用可以很快找到对应的对象。这也是 HotSpot 默认的访问方式。

posted on 2021-03-08 21:43  萌新J  阅读(258)  评论(0编辑  收藏  举报