Java 小知识点:Object 问题以及各种小知识点

汇总一下之前写的小知识点

关于 Object o = new Object() 的几个问题

一、对象创建的过程

主要知识点:对象创建过程中的半初始化状态

1、以下面这段代码为例,看一下 Object 对象创建的过程

Object o = new Object();

2、通过 idea 里面的 jclasslib 插件可以获取对应的字节码
jclasslib 插件
2.1、根据 jclasslib 获取到上面代码运行的字节码信息如下
Object 对象创建过程字节码
3、通过字节码信息来分析对象的创建过程

  1. 首先根据第 1 行字节码大概能分析出这是 new 一个对象,对应的是 java.lang.Object 对象,可以大概总结出这一行是申请一块内存存储 new 出来的对象,这时候对象里面的成员变量就是一个默认初始值
  2. 接着看第 3 行字节码,字面意思就是调用 java.lang.Object 的 init 方法,这里就是调用对象的构造方法进行初始化,进行初始化之后对象里面的成员变量就是正常的赋值
  3. 第 4 行字节码的意思就是将变量 o 与对象建立关联

二、DCL 单例到底需不需要加 volatile 修饰

主要知识点:指令重排序问题

1、相关知识点

  • volatile:修饰代表线程间可见,同时禁止指令重排序
  • 指令重排序:是 CPU 获取内存进行等待的时候同时执行指令,期间执行的顺序就变成乱序,也是因为 CPU 提高效率而乱的,就好像煮饭的同时可以做其他的
  • 单例:某一个类只能在内存里 new 出一个对象
  • DCL:Double check lock 双重检查锁

2、DCL 单例代码

public class DCL {
    
    

    private static volatile DCL INSTANCE;

    private DCL() {
    
    }

    public static DCL getInstance() {
    
    
        if (INSTANCE == null) {
    
    
            synchronized (DCL.class) {
    
    
                if (INSTANCE == null) {
    
    
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                    INSTANCE = new DCL();
                }
            }
        }
        return INSTANCE;
    }

}

3、分析过程

  1. 对象创建的过程 问题中可以知道,在第 1 行指令执行半初始化的时候,如果第 3、4 行指令发生了指令重排序!!!
  2. 这时候 INSTANCE 才 new 到一半,还没有执行构造方法进行初始化,第二个线程拿到的就是一个半初始化对象
  3. 这时候对象里面的成员变量还没有赋上对应的值,无法保证数据一致性
  4. 结论:如果要保证数据一致性就需要加,不需要保证数据一致性可以不加 (但是一般单例都是有成员变量,所以一般都要加)

三、对象在内存中的存储布局

主要知识点:对象、数组在内存中的布局

1、查看布局需要引入对应的依赖包,这里引入的是 jol-cord
JOL:Java Object Layer

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

2、普通对象的内存布局

Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

/** 运行结果
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
*/

3、数组对象的内存布局

Object[] os = new Object[1];
System.out.println(ClassLayout.parseInstance(os).toPrintable());

/** 运行结果
[Ljava.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)                           f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779)
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     4   java.lang.Object Object;.<elements>                        N/A
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*/

4、布局分析

普通对象

  • 前面三部分 12 个字节都是 object header,分别是 markword、class pointer、instance data
  • 第 12 个字节往后 4 个字节是下一个对象的补齐 padding
  • 所以 Object o = new Object() 在内存中占用 16 个字节

数组对象

  • 和普通对象相比 object header 多一个 4 字节的数组长度和 4 字节的 elements

四、对象头具体包括什么

主要知识点:markword、klasspointer

MarkWord

  • 主要存储对象自身运行时数据
  • 锁信息、GC 信息、Identity hashcode
  • 测试方式:给对象加上锁,可以看到内存布局 object header 里面的 VALUE 发生了变化
