1. 请解释一下对象创建的过程?
Java对象创建的过程主要分为以下五个步骤:
-
类加载检查
Java虚拟机在读取一条new指令时候,首先检查能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化。如果没有,则会先执行相应的类加载过程。 -
内存分配
在通过类加载检查后,则开始为新生的对象分配内存。该对象所需的内存大小在类加载完成后便可确定,因此为每个对象分配的内存大小是确定的。而分配方式主要有两种,分别为: (1)指针碰撞 应用场合:堆内存规整(通俗的说就是用过的内存被整齐充分的利用,用过的内存放在一边,没有用过的放在另外一边,而中间利用一个分界值指针对这两边的内存进行分界,从而掌握内存分配情况)。 即在开辟内存空间时候,将分界值指针往没用过的内存方向移动向应大小位置即可)。 将堆内存这样划分的代表的GC收集器算法有:Serial,ParNew (2)空闲列表 应用场合;堆内存不规整(虚拟机维护一个可以记录内存块是否可以用的列表来了解内存分配情况) 即在开辟内存空间时候,找到一块足够大的内存块分配给该对象即可,同时更新记录列表。 将堆内存这样划分的代表的GC收集器算法有:CMS
-
初始化默认值
在分配内存完成后,紧接着,虚拟机需要将分配到的内存空间都进行初始化(即给一些默认值),这样做是为了保证对象实例的字段在Java代码中可以在不赋初值的情况下使用。程序可以访问到这些字段对应数据类型的默认值。 -
设置对象头
初始化默认值完成后,虚拟机对对象进行一些简单设置,如标记该对象是哪个类的实例,这个对象的Hash码,该对象所处的年龄段等等(这些可以理解为对象实例的基本信息)。这些信息被写在对象头中。JVM根据当前的运行状态,会给出不同的设置方式。 -
执行初始化方法
在设置对象头完成后,最后执行由开发人员编写的对象的初始化方法,把对象按照开发人员的设计进行初始化,一个对象便创建出来了。
2. DCL(Double Check Lock)单例需不需要加volatile?
答: 需要添加volatile,否则可能会获取到半初始化对象从而引发程序未知错误!
单例对象用上volatile关键字,他的作用就是: 禁止指令重排序!
public class Singleton01 {
public int total = 10;
private static volatile Singleton01 instance;
private Singleton01(){}
public static Singleton01 getInstance(){
if(instance == null){
//Double Check Lock
synchronized (Singleton01.class){
if(instance == null){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Singleton01();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
Singleton01 instance1 = Singleton01.getInstance();
System.out.println(instance1.hashCode());
}
}
}
扩展知识:
- volatile的作用
volatile只能用来修饰成员变量,它有两大特性:可见性、有序性,此处的有序性区别于synchornized的有序性。synchornized的有序性指的是多个线程要有序地执行临界区的代码,而volatile的有序性指的是指令有序性(指令不可重排)。- 什么是指令重排序?
指令重排序是指在运行程序时,处理器为了优化性能,代码的执行顺序可能不会按代码的顺序执行,比如前后两个赋值指令,第一条需要去磁盘读取数据,第二条需要去内存读取数据,程序在执行的时候可能会在去磁盘读取数据的时候先把第二条赋值指令执行完,等从磁盘读完数据再执行第一条赋值指令,这就叫指令重拍序,而volatile保证的有序性即防止指令重排。
3.指令重排发生的条件?
不影响单线程的最终一致性,也就是说,在单个线程内,两条指令变换顺序,不会影响最终结果,那么处理器可能会因为性能考虑指令重排,比如两条单纯赋值的指令
3.对象在内存中的存储布局?(对象和数组的存储不同)
对象在堆内存中的存储布局可以划分为三个部分:
-
对象头(Header)
HotSpot虚拟机对象的对象头部分包括三类信息:
第一类(Mark Word)是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
另外一部分(Class Pointer)是类型指针,即对象指向它的类型元数据的指针。
数组长度: 只有数组对象才有,在32位或者64位JVM中,长度都是32bit。 -
实例数据(Instance Data)
对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。 -
对齐填充(Padding)
对齐填充仅仅起着占位符的作用,将对象补齐到8字节的整数倍。
由于虚拟机要求对象起始地址必须是8字节的整数倍,所以后面有几个字节用于把对象的大小补齐至8字节的整数倍,没有特别的功能,对齐填充不是必须存在的,仅仅是为了字节对齐。
为什么必须是8个字节?
根据“计算机组成原理”,8个字节是计算机读取和存储的最佳实践。
4. 对象头具体包括什么?
Java对象的对象头由三部分组成:
- Mark Word
MarkWord用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、同步锁信息、偏向锁标识等等。Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
通常我们都是使用的64位的JVM,Mark Word 在64位 JVM 中内部结构如下图:
-
类型指针
类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。Java对象的类数据保存在方法区。 -
数组长度(只有数组对象才有)
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度。如果对象是数组类型,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
5. 对象怎么定位?
比如:T t = new T(); 如何通过引用变量t 去 找到T的实例。
JVM的执行过程,其实是有两种方式的, (1)直接应用 和(2)句柄方式.
-
直接引用
优点: 直接访问
缺点: GC需要移动对象的时候稍微麻烦
-
句柄方式
优点: 对象小,垃圾回收时不用频繁改动t
缺点: 两次访问
6.对象怎么分配?
扩展知识:
- TLAB
TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。
如果没有启用 TLAB,多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程。
启用了 TLAB 之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作。
- 逃逸分析
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
经过逃逸分析之后,可以得到三种对象的逃逸状态。
- GlobalEscape(全局逃逸), 即一个对象的引用逃出了方法或者线程。例如,一个对象的引用是复制给了一个类变量,或者存储在在一个已经逃逸的对象当中,或者这个对象的引用作为方法的返回值返回给了调用方法。
- ArgEscape(参数级逃逸),即在方法调用过程当中传递对象的应用给一个方法。这种状态可以通过分析被调方法的二进制代码确定。
- NoEscape(没有逃逸),一个可以进行标量替换的对象。可以不将这种对象分配在传统的堆上。
编译器可以使用逃逸分析的结果,对程序进行一下优化。
a. 堆分配对象变成栈分配对象。一个方法当中的对象,对象的引用没有发生逃逸,那么这个方法可能会被分配在栈内存上而非常见的堆内存上。
b.消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。
c.矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。
7.Object o = new Object(); 在内存中占用几个字节?
如果jvm没开启CompressedClassPointers类型指针压缩,那么首先new Object()占用8(markword)+8(class pointer)+0(instance data)+0(补齐为8的倍数)16个字节
8.为什么hotsport不使用C++对象来代表java对象?
因为C++对象有一个virtual table 这个是java对象所不需要也没有的。会占用内存
9.Class对象是在堆中还是在方法区中?
Class对象是存放在堆中