对象实例化方式以及直接内存
学习途径: 尚硅谷【宋红康】
对象实例化方式以及直接内存
1,对象实例化
涉及 虚拟机栈、堆以及方法区
对象实例化 方式 | 说明 | |
---|---|---|
1 | new | 1,Xxx的静态方法 2,XxxBuilder/XxxFactory的静态方法 |
2 | Class的newInstance() | 反射的方式 ,空参构造器,public |
3 | Constructor的newlnstance(Xxx) | 反射的方式,构造器随意,权限随意 |
4 | 使用clone() | 不调用任何构造器,当前类需实现Cloneable接口,实现clone()方法 |
5 | 使用反序列化 | 从文件、网络中获取一个对象的二进制流 |
6 | 第三方库Objenesis |
2,创建对象的步骤
细节:
创建对象的过程中:
dup指令会在操作数栈中创建两个引用,一个是复制 一个是用于调方法
invokespecial 指令调用默认的空参构造器
步骤:
- 判断对象对应的类是否加载、链接、初始化
- 为对象分配内存
- 如果内存规整–指针碰撞法
- 如果内存不规整-- JVM需维护一个列表 – 空闲列表 来分配内存
- 处理并发安全问题
- 采用CAS配上失败重试保证更新的原子性
- 每个线程预先分配一块TLAB
- 初始化分配到的空间
- 所有属性设置默认值 ,保证对象实例字段在不赋值时可以直接使用
- 设置对象的对象头
- 执行init方法进行初始化
每一小步说明
1,加载、链接、初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息.是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象。
2,为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
3,指针碰撞
内存”整整齐齐“
- 如果内存是规整的,那么虚拟机将采用的是指针碰撞法( Bump The Pointer )来为对象分配内存。
- 意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集 器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact(整理)过程的收集器时,使用指针碰撞。
4,空闲列表分配
- 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。
- 大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表( Free List)”。大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表( Free List)”。
具体选择那种分配方式由java堆是否规整决定,而java堆是否规整又由所采取的垃圾收集器是否带有压缩整理的功能而决定
5,测试对象实例化的过程
①加载类元信息-
②为对象分配内存-
③处理并发问题-
④属性的默认初始化(零值初始化)-
⑤设置对象头的信息-
⑥属性的显式初始化、代码块中初始化、构造器中初始化
6,给对象的属性赋值的操作
①属性的默认初始化-
②显式初始化-
⑧代码块中初始化-
④构造器中初始化
7,设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、 锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
8,执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
小问题
那么我们如何才能说一个对象已经被构建完成(创建成功)了呢?
new 只是搭模子
Object() 构造器就真正把细节填充了,有鼻有眼的 属性的初始化等
但我们应该是要这样认为:带下述六个步骤都做完了,,才能说我们把对象造完了。。详见上述:5,测试对象实例化的过程
3,对象内存布局
- 对象头(Header)
- 包含两部分
- 运行时元数据( Mark Word )
- 哈希值( HashCode )
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针一指向类元数据InstanceKlass ,确定该对象所属的类型
- 运行时元数据( Mark Word )
- 说明:如果是数组,还需记录数组的长度
- 包含两部分
- 实例数据(Instance Data)
- 说明
- 它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
- 规则
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
- 说明
- 对齐填充(Padding)
- 不是必须的,也没特别含义,仅仅起到占位符的作用
Demo
- 不是必须的,也没特别含义,仅仅起到占位符的作用
public class Customer{
int id=1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{}
------
public class CustomerTest{
public static void main(String[] args){
Customer cust = new Customer();
}
}
4,对象访问定位
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
JVM是如何通过栈帧中的对象引|用访问到其内部的对象实例的呢?
定位 ,通过栈上reference访问
对象访问定位的两种方式
- 句柄访问
- 直接指针[hotspot采取该方式]
优点:
直接指针:
调用十分快速,省了一次寻址的步骤
句柄访问:
reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。
标记整理算法时的引用
s0/s1[from/to 区]的移动的引用等
5,直接内存
元空间就是一种直接内存,,而且,如果是元空间OOM[java.lang.OutofMemory: Direct buffer memory]报错的话,这种error事不能被jclasslib等工具检测到的
NIO中也使用到了直接内存,通过直接内存代替内存之间的copy
因此,我们的OOM检测方向
- 1,堆
- 2,直接内存
通过上述所述:
我们可以将java内存简单概述为:
java process memory = java heap + native memory