synchronized (o) {
    
    
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

/** 运行结果
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 a9 20 09 (10001000 10101001 00100000 00001001) (153135496)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      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
*/

KlassPointer

  • 类型指针是对象指向类元数据的指针,通过这个指针知道这个对象是哪个类的实例
  • 对象指针有时候是 4 字节、有时候是 8 字节,主要是 jvm 默认启动了压缩 +UserCompressedClassPointers,将 64 位压缩成了 4 字节
  • 扩展:当内存超过 32G 的时候会膨胀成 8 字节,压缩不再起作用
    • KlassPointer 寻址极限是 4byte * 8 byte = 32bit,即 2 的 32 次方个内存单元地址,每个对象长度也必须是 8 的整数倍,当大于 32G 内存地址的时候,无法进行寻找,所以压缩失败

五、对象怎么定位

主要知识点:直接、间接寻址

间接 (句柄方式)

  • 指向一组指针,一个实例数据指针指向堆里面真正的对象,一个类型指针指向方法区的 class
  • 优点:方便 GC,GC 复制的时候指向的变量不需要变
  • 缺点:效率不高

直接指针

  • 直接指向堆里面的类型数据指针,类型数据指针指向方法区的 class
  • 优点:直接找效率高
  • 缺点:每一次变化,指向的变量都需要改变

六、对象怎么分配

主要知识点:线上 -> 线程本地 -> Eden -> Old

分配过程

  • new 对象的时候,首先判断是否能够放到栈上,栈一弹出 (pop) 就结束了
    • 栈大小够、且不存在逃逸就可以分配到栈上,回收的时候不需要 GC 介入,效率高
  • 不能放在栈上,再判断是否很大,很大直接放到老年代上,经过 Full GC、Major GC 进行对象回收结束
  • 如果不大,放在线程本地缓冲区(TLAB),放不下就放到 Eden 区
    • TLAB:Thread local allocation buffer,每个线程在 Eden 区都独占有一个额外的小空间,优先会放到这个地方,放不下就放到 Eden 区
  • 在 Eden 区被一次 GC 之后清除了就结束,没被清除就进入 Survive 区,再经过 GC,如果年龄够大的话就进入 Eden 区,不够就进入另一个 Survive 区循环

小知识点

1、String 和 StringBuffer 区别

  • String
    • String 对象不可变,值改变只是创建了一个新的对象,值存在于常量池,不用不会被销毁
    • String 类被 final 修饰,不可以被继承
  • StringBuffer
    • StringBuffer 对象可变,主要根据构造方法创建,对象存在栈区,不用会被销毁

2、int 和 Integer 区别

  • Integer 是 int 的包装类,int 是 java 的基本类型
  • Integer 默认值是 null,int 默认值是 0
  • Integer 是指向对象,int 是直接存储数据值(Java 会对 -128 ~ 127 的数值进行缓存)

3、数组 Array 和 列表 ArrayList 的区别

  • Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型
  • Array 大小固定,ArrayList 大小可以动态变化
  • ArrayList 提供更多的方法和特性

4、什么是值传递和引用传递

  • 值传递:在方法调用时,实参将自己的一份拷贝传给形参,在方法内对该参数修改不影响实参
  • 引用传递:在方法调用的时候,实参将自己的地址传递给形参,在方法内,对该参数的改变就是对实参实际操作
  • 基本类型是值传递,引用类型是引用传递

5、Java 支持的数据类型有哪些,什么是自动拆箱、自动装箱

  • byte、short、int、long、double、float、boolean、char
  • 自动拆箱:将一个包装类对象赋值给类基本类型的数据
  • 自动装箱:将一个基本类型的数据赋值给了包装类对象
  • 两者可以大大简化基本类型变量和包装类对象之间的转换过程

6、为什么会出现 4.0-3.6=0.4000000001 这种情况

  • 计算机使用的是二进制,但是浮点数没法使用二进制进行精确,计算机给我们展现十进制或者说我们输入十进制计算机去处理,这都需要不断进行二进制与十进制的转换

7、Java8 新特性简单介绍

  • Lambda 表达式
    • 参数列表 -> 实现逻辑
  • 函数式接口
    • 只包含一个抽象方法的接口,匿名实现类都可以用 Lambda 表达式来写
  • 方法引用/构造器引用
    • 方法引用
      • 对象::实例方法名
      • 类::静态方法名
      • 类::实例方法名
  • 构造器引用
    • ClassName::new
  • Stream API
    • 处理集合的关键抽象概念,提供了一种高效且易用的处理数据的方式
    • Collection 是一种静态的内存数据结构,主要面向内存,存储在内存中,Stream 是有关计算的,主要面向 CPU,通过 CPU 实现计算
    • 操作步骤
      • 创建 Stream:数据源获取流(数组、集合)
      • 中间操作:对数据源的数据进行处理
      • 终止操作:执行中间操作,并产生结果
    • 注意点:Stream 不会存储元素、不会改变源对象,返回一个新的结果 Stream,懒操作,需要结果的时候才会执行
  • Optional 类
    • 容器类,可以保存类型 T 的值,代表这个值存在,或者只保存 null,表示这个值不存在,可以避免空指针

8、== 和 equals 区别

  • == 是判断是不是指向同一个内存空间,equals 是判断指向的内存空间的值是不是相同
  • == 是对内存地址进行比较,equals 是对字符串内容进行比较
  • == 指引用是否相同,equals 指值是否相同

9、Object 如果不重写 hashCode,hashCode 如何计算出来

  • 如果不重写,使用的是本地方法,返回的是当前对象的内存地址

10、为什么重写 equals 还要重写 hashcode

  • 因为必须保证重写后的 equals 方法认定相同的两个对象拥有相同的 hashcode
  • hashcode 方法重写原则就是保证 equals 方法认定为相同的两个对象拥有相同的 hashcode

11、如果对一个类不重写,它的 equals 方法是如何比较的

  • 比较的是引用类型的变量所指向的对象地址,和 == 一样

12、Java 里面的 final 关键字是怎么用的

  • 修饰类、方法、局部变量、成员变量
  • 类不可被继承、方法不可被重写、变量赋值后不可修改

13、介绍一下 volatile

  • 保证可见性和禁止指令重排序的一个关键字
  • 被 volatile 修饰的变量存放在主内存中,修改的时候会同时修改主内存,读取的时候直接从主内存中读

14、关于 Synchronized 和 Lock

  • synchronized 可以加在方法或代码块中,lock 需要显性指定起始和结束位置
  • synchronized 托管在 jvm 执行,lock 是 Java 写的控制锁代码
  • synchronized 是悲观锁,lock 是乐观锁
  • synchronized 是关键字,lock 是接口

15、Synchronized 修饰静态方法和修饰成员方法锁的是什么

  • 静态方法:给对象加锁
  • 成员方法:给实例对象加锁

16、方法覆盖和方法重载的意思

  • 方法覆盖:覆盖掉之前的方法(Override)
  • 方法重载:相同的方法名,但是传递的参数不一样

17、如何通过反射获取对象私有字段值

  • getDeclaredField 获取字段,setAccessible(true) 设置访问权限

18、内部类可以引用他包含类的成员吗,有什么限制

  • 内部类访问规则
    • 内部类可以直接访问外部类中的成员,包括私有,因为内部类中持有一个外部类的引用(外部类名.this)
    • 外部类要访问内部类需要创建对象
  • 静态内部类
    • 用 static 修饰,在访问限制上它只能访问外部类中的 static 成员变量或方法
  • 成员内部类
    • 普通内部类可以无条件访问外部类的所有成员属性和成员方法(包括 private 和 static)
    • 内外部类拥有同名的变量或方法的时候,会隐藏外部的,如果需要访问外部的需要 外部类.this.成员 访问
  • 局部内部类
    • 定义在外部类的方法中,可以直接访问外部类的所有成员,但是不能随便访问局部变量,局部变量被 final 修饰才能访问
  • 匿名内部类
    • 内部类必须是继承一个类或实现一个接口

19、什么是范型

  • 是一种书写规范,编译时类型安全检测机制

20、解释一下类加载机制,双亲委派模型,好处是什么

  • 类加载机制
    • 把描述类的数据从 class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型
  • 双亲委派模型
    • 接到类加载请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器完成类加载任务,就成功返回,父类都无法完成加载时才自己去加载
  • 好处
    • 无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,所以在不同的环境中都是同一个类

21、static 关键字意思,是否可以 override private 或者 static 方法

  • static:static 关键字修饰的内容都是静态的
    • 方法、变量:不需要创建对象就能访问
    • 代码块:创建时首先执行的代码,进行一些复杂的初始化工作
  • 不能覆盖 private 和 static 方法,方法覆盖是运行时绑定,static 是编译时静态绑定,private 其他类无法访问这个方法

22、类和对象的区别

  • 类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。

23、线程和进程

  • 进程是资源分配的最小单位,线程是程序执行的最小单位,是资源调度的最小单位
  • 进程有独立地址空间,线程是共享进程中的数据
  • 一个进程可以有多个线程,一个线程只有一个进程
  • 不同进程之间需要通信实现同步,不同线程之间需要协作同步
  • 通俗:线程是儿子,进程是父亲,一个父亲可以有多个儿子,一个儿子只有一个父亲

猜你喜欢

转载自blog.csdn.net/baidu_40468340/article/details/128465625
今日推荐