logo头像
Snippet 博客主题

面试官:你真的了解Java对象吗

公司最近在招一位4年以上的Java高级开发工程师,今天早上HR给了我一份的简历,让我去会议室进行技术面试。

我大概看了下简历的基本项(男、29岁、本科、5年Java开发经验),就去会议室面见应聘者了。

首先见了应聘者打了声招呼,紧接着他开始自我介绍,balabala………

介绍完毕后,试想着,5年工作经验应该在可以受得了我无情的毒打。

然后,我问了一些基础性东西,他的回答令我比较满意,但是都是凤毛麟角。

因为我个人喜欢深度挖掘知识点,随后,我为他准备了面试连环炮,但结果却事与愿违。

面了几下后了解到他的离职原因: 由于今年疫情扩散和资本市场萎缩的原因,上一家公司(国企)大幅度降薪,工作不好找。

不过,他还是算比较乐观的,说是同事和他同样工作经验,都可以找20k以上的薪资,他相信他也可以。

我面了几下后,就让回去等通知了。不是我个人比较自满,而是现在的市场太混乱、人心太浮躁罢了。

5年经验的技术开发者,上来只想着谈分布式、谈架构,真的是让我大为所叹。

不积跬步,无以至千里;不积小流,无以成江海。

架构都是一点一点的基础累积下来的,基础不扎实,谈何架构,谈何未来?

现在早已不是2015年那个时候,闭着眼睛会CRUD就可以找到一份待遇不错的工作。

我现在终于明白为什么大厂特别注重数据结构与算法、计算机基础与原理、框架原理、网络通信原理。

随着时代的进步,初级工程师已经太饱和了,高级工程师还很稀缺。这个社会早对IT从业人员有了更高的要求。

下面是我问他Java对象相关知识点的面试连环炮,希望以后的日子里,自己能够引以为戒,持续深入学习。

1. 你了解Java的Object类吗?

万物皆对象。Java中java.lang.Object类是Java Class的超类,任何Java类的顶层都隐式继承它。

下面我们通过源码来学习一下Object.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package java.lang;

/**
* Class Object是类Object结构的根。
* 每个CLass都将Object作为超类。
× 所有对象(包括数组)都实现了这个类的方法。
*
* @author unascribed
* @see java.lang.Class
* @since JDK1.0
*/
public class Object {

private static native void registerNatives();
static {
// 静态初始化native方法,即使与操作系统绑定的C/C++方法
registerNatives();
}

/**
* 返回此Object的运行时类。
* 返回的类对象是被表示类的static synchronized方法锁定的对象。
*
* 实际结果的类型是Class<? extends |X|>其中|X|是静态类型上其表达的擦除getClass被调用。
* 例如,在此代码片段中不需要转换:
* Number n = 0;
* Class<? extends Number> c = n.getClass();
*
* @return 返回类对象的运行时类的Class对象。
* @jls 15.8.2 Class Literals
*/
public final native Class<?> getClass();

/**
* 返回对象的哈希码值
*/
public native int hashCode();


/**
* 返回当前对象this和obj是否指向的是同一个引用
*/
public boolean equals(Object obj) {
return (this == obj);
}

/**
* 创建并返回此对象的副本。
*/
protected native Object clone() throws CloneNotSupportedException;

/**
* 返回对象的字符串表示形式。默认:Class Name + @ + HashCode的十六进制
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

/**
* 唤醒正在等待对象监视器的单个线程。只能唤醒一个线程。
*/
public final native void notify();


/**
* 唤醒正在等待对象监视器的所有线程。可以唤醒多个线程。
*/
public final native void notifyAll();

/**
* 使当前线程等待。
* 直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间timeout已到才被唤醒。
*/
public final native void wait(long timeout) throws InterruptedException;


/**
* 和wait(long timeout)类似,但它允许对放弃之前等待通知的时间进行更精细(纳秒)的控制。
*/
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos > 0) {
timeout++;
}

wait(timeout);
}

/**
* 相当于wait(0),线程的虚假唤醒:不可以用if判断,而是用while判断。
*/
public final void wait() throws InterruptedException {
wait(0);
}

/**
* 当垃圾收集确定不再有对该对象的引用时,会调用这个方法进行GC。只会调用一次。
*/
protected void finalize() throws Throwable { }

2. 解释一下对象的创建过程?

