1 对象的创建方式
1.1 new关键字
这是最简单最常用的创建对象方式,包括无参的和有参的构造函数。
Student student1 = new Student();
Student student2 = new Student("小明");//name
1.2 Class或Constructor类的newInstance方法
该方式和Class类的newInstance方法很像。
java.lang.relect.Constructor
类里也有一个newInstance方法可以创建对象。
我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
// Class
Student student = (Student)Class.forName(className).newInstance();
// 或者:
Student stu = Student.class.newInstance();
// Constructor
Constructor<Student> constructor = Student.class.getInstance();
Student stu = constructor.newInstance();
1.3 Clone()方法
无论何时我们调用一个对象的clone方法,JVM都会创建一个新的对象,同时将前面的对象的内容全部拷贝进去。
事实上,用clone方法创建对象并不会调用任何构造函数。
需要注意的是,要使用clone方法,我们必须先实现Cloneable接口并实现其定义的clone方法。
Student stu2 = <Student>stu.clone();
1.4 反序列化
Java 中常常进行 JSON 数据跟 Java 对象之间的转换,即序列化和反序列化。
当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口,虽然该接口没有任何方法。
ObjectInputStream in = new ObjectInputStream (new FileInputStream("data.obj"));
Student stu3 = (Student)in.readObject();
//值得一说的时,很多时候一些优秀的第三方库可以帮我们很容易地实现序列化和反序列化。比如Jackson 、 Gson。
//下面是一个Jackson 的一个例子:
public static <T> T readValue(String content, TypeReference valueType) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return (T) objectMapper.readValue(content, valueType);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
总的看来,除了使用new关键字之外的其他方法全部都是转变为invokevirtual(创建对象的直接方法) 创建。
使用被new的方式转变为两个调用,new和invokespecial(构造函数调用)。
1.5 对比
两种newInstance方法区别:
- 1 从包名看,Class类位于java的lang包中,而构造器类是java反射机制的一部分。
- 2 实现上,Class类的newInstance只触发无参数的构造方法创建对象,而构造器类的newInstance能触发有参数或者任意参数的构造方法。
- 3 Class类的newInstance需要其构造方法是共有的或者对调用方法可见的,而构造器类的newInstance可以在特定环境下调用私有构造方法来创建对象。这点可以从上面源码的第1 条解释可以看出。
- 4 Class类的newInstance抛出类构造函数的异常,而构造器类的newInstance包装了一个InvocationTargetException异常。这是封装了一次的结果,即Class类本质上调用了反射包构造器类中无参数的newInstance方法,捕获了InvocationTargetException,将构造器本身的异常抛出。
- 5 有无使用构造函数:
有
:new、Class类&Constructor类的newInstance
无
:反序列化、Clone
2 对象内部结构
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)
、实例数据(Instance Data)
和对齐填充(Padding)
。
2.1 对象头(Header)
HotSpot虚拟机的对象头包括两部分信息:markword
和 klass
。
2.1.1 markword
第一部分markword,用于存储对象自身的运行时数据:
如哈希码(HashCode)
、GC分代年龄
、锁状态标志
、线程持有的锁
、偏向线程ID
、偏向时间戳
等;
这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”
。
2.1.2 klass
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
2.2 实例数据(Instance Data)
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
2.3 对齐填充(Padding)
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
2.4 对象大小计算
- 1 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
- 2 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
- 3 64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
- 4 静态属性不算在对象大小内。
/**
* 64位开启指针压缩的话,markword变成8字节,压缩了class指针为4字节,故对象头12字节
* 64位没有开启指针压缩的话,markword8字节,class指针8字节,对象头16字节
* 32位markword为4字节,class指针为4字节,对象头8字节
*
* 另外,静态属性所占用的空间通常不算在对象本身,因为它的引用是在方法区。
*
*/
public class ObjectSize {
public static void main(String[] args){
System.out.println(SizeOfTool.getObjectSize(new A(),SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new B(),SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new C(),SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new D(),SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new E(),SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new Q(),SizeEnum.B));
/**
* 64位压缩指针下,对象头12字节,数组长度描述4字节,数据4*100 =16+400 = 416
*/
System.out.println(SizeOfTool.getObjectSize(new int[100],SizeEnum.B));
/**
* 属性4位对齐
* 64位压缩指针下,对象头12字节,数组长度描述4字节,数据1*100,对齐后104 = 16+104 = 120
*/
System.out.println(SizeOfTool.getObjectSize(new byte[100],SizeEnum.B));
/**
* 二维数组
* 64位指针压缩下
* 第1维数组,对象头12字节,数组长度描述4字节,2个数组引用共8字节,共24字节
* 第2维数组,对象头12字节,数组长度描述4字节,100个数组引用共400字节,对齐后共416字节
* 第1维的2个引用所指对象大小 = 2*416 = 832 字节
* 共24+832 = 856字节
*/
System.out.println(SizeOfTool.getObjectSize(new int[2][100],SizeEnum.B));
/**
* 二维数组
* 64位指针压缩下
* 第1维数组,对象头12字节,数组长度描述4字节,100个数组引用共400字节,共416字节
* 第2维数组,对象头12字节,数组长度描述4字节,2个数组引用共8字节,共24字节
* 第1维的100个引用所指对象大小 = 100*24 = 2400 字节
* 共416+2400 = 2816字节
*/
System.out.println(SizeOfTool.getObjectSize(new int[100][2],SizeEnum.B));
System.out.println(SizeOfTool.getObjectSize(new Object(),SizeEnum.B));
/**
* 不算static属性
* private final char value[];
* private int hash; // Default to 0
* private transient int hash32 = 0;
*
* 32位下,String对象头8字节,2个int类型8字节,char数组引用占4字节,共占24字节
* 另外,还要算上value[]数组的占用,数组对象头部8字节,数组长度4字节,对齐后共占16字节
* -> String对象对象大小24+16 = 40字节
* 64位开启指针压缩下(压缩指针),String对象头12字节,2个int类型8字节,char数组引用占4字节,共占24字节
* 另外,还要算上value[]数组的占用,数组对象头部12字节,数组长度4字节,对齐后共占16字节
* -> String对象大小24+16=40字节
*/
System.out.println(SizeOfTool.getObjectSize(new String(),SizeEnum.B));
/**
* transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
* transient int size;
* int threshold;
* final float loadFactor;
* transient int modCount;
*
* 64位开启指针压缩下,对象头部12字节,数组引用4字节,3个int12字节,float4字节,共32字节
* 另外,算上Entry<K,V>[] = 对象头12 +属性16字节+数组长度4字节 = 32字节
* final K key;
* V value;
* Entry<K,V> next;
* int hash;
* 对象头12字节,3个引用共12字节,1个int4字节 -> 一个entry至少占用28字节
* -> 32+32=64字节
*/
System.out.println(SizeOfTool.getObjectSize(new HashMap(),SizeEnum.B));
}
}
//32位下对象头8字节,byte占1字节,对其填充后,总占16字节
//64位开启指针压缩下对象头12字节,byte1字节,对齐后占16字节
class A{
byte b1;
}
//32位下对象头8字节,8个byte8字节,总16字节
//64位开启指针压缩下对象头12字节,8个byte8字节,对齐后占24字节
class B{
byte b1,b2,b3,b4,b5,b6,b7,b8;
}
//32位下对象头8字节,9个byte9字节,对其填充后,总24字节
//64位开启指针压缩下对象头12字节,9个byte9字节,对齐后占24字节
class C{
byte b1,b2,b3,b4,b5,b6,b7,b8,b9;
}
//32位下对象头8字节,int占4字节,引用占4字节,共16字节
//64位开启指针压缩下对象头12字节,int占4字节,引用占4字节,对齐后占24字节
class D{
int i;
String str;
}
//32位下对象头8字节,int4字节,byte占1字节,引用占4字节,对其后,共24字节
//64位开启指针压缩下对象头12字节,int占4字节,引用占4字节,byte占1字节,对齐后占24字节
class E{
int i;
byte b;
String str;
}
/**
* 对齐有两种
* 1、整个对象8字节对齐
* 2、属性4字节对齐 ****
*
* 对象集成属性的排布
* markword 4 8
* class指针 4 4
* 父类的父类属性 1 1
* 属性对齐 3 3
* 父类的属性 1 1
* 属性对齐 3 3
* 当前类的属性 1 1
* 属性对齐填充 3 3
* 整个对象对齐 8+12 =》 24 12+12=》24
*/
class O{
byte b;
}
class P extends O{
byte b;
}
class Q extends P{
byte b;
}
3 new一个对象时候JVM做了什么
在创建对象的时候,jvm为我们做了什么?
类加载阶段先父后子
,创建对象也是先父后子
package stringTest;
/**
* Intent: 父类子类的加载顺序
*/
public class LoadingOrder {
public static void main(String[] args) throws Exception {
B b = new B();
// Class.forName("stringTest.B"); // 执行静态代码块和为静态属性初始化值
// ClassLoader.getSystemClassLoader().loadClass("stringTest.B"); // 不执行,只加载类
}
}
class A {
// 静态代码块和静态属性按顺序执行(类加载阶段)
static int staticNum = staticMethod();
static {
System.out.println("A静态代码块");
}
// 非静态属性初始化值和非静态代码块顺序执行(创建对象阶段)
int num = method();
{
System.out.println("A代码块");
}
// 最后才调用构造方法
A() {
System.out.println("A构造方法");
}
static int staticMethod() {
System.out.println("A调用静态方法为静态属性初始化值");
return 5;
}
int method() {
System.out.println("A调用普通方法为属性初始化值");
return 5;
}
}
class B extends A {
static int num2 = staticMethod1();
static {
System.out.println("B静态代码块");
}
static int staticMethod1() {
System.out.println("B调用静态方法为静态属性初始化值");
return 5;
}
{
System.out.println("B代码块");
}
B() {
System.out.println("B构造方法");
}
}
Class.forName()与ClassLoader.getSystemClassLoader().loaderClass()这两种类加载方式有所区别:
1.Class.forName()会执行静态代码块并且为静态属性初始化值。
2.classLoader只负责把类从硬盘或网络中加载到内存中,不进行连接,也就没有后面的初始化,所以不会执行。
这也是为什么获取JDBC驱动时要用Class.forName()的原因,其源码是在静态代码块中获取驱动对象的
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
//往DriverManager中注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
}
}
4 JVM中对象的寻找
对象的访问定位:句柄与直接指针。
Java程序通过栈中的reference数据来操作堆上的具体对象,而对象的访问方式取决于虚拟机的实现。
主流访问方式有句柄
和直接指针
两种。
4.1 句柄方式
Java堆中将会划出一块内存来作为句柄池,reference对象存储的就是对象的句柄地址。句柄中包含了对象实例数据和类型数据的具体地址:
4.2 直接指针方式
reference对象直接存储对象地址:
4.3 两者对比
- 句柄
由于reference中存储的是稳定的句柄地址,在对象被移动时(如GC过程中的对象移动),只需改变句柄中实例数据指针,而reference本身不用动。
- 直接指针
速度快,节省了一次指针定位的时间开销,HotSpot采用此方式。
5 String类在内存中实现
==:比较引用类型比较的是地址值是否相同
equals:比较引用类型默认也是比较地址值是否相同,而String类重写了equals()方法,比较的是内容是否相同。
String s = new String(“hello”)和String s = “hello”;
的区别?
String s = new String(“hello”)
会创建2(1)个对象,String s = “hello”
创建1(0)个对象。
注:当字符串常量池中有对象hello时括号内成立!
public class Str {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = "hello";
String s3 = "helloworld";
System.out.println(s1 == s2);// false
System.out.println(s1.equals(s2));// true
System.out.println(s3 == s1 + s2);// false
System.out.println(s3.equals((s1 + s2)));// true
System.out.println(s3 == "hello" + "world");//false
System.out.println(s3.equals("hello" + "world"));// true
}
}
jdk1.6
下字符串常量池是在永久区中,是与堆完全独立的两个空间;
jdk1.7,1.8
下字符串常量池已经转移到堆中了,是堆中的一部分内容。
String s = new String(“hello”)
会创建2(1)个对象,String s = “hello”创建1(0)个对象。
当字符串常量池中有对象hello时括号内成立。
字符串如果是变量相加,先开空间,在拼接。
字符串如果是常量相加,是先加,然后在常量池找,如果有就直接返回,否则,就创建。