赞
踩
Java是一门面向对象的编程语言。基于面向对象编程思想,Java编程是围绕类和对象来设计程序。
对象是Java中很重要且常见的内容。在Java语言层面,创建对象通常只是一个new关键字(复制、反序列化例外,还有反射)。
创建Java对象,有四种方式。创建对象,也叫类的实例化。
这个是最常见的。也是本文要讨论的。
User user = new User();
能够分析类能力的程序被称为反射(reflective)。反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。特别是在设计或运行时添加新类时,能够快速地应用开发工具动态添加新添加类的能力。
反射机制的功能极其强大,有以下:
反射是一种功能强大且复杂的能力,这里不展开了,主要介绍运行时创建对象的功能。
java.lang.Class
或者 java.lang.relect.Constructor
java.lang.Class
//jdk9之前
User user1 = (User) Class.forName("com.program.highway.User").newInstance();
System.out.println("user1:" + user1);
//jdk9以及之后
User user2 = (User) Class.forName("com.program.highway.User").getDeclaredConstructor().newInstance();
System.out.println("user2:" + user2);
java.lang.relect.Constructor
Class<User> clazz = User.class;
Constructor<User> constructor = clazz.getConstructor();
User user1 = constructor.newInstance();
//这个其实和Class.forName类似,classforName返回值也是Class
User user2 = clazz.newInstance();
调用对象的clone(),盖翠霞必须实现Cloneable接口,并重写clone()方法。
User user = new User();
User user1 = (User) user.clone();
通过网络传输或者读本地文件等方法,反序列化得到对象。该类必须实现Serializable接口。
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
User user = (User) in.readObject();
不同于语言层面,new关键字在JVM中的处理,又是另一番情景。
当Java虚拟机JVM遇到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,必须先执行类加载。
类加载检查通过后,JVM会为新生对象分配内存(下一节会详细介绍)。对象需要的内存空间大小在类加载完成后就可以确定,对象的内存分配相当于把一块确定大小的内存从Java堆中划分出来。
内存分配完成后,JVM必须将分配到的内存空间(不包括对象头)都初始化为零值。这也保证了对象的实例属性在Java代码中可以不赋初始值就直接使用,以便程序能访问到这些字段的数据类型所对应的零值。
JVM还要对对象进行必要的设置,例如这个对象属于哪个类实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息在对象头(Object Header)中。
上面的步骤执行完,JVM创建对象的工作就完成了,也就是字节码new指令已经完成,一个新的对象已经产生了。接下来要执行构造函数,也就是Class文件中 <init>()
方法,所有的属性都是默认值,对象需要的其他资源和状态信息还没有按预定的意图(编写的代码)构造好。通常Java编译器会在遇到new关键字的地方同时生成两条字节码指令 new
和 invokespecial
。
放一段字节码,来验证一下。
/**
* User类
*
* @author 编程还未
* @since 2022/5/22 1:03
**/
public class User {
private Integer id;
private String name;
private Integer age;
}
/**
* @author 编程还未
* @since 2022/5/22 1:04
**/
public class TestUser {
public static void main(String[] args) {
User user = new User();
}
}
TestUser.class
的字节码,main方法里面 NEW
指令后面跟着 INVOKESPECIAL
NEW:创建一个对象。
DUP:复制操作数栈栈顶的值,并插入到栈顶。
INVOKESPECIAL:调用实例方法,专门用来调用父类方法、私有方法和实例初始化方法。
// class version 57.0 (57)
// access flags 0x21
public class program/highway/TestUser {
// compiled from: TestUser.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lprogram/highway/TestUser; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 9 L0
NEW program/highway/User
DUP
INVOKESPECIAL program/highway/User.<init> ()V
ASTORE 1
L1
LINENUMBER 10 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE user Lprogram/highway/User; L1 L2 1
MAXSTACK = 2
MAXLOCALS = 2
}
new
指令之后会执行INVOKESPECIAL
指令调用<init>()
方法,按编写的程序来对对象进行初始化。这样一个真正可用,符合编程预期的对象才算完全被构造出来。
对象的内存分配就是把一块确定大小的内存从JVM堆中划分出来,主要有两种方式。
Java堆中内存是规整的情况下,被使用过的内存被放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存的时候就往指向空闲空间方向挪动一段与对象大小相等的距离。这种方式被称为“指针碰撞”。
Java堆中内存并不规整的情况下,已被使用的内存和空闲的内存混在一起。JVM会维护一个列表,记录哪块内存是可用的,在分配的时候从列表中找到 一块大小合适的空间划分给对象实例,并记录在列表上。这种方式被称为“空闲列表”。
这两种内存分配方式,各有优劣。指针碰撞简单高效,但是内存必须规整;空闲列表相对复杂,但是没有内存是否规整的限制。
Java堆是否规整,是由GC决定的。带空间压缩整理的GC,可以使用指针碰撞。例如:Serial、ParNew带有压缩整理使用的是指针碰撞;CMS基于清除算法的,只能使用空闲列表。
对象创建在JVM中非常频繁,并发情况下并不是线程安全的。有两种方案解决线程安全的问题
1. CAS再加上失败重试的方法保证操作的原子性;
2. 把内存分配动作按照线程划分在不同的空间中进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。线程分配内存,在TLAB中分配,TLAB用完了空间,分配新的TLAB时需要同步锁定。
Java语言是面向对象编程语言的一个经典范例。Java语言中接口和实现类很常见,接口定义一些方法,实现类具体实现方法,不同实现类可以有不同实现。JVM和具体的虚拟机也类似这种关系,最经典的虚拟机莫过于HotSpot。
JVM规范定义了关于类和对象的内容,但是并未具体规定如何实现。探索一下HotSpot关于对象的内容。
HotSpot VM使用oop描述对象,使用klass描述类,这种方式被称为对象类二分模型。
oop:ordinary object pointer,普通对象指针,用来描述对象实例信息。
Klass:Java类的C++对等体,用来描述Java类。
二分模型的设计,是为了在HotSpot中不让每个对象中都含有一个vtable(虚函数表),所以就把对象模型拆成Klass和oop。其中,oop中不含有任何虚函数,也就没有虚函数表;Klass中含有虚函数表,可以进行方法的分发。
Java类通过Klass表示。Klass就是Java类在HotSpot中的C++对等体,主要用于描述Java对象的具体类型。HotSpot在加载Class文件时会在元数据区(JVM的方法区,HotSpot中用元数据区实现方法区)创建Klass,表示类的元数据,通过Klass可以获取类的常量池、字段和方法等信息。
上图所示,是Klass的继承体系,来自HotSpot源码1.8版本,C++代码。
Metadata是元数据类的基类;
Method表示Java类中的方法;
MethodData保存已收集的有关方法的信息;
ConstantPool是一个数组,包含了类文件中描述的类常量;
Klass是Java类在HotSpot中的C++对等体。
Klass提供:
这两个函数合并为一个C++类。
InstanceKlass 是 Java 类的 VM 级别表示。它包含执行运行时类所需的所有信息。
InstanceRefKlass是Java类的专用InstanceKlass,描述java.lang.ref.*类。
InstanceMirrorKlass 是 java.lang.Class 实例的专用 InstanceKlass。
InstanceClassLoaderKlass 是 InstanceKlass 的一个特化。它不添加任何字段。添加它是为了遍历该类加载器指向的类加载器键的依赖项。
ArrayClass是所有数组类的抽象基类。
ObjArrayKlass 是 objArrays 的类。
TypeArrayKlass 是 typeArray 的 klass 它包含元素的类型和大小。
Java对象用oop来表示,在Java创建对象的时候创建。也就是,Java应用程序运行过程中每创建一个Java对象,在HotSpot VM内都会创建一个oop实例来表示Java对象。
上图所示,是oop的继承体系,来自HotSpot源码1.8版本,C++代码。
oopDesc是对象类的顶级基类,别名是oop。oopDesc是所有类名格式为xxxOopDesc类的基类,这些类的实例表示Java对象,这些类中会声明一些保存Java对象信息的字段。
instanceOopDesc是Java类的实例。
markOopDesc是指Java对象的头信息。
arrayOopDesc是所有数组的抽象基类。它没有声明纯虚拟来强制执行此操作,因为这会在每个实例中分配一个 vtbl。
objArrayOopDesc是一个包含 oops 的数组。
typeArrayOop 是一个包含基本类型(非 oop 元素)的数组。它用于{characters, singles, doubles, bytes, shorts, integers, longs}的数组。
oop是指向一片内存的指针,这片内存可视作Java对象、数组(强制类型转换),对象的本质就是用对象头和字段数据填充这片内存。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)。
HopSpot虚拟机对象的对象头部分包括两类信息。
oopDesc
类的小部分如下,HotSpot VM 1.8版本,C++代码。
class oopDesc {
private:
markOop _mark;
union _metadata {
Klass *_klass;
narrowKlass _compressed_klass;
} _metadata;
}
第一类是用于存储对象自身的运行数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中分别是32个bit和64个bit。这部分内容被称为Mark Word。也就是上面oopDesc
代码中的markOop _mark
。
对象头的另外一部分是类型指针,上面代码中的_metadata
,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
_metadata
是一个union
类型,包含Klass *_klass;
和narrowKlass _compressed_klass;
。Klass
表示正常的指针,narrowKlass
是针对64位CPU的优化。如果开启了类指针压缩-XX:+UseCompressedClassPointers
,HotSpot会把指向klass的指针压缩为一个无符号32位整数(_compressed_klass),剩下的32位用于存放对象字段数据。如果是typeArrayOop
或objArrayOop
,还能存放数组长度。
实例数据部分是对象真正存储的有效信息,也就是我们在程序代码里面定义的各种类型的字段内容,包括父类继承下来的和子类中定义的属性,都必须记录下来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle)和属性在Java源码中定义的顺序的影响。
HotSpot虚拟机默认的分配顺序为long/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),相同宽度的属性会被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot的参数-XX:+CompactFields
是开启状态,那子类之中较窄的属性也允许插入父类属性的空隙中,可以节省一点空间。
对齐填充,并不是必须的,只是占位符的作用。HotSpot的自动内存管理系统要求对象起始地址必须是8字节的整数倍,任何对象的大小都是8字节的整数倍。对象头部被设计成8字节的倍数(1倍或2倍)。因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
JOL(Java Object Layout),是openjdk的一个工具,可以查看详细的对象布局。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
把包引入到项目中,放一段测试用的代码
import org.openjdk.jol.info.ClassLayout;
import java.util.List;
/**
* @author 编程还未
*/
public class TestJOL {
public static void main(String[] args) {
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
static class User {
byte a;
short b;
int c;
long d;
float e;
double f;
boolean g;
char h;
String i;
Child child;
List<Child> childList;
}
static class Child {
String name;
}
}
开启指针压缩(-XX:+UseCompressedOops
)运行结果如下:
com.program_highway.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00c01200
12 4 int User.c 0
16 8 long User.d 0
24 8 double User.f 0.0
32 4 float User.e 0.0
36 2 short User.b 0
38 2 char User.h
40 1 byte User.a 0
41 1 boolean User.g false
42 2 (alignment/padding gap)
44 4 java.lang.String User.i null
48 4 com.program_highway.TestJOL.Child User.child null
52 4 java.util.List User.childList null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
关闭指针压缩(-XX:-UseCompressedOops
)运行结果如下:
com.program_highway.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00c01200
12 4 int User.c 0
16 8 long User.d 0
24 8 double User.f 0.0
32 4 float User.e 0.0
36 2 short User.b 0
38 2 char User.h
40 1 byte User.a 0
41 1 boolean User.g false
42 6 (alignment/padding gap)
48 8 java.lang.String User.i null
56 8 com.program_highway.TestJOL.Child User.child null
64 8 java.util.List User.childList null
Instance size: 72 bytes
Space losses: 6 bytes internal + 0 bytes external = 6 bytes total
对象大小=对象头+实例数据
本小节内的运行结果,还是用JOL中的Java示例,Java1.8版本
alignment/padding gap
:对齐/填充间隙(基本数据类型用的)。单个对象内部的成员变量填充对齐。开启指针压缩按4的倍数补齐;关闭指针压缩按8的倍数补齐。因为对象内部的引用类型要么4bytes要么8bytes,所以说这个属性是基本数据类型对齐用的,下面不再重复解释。
object alignment gap
:对象对齐间隙。单个对象内部整体按8的倍数对齐。
-XX:+UseCompressedOops
:普通指针压缩。64位HotSpot VM1.6 update14 开始正式支持。会把对象内的普通对象实例由8 bytes(64位)压缩到4 bytes(32位)。
-XX:+UseCompressedClassPointers
:HotSpot会把对象头中_metadata
指向klass的指针压缩为一个无符号32位整数,4bytes。
JVM参数:-XX:+UseCompressedOops -XX:-UseCompressedClassPointers
com.program_highway.jol.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x00000266a13836e8
16 8 long User.d 0
24 8 double User.f 0.0
32 4 int User.c 0
36 4 float User.e 0.0
40 2 short User.b 0
42 2 char User.h
44 1 byte User.a 0
45 1 boolean User.g false
46 2 (alignment/padding gap)
48 4 java.lang.String User.i null
52 4 com.program_highway.jol.TestJOL.Child User.child null
56 4 java.util.List User.childList null
60 4 (object alignment gap)
Instance size: 64 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
该情况下计算:
对象大小=对象头+实例数据
对象大小=64 bytes
对象头=16 bytes,包含:object header: mark 8 bytes 和 object header: class 8 bytes
实例数据=分两大部分,一部分是基本数据类型,一部分是引用类型(对象)。基本数据类型的大小=8+8+4+4+2+2+1+1=30 bytes,补齐补2个bytes,32 bytes;引用类型的大小=4+4+4=12 bytes;
最终结果:对象大小(64 byets)=对象头(16bytes)+实例数据【基础类型(32bytes)+引用类型(12bytes)】+ 对象对齐间隙(4 bytes)
JVM参数:-XX:+UseCompressedOops -XX:+UseCompressedClassPointers
注意:-XX:+UseCompressedClassPointers
参数依赖-XX:+UseCompressedOops
这是JVM1.8默认的配置,两个压缩都开启。
com.program_highway.jol.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 int User.c 0
16 8 long User.d 0
24 8 double User.f 0.0
32 4 float User.e 0.0
36 2 short User.b 0
38 2 char User.h
40 1 byte User.a 0
41 1 boolean User.g false
42 2 (alignment/padding gap)
44 4 java.lang.String User.i null
48 4 com.program_highway.jol.TestJOL.Child User.child null
52 4 java.util.List User.childList null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
该情况下计算
对象大小=对象头+实例数据
对象大小=56 bytes
对象头=12 bytes,包含:object header: mark 8 bytes 和 object header: class 4 bytes
实例数据=分两大部分,一部分是基本数据类型,一部分是引用类型(对象)。基本数据类型的大小=8+8+4+4+2+2+1+1=30 bytes,补齐补2个bytes,32 bytes;引用类型的大小=4+4+4=12 bytes;
最终结果:对象大小(64 bytes)=对象头(12 bytes)+实例数据【基础类型(32 bytes)+引用类型(12 bytes)】
能被8整除,就不需要补齐了。
JVM参数:-XX:-UseCompressedOops -XX:-UseCompressedClassPointers
com.program_highway.jol.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x000001c320aa36e8
16 8 long User.d 0
24 8 double User.f 0.0
32 4 int User.c 0
36 4 float User.e 0.0
40 2 short User.b 0
42 2 char User.h
44 1 byte User.a 0
45 1 boolean User.g false
46 2 (alignment/padding gap)
48 8 java.lang.String User.i null
56 8 com.program_highway.jol.TestJOL.Child User.child null
64 8 java.util.List User.childList null
Instance size: 72 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
该情况下计算
对象大小=对象头+实例数据
对象大小=72 bytes
对象头=16 bytes,包含:object header: mark 8 bytes 和 object header: class 4 bytes
实例数据=分两大部分,一部分是基本数据类型,一部分是引用类型(对象)。基本数据类型的大小=8+8+4+4+2+2+1+1=30 bytes,8位补齐补2个bytes,32 bytes;引用类型的大小=8+8+8=24 bytes;
最终结果:对象大小(72 byets)=对象头(16 bytes)+实例数据【基础类型(32 bytes)+引用类型(24 bytes)】
能被8整除,就不需要补齐了。
对象大小受三个JVM属性的影响,上述两个-XX:+/-UseCompressedOops -XX:+/-UseCompressedClassPointers
,还有-XX:+/-CompactFields
。
-XX:+/-UseCompressedOops
:开启/关闭普通指针压缩
-XX:+/-UseCompressedClassPointers
:开启/关闭类指针压缩
-XX:+/-CompactFields
:开启/关闭紧凑属性,
这三个属性默认都是开启的,在默认情况下计算对象大小。
对象大小=对象头+实例数据+object alignment gap
(对象对齐间隙)
对象头=12 bytes
实例数据=基础数据类型占用之和+alignment/padding gap
(对齐/填充缝隙)+引用类型占用之和(一个对象占4 bytes)+object alignment gap
(对象对齐间隙)
有继承关系如何处理?(默认三个属性都开启)
如果上述类User
有继承关系且父类有属性,User
类会有父类的属性,父类的基本数据类型会单独计算。
Father
类,User
类继承Father
类
static class Father {
private String address;
short age;
}
结果:
com.program_highway.jol.TestJOL$User object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c182
12 2 short Father.age 0
14 2 (alignment/padding gap)
16 4 java.lang.String Father.address null
20 4 int User.c 0
24 8 long User.d 0
32 8 double User.f 0.0
40 4 float User.e 0.0
44 2 short User.b 0
46 2 char User.h
48 1 byte User.a 0
49 1 boolean User.g false
50 2 (alignment/padding gap)
52 4 java.lang.String User.i null
56 4 com.program_highway.jol.TestJOL.Child User.child null
60 4 java.util.List User.childList null
Instance size: 64 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
有继承关系的对象大小=对象头+父基本数据类型占用大小+父基本数据类型的alignment/padding gap
(对齐/填充缝隙)+ 自己的基本数据类型占用大小+自己的基本数据类型的alignment/padding gap
(对齐/填充缝隙)+ 父类和自己的引用类型占用之和(一个对象占4 bytes)
对象的访问,会通过保存在栈里的reference来操作堆里的对象。Java虚拟机规范只规定reference是一个指向对象的引用,没有规定引用怎样指向对象。指向的方式由虚拟机自行实现。主要有两种方式句柄和直接指针。
HotSpot主要使用直接访问的方式。当然其他语言的虚拟机中句柄访问也很常见。
Java堆中会划分出一块内存作为句柄池,reference中存储的是对象的句柄地址。句柄中包含对象实例数据和类型数据各自具体的地址信息。
句柄访问的优势就是reference中存储的是稳定句柄地址,在对象被移动时(垃圾回收的时候会移动对象),只会改变句柄中的实例数据指针,而reference本身不需要被修改。
Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的是对象地址。
直接访问的优势是速度更快,节省了一次指针定位的时间开销。由于对象访问在Java中非常频繁,节省的这一次开销是很有必要的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。