对象在 JVM 中是怎么存储的
对象头里有什么?
文章收录在 GitHub JavaKeeper ,N线互联网开发必备技能兵器谱,有你想要的。
作为一名 Javaer,生活中的我们可能暂时没有对象,但是工作中每天都会创建大量的 Java 对象,你有试着去了解下自己的“对象”吗?
我们从四个方面重新认识下自己的“对象”
创建对象的 6 种方式
创建一个对象在 JVM 中都发生了什么
对象在 JVM 中的内存布局
对象的访问定位
一、创建对象的方式
使用 new 关键字
这是创建一个对象最通用、常规的方法,同时也是最简单的方式。通过使用此方法,我们可以调用任何要调用的构造函数(默认使用无参构造函数)
Person p = new Person();
使用 Class 类的 newInstance(),只能调用空参的构造器,权限必须为 public
//获取类对象 Class aClass = Class.forName("priv.starfish.Person"); Person p1 = (Person) aClass.newInstance();
Constructor 的 newInstance(xxx),对构造器没有要求
Class aClass = Class.forName("priv.starfish.Person"); //获取构造器 Constructor constructor = aClass.getConstructor(); Person p2 = (Person) constructor.newInstance();
clone()
深拷贝,需要实现 Cloneable 接口并实现 clone(),不调用任何的构造器
Person p3 = (Person) p.clone();
反序列化
通过序列化和反序列化技术从文件或者网络中获取对象的二进制流。
每当我们序列化和反序列化对象时,JVM 会为我们创建了一个独立的对象。在 deserialization 中,JVM 不使用任何构造函数来创建对象。(序列化的对象需要实现 Serializable)
//准备一个文件用于存储该对象的信息 File f = new File("person.obj"); FileOutputStream fos = new FileOutputStream(f); ObjectOutputStream oos = new ObjectOutputStream(fos); //序列化对象,写入到磁盘中 oos.writeObject(p); //反序列化 FileInputStream fis = new FileInputStream(f); ObjectInputStream ois = new ObjectInputStream(fis); //反序列化对象 Person p4 = (Person) ois.readObject();
第三方库 Objenesls
Java已经支持通过 Class.newInstance() 动态实例化 Java 类,但是这需要Java类有个适当的构造器。很多时候一个Java类无法通过这种途径创建,例如:构造器需要参数、构造器有副作用、构造器会抛出异常。Objenesis 可以绕过上述限制
二、创建对象的步骤这里讨论的仅仅是普通 Java 对象,不包含数组和 Class 对象(普通对象和数组对象的创建指令是不同的。创建类实例的指令:new,创建数组的指令:newarray,anewarray,multianewarray)
1. new指令虚拟机遇到一条 new 指令时,首先去检查这个指令的参数是否能在 Metaspace 的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过(即判断类元信息是否存在)。如果没有,那么须在双亲委派模式下,先执行相应的类加载过程。
2. 分配内存接下来虚拟机将为新生代对象分配内存。对象所需的内存的大小在类加载完成后便可完全确定。如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小。分配方式有“指针碰撞(Bump the Pointer)”和“空闲列表(Free List)”两种方式,具体由所采用的垃圾收集器是否带有压缩整理功能决定。
如果内存是规整的,就采用“指针碰撞”来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器采用的是 Serial、ParNew 这种基于压缩算法的,就采用这种方法。(一般使用带整理功能的垃圾收集器,都采用指针碰撞)
如果内存是不规整的,虚拟机需要维护一个列表,这个列表会记录哪些内存是可用的,在为对象分配内存的时候从列表中找到一块足够大的空间划分给该对象实例,并更新列表内容,这种分配方式就是“空闲列表”。使用CMS 这种基于Mark-Sweep 算法的收集器时,通常采用空闲列表。
我们都知道堆内存是线程共享的,那在分配内存的时候就会存在并发安全问题,JVM 是如何解决的呢?
一般有两种解决方案:
对分配内存空间的动作做同步处理,采用 CAS 机制,配合失败重试的方式保证更新操作的原子性
每个线程在 Java 堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配,当这部分区域用完之后,再分配新的"私有"内存。这种方案称为 TLAB(Thread Local Allocation Buffer),这部分 Buffer 是从堆中划分出来的,但是是本地线程独享的。