当然不是简单的new Object();操作,对象的创建存在半初始化状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ObjectTest {

private int age ;

public ObjectTest(int age) {
this.age = age;
}

public static void main(String[] args) {
ObjectTest obj = new ObjectTest(25);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
litong@LT:/media/litong/文档/code/java/back_end_services/java/src$ javac ObjectTest.java # 编译字节码
litong@LT:/media/litong/文档/code/java/back_end_services/java/src$ javap -c ObjectTest.class # 查看字节码
Compiled from "ObjectTest.java"
public class ObjectTest {
public ObjectTest(int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field age:I
9: return

public static void main(java.lang.String[]);
Code:
0: new #3 // class ObjectTest
3: dup
4: bipush 25
6: invokespecial #4 // Method "<init>":(I)V
9: astore_1
10: return
}

一个对象的创建至少经过new、dup、invokespecial、astore_1三个字节码指令:

  • new: 当调用new指令时,JVM虚拟机向操作系统申请合适的空间,创建了一个类的实例,但是还没有调用构造器函数。
  • dup:使用invokespecial命令会从操作数堆栈中弹出nargs参数值和objectref正是因为需要调用这个函数才导致中间必须要有一个 dup 指令,不然调用完<init>函数以后,操作数栈为空,就再也找不回刚刚创建的对象了。
  • invokespecial : 使用 invokespecial调用了 <init> 后才真正调用了构造器函数。
  • astore_1:将对象空间和对象引用关联。

上述Java代码片段ObjectTest obj = new ObjectTest(25); 对象的转述流程如下:

  • 类校验成功后,加载ObjectTest.class。
  • 内存中申请一块合适大小的内存。并在内存生成一个引用变量,此时内存地址和引用变量未产生关联。
  • 第一次初始化:加载初始化,成员变量若为基本数据类型,则默认值是基本数据类型的默认值;若为引用类型,默认值为null。此时age的值为0。
  • 第二次初始化:构造初始化,此时age的值为25;
  • 所有初始化完成后,将引用变量的指向new的那块内存地址,完成对象的创建。

3. DCL单例到底需不需要加volatite?

答案是加,而且一定要加。首先我们先回顾一次懒汉式单例到DCL单例的演变,然后我们一一剖析为什么一定要加volatite。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.concurrent.TimeUnit;

/**
* @projectName: Singleton
* @className: com.java.Singleton
* @description: 单例对象演示
* @author: tong.li
* @createTime: 2020/6/29 下午21:37
* @version: v1.0
*/
public class Singleton {

/** 单例实例引用 */
private static Singleton SINGLETON = null;


/**
* 私有构造,禁止外部构造
*/
private Singleton(){
}


public static Singleton getInstance() {
// 首先判断是否为null
if (SINGLETON == null) { // 假设所有线程都进来,先阻塞1秒,都全部唤醒后,会new多次
try {
// 这里睡眠1秒,会实例化多次
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 若为null,直接new
SINGLETON = new Singleton();
}
// 返回最终的实例对象
return SINGLETON;
}

public static void main(String[] args) {

// 创建具有5个线程的线程池
for (int i = 0; i < 5 ; i++) {
// 多线程访问对象,看是否同一个对象
new Thread(() -> System.out.println(Singleton.getInstance())).start();
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
# 上述代码运行结果,很明显不是同一个对象,意味着该对象不是单例的
com.java.object.Singleton@6fcaf925
com.java.object.Singleton@39531411
com.java.object.Singleton@220af032
com.java.object.Singleton@39531411
com.java.object.Singleton@78541db1
# 在getInstance()方法直接加synchronized运行可以解决该问题,但是锁粒度太大,性能很低效
com.java.object.Singleton@4e38bc80
com.java.object.Singleton@4e38bc80
com.java.object.Singleton@4e38bc80
com.java.object.Singleton@4e38bc80
com.java.object.Singleton@4e38bc80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 锁细化后仍然存在线程安全问题
*/
public static Singleton getInstance() {
// 首先判断是否为null
if (SINGLETON == null) {
// 锁粒度细小化,但是仍然存在线程安全
// 如果线程1先进来再阻塞,其他线程都会进入到这个判断里等待线程1释放锁。
// 若线程1创建完对象后释放锁,其他线程都可以竞争这把锁,从而会多次new
synchronized (Singleton.class) {
try {
// 这里睡眠1秒,会实例化多次
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 若为null,直接new
SINGLETON = new Singleton();
}
}
// 返回最终的实例对象
return SINGLETON;
}
1
2
3
4
5
6
# 锁细化后的执行结果仍然存在线程安全问题,不是单例对象。
com.java.object.Singleton@17ea7521
com.java.object.Singleton@220af032
com.java.object.Singleton@6fcaf925
com.java.object.Singleton@39531411
com.java.object.Singleton@4e38bc80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* DCL,双重判断保证单例
*/
public static Singleton getInstance() {
// 首先判断是否为null
if (SINGLETON == null) {
// 锁粒度细小化
synchronized (Singleton.class) {
// DCL(Double Check Lock),锁的双重判断,即使多线程进来,也确保是单例
if (SINGLETON == null) {
try {
// 这里睡眠1秒,会实例化多次
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 若为null,直接new
SINGLETON = new Singleton();
}
}
}
// 返回最终的实例对象
return SINGLETON;
}
1
2
3
4
5
6
# DCL模式下肯定是单例
com.java.object.Singleton@78541db1
com.java.object.Singleton@78541db1
com.java.object.Singleton@78541db1
com.java.object.Singleton@78541db1
com.java.object.Singleton@78541db1

了解到了DCL,我们回过头来看问题,volatite有两种语义:保证线程可见和禁止指令重排序。

结合问题2,对象的创建存在半初始化状态,若没有被volatite修饰的,创建对象的指令有可能重排序。

若在此基础上多线程访问,访问到的Object可能是半初始化不完整的对象。所以一定DCL单例一定要加volatile。

4. 简述对象在内存中的布局

在HotSpot虚拟机中,对象在内存中的布局分为三部分:对象头、实例数据、对齐填充。

1. 对象头:包括两部分:Mark Word、类型指针、数组长度(如果是数组类型)
 1. Mark Word:存储对象自身的运行时数据(哈希码、GC年龄、锁标志、持有的锁等)。被设计成非固定数据结构,根据对象状态占用内部空间。64位虚拟机占用8个字节。
 2. Class Poniter:对象指向它的类元数据的指针。虚拟机通过这个指针确定对象是哪个类的实例。开启指针压缩(-XX:+UseCompressedOops)占4个字节,未开启压缩占8个字节。
 3. length:数组长度,对于数组对象,对象头中必须有一块数据记录数组长度,因为JVM无法从数组的元数据确定数组的大小。也占用4字节。
2. Instance Data:实例数据是对象真正存储的有效信息,就是代码中定义的各种类型的字段内容,包括从父类继承下来的和子类中定义的。
3. Padding: HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。当对象的实例数据部分没有对齐,用对齐填充补全,连续空间会将读效率会更高。所以对齐填充不是必然存在的。

5. 对象头具体包括什么?

对象头包含Mark Word和Class Poniter。

在OpenJDK的HotSpot源码markOop.hpp 中可以找到定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef SHARE_VM_OOPS_MARKOOP_HPP
#define SHARE_VM_OOPS_MARKOOP_HPP

#include "oops/oop.hpp"

// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//

6. 为什么堆内存到32G后,指针压缩会失效?

对象头中的Class Pointer默认占8个字节,开启-XX:+UseCompressedOops后,为了节省空间压缩为4个字节,4*8=32位表示可寻址4G个对象,在内存空间小于32G时,可以通过编码、解码方式进行优化,使得jvm可以支持更大的内存配置。当堆内存大于32G时,压缩指针参数会失效,会强制使用64位(即8字节)来对java对象寻址。

32位操作系统可以寻址到多大内存 答:4g 因为 2^32=4 1024 1024=4g,那么64位寻址就更大了。

用64位寻址,会给我们寻址带宽和对象引用造成负担。寄存器的寻址能力在32G左右,所以开启指针压缩和未开启指针压缩都会按64位进行寻址。

一旦你越过那个神奇的30-32G的边界,指针就会切回普通对象的指针,每个对象的指针都变长了,就会使用更多的CPU内存带宽,也就是说你实际上失去了更多的内存。事实上当内存到达40-50GB的时候,有效内存才相当于使用内存对象指针压缩技术时候的32G内存。

这段描述的意思就是说:即便你有足够的内存,也尽量不要超过32G,因为它浪费了内存,降低了CPU的性能,还要让GC应对大内存显得费时费力。

7. 简述对象的分配过程

优先栈上分配,分析是否有对象逃逸。栈上分配对象,对象弹栈,对象生命周期结束,不需要GC介入,效率高。

优先分配大对象,直接分配到老年代。

优先给TLAB(Thread Local Allocation Buffer,线程本地分配缓存区)分配,因为多个线程都会同一块区域分配(抢内存,俗称指针碰撞),TLAB其实是Eden区划分好的线程区域。

再TLAB分配完成后,经历过一次YGC,垃圾对象直接回收,若改对象不是垃圾对象,年龄不够,复制到Surivor区,循环往复。直到年龄够大,会被放入老年代中。

1593940781933

8. Object obj = new Object()在内存中占用多少个字节?

普通对象开启指针压缩:Mark Word(8字节)+Class Pointer(4字节)+Padding(4字节) = 16 字节

普通对象未开启指针压缩(含指针压缩失效):Mark Word(8字节)+Class Pointer(8字节)+Padding(4字节) = 20 字节

用代码查看内存布局:

  1. 在pom中导入java对象布局依赖

    1
    2
    3
    4
    5
    6
    <!--  java对象布局依赖工具 -->
    <dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
    </dependency>
  1. 编写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.openjdk.jol.info.ClassLayout;

/**
* @projectName: ObjectLayout
* @className: com.java.ObjectLayoutDemo
* @description: 对象布局演示
* @author: tong.li
* @createTime: 2020/6/29 下午5:38
* @version: v1.0
*/
public class ObjectLayoutDemo {

private long a;

public static void main(String[] args) {

Object obj = new Object();
// 打印空对象的内存布局,总共16个字节
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("--------------------------------------------------------------");
ObjectLayoutDemo old = new ObjectLayoutDemo();
// 打印有实例数据对象的内存布局,
// 如果a是4个字节以下的数据类型,大小仍然保持16个字节,int类型(4个字节)不用填充,char类型(2个字节),需要填充2个字节,
// 如果是long:对象头12字节+8个字节=20,需要对齐(8的整数倍)=24
System.out.println(ClassLayout.parseInstance(old).toPrintable());
System.out.println("--------------------------------------------------------------");
// 打印数组(无数据)的内存布局,4+4+4+4(数组长度)=16,不需要补齐
int[] array1 = new int[0];
System.out.println(ClassLayout.parseInstance(array1).toPrintable());
System.out.println("--------------------------------------------------------------");
// 打印数组(有数据)的内存布局
// 16 + 2 × 4 = 24,不用补齐
int[] array2 = new int[2];
System.out.println(ClassLayout.parseInstance(array2).toPrintable());
int[] array3 = new int[3];
// 16+3*4=28,补齐到32字节
System.out.println(ClassLayout.parseInstance(array3).toPrintable());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# 前面8个字节是Mark Word,后面一个是Class Poniter,最后一个是内存对齐
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

--------------------------------------------------------------
com.java.object.entity.ObjectLayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (alignment/padding gap)
16 8 long ObjectLayoutDemo.a 0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

--------------------------------------------------------------
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

--------------------------------------------------------------
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 8 int [I.<elements> N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 int [I.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

9. 对象怎么定位?

  • 间接句柄方式:方便GC,GC复制的时候不需要移动这个句柄指针。
  • 直接指针方式:访问效率高,JaHotSpot虚拟机默认使用对象定位方式。

10. 为什么HotSpot不使用C++对象来代表Java对象?

HotSpot虚拟机定义了OOP/Class二元模型。Java对象里面除过有个普通对象,还有一个字节码的类对象。

每个C++对象里面有虚函数表。若用C++对象直接代表Java对象,Java都要装载这个虚函数表,内存占用太大,所以不采用。

11. Class对象是在堆还是在方法区?

Class对象是存放在堆区的,不是方法区。

Class类的元数据(方法代码、变量名、方法名、访问权限、返回值)都在方法区(元空间或永久带),但是Class类的实例对象是在堆区的。

当我们加载一个类时,实际上,Method Area区有个C++对象InstanceClassOop指向堆中的类对象。

支付宝打赏 微信打赏

请作者喝杯咖啡吧