Java

基础

移位

如果移位的位数超过数值所占有的位数会怎样?

当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。 也就是说:x<<42等同于x<<10,x>>42等同于x>>10,x >>>42等同于x >>> 10。

基本类型

为什么说是几乎所有对象实例都存在于堆中呢?

这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。

如果没有逃逸分析,则所有对象实例都存在于堆中。

基本数据类型并不是存放在栈中

如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆/方法区/元空间中。

包装类型的缓存机制了解么?

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。

String

Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间

异常

类继承结构:

不要在 finally 语句块中使用 return!

当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

public static void main(String[] args) {
    System.out.println(f(2));
}
public static int f(int value) {
    try {
        return value * value;
    } finally {
        if (value == 2) {
            return 0;
        }
    }
}

This will return 0

finally 中的代码一定会执行吗?

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
} finally {
    System.out.println("Finally");
}

序列化

序列化号 serialVersionUID 属于版本控制的作用。反序列化时,会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID。

为什么要手动指定 serialVersionUID?

为什么不推荐使用 JDK 自带的序列化?

相关的序列化/反序列化库

Kryo, Protobuf, Hessian, Thrift,Avro

泛型

泛型擦除

List<String> words = new ArrayList<>();
words.add(“Hello”);
String s = words.get(0)

会擦除成:

List words = new ArrayList();
words.add(“Hello”);
String s = (String) words.get(0);

代理

代理类和实现类都要实现同一个接口,一般用于实现类的代码无法修改的情况。

动态代理

BigDecimal

RoudingMode

Unsafe

获取Unsafe对象

由于默认getUnsafe方法由于调用类的类加载器必须是Bootstrap类加载,无法直接调用。可以通过反射的方式来获取:

private static Unsafe reflectGetUnsafe() {
    try {
      Field field = Unsafe.class.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      return (Unsafe) field.get(null);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
      return null;
    }
}

内存操作

//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);

操作的是堆外内存,需要手动释放。

为什么要使用堆外内存?

内存屏障

类似volatile,但是更加灵活。

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

对象操作

可以直接获取对象的字段的offset,并可以直接修改字段的值,越过访问权限。

long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value"));
Main main = new Main();
unsafe.putInt(main, offset, 42);

还可以越过权限和构造函数进行直接的对象构造:

public void objTest() throws Exception{
    A a1=new A();
    System.out.println(a1.getB());
    A a2 = A.class.newInstance();
    System.out.println(a2.getB());
    A a3= (A) unsafe.allocateInstance(A.class);
    System.out.println(a3.getB());
}

CAS 操作

/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

CAS: Compare and Swap. 将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS 是一条 CPU 的原子指令(cmpxchg 指令)。

线程调度

可以直接将线程阻塞或恢复正常。注意是直接对线程进行操作。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);

比如LockSupport里面:

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

Class 操作

与对象操作类似,这时操作的是类,比如获取静态变量或修改静态变量。

//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);

还能直接创建一个类。

public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain);
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

使用defineAnonymousClass可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。

系统信息

//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();

SPI

语法糖

switch 支持 String 与枚举

switch 支持 String其实是通过hashcode和equals实现的,因为switch 实际上只支持int和char

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
        case "hello":
            System.out.println("hello");
            break;
        case "world":
            System.out.println("world");
            break;
        default:
            break;
        }
    }
}

解糖后:

public class switchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

数值字面量

数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

public class Test {
    public static void main(String... args) {
        int i = 10_000;
        System.out.println(i);
    }
}

集合

类继承关系:

Comparable和Comparator的区别

// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public  class Person implements Comparable<Person> {
    private String name;
    private int age;
    ...
    
    /**
     * T重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        if (this.age > o.getAge()) {
            return 1;
        }
        if (this.age < o.getAge()) {
            return -1;
        }
        return 0;
    }
}
Collections.sort(arrayList, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
});

HashMap 的长度是 2 的幂次方的原因

并发

线程和进程

一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。

在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。

严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()。

JUC

LongAdder

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

CAS存在的问题

ABA问题

增加时间戳,比如AtomicStampedReference里面就加了一个新field: stamp, 而且CAS操作不仅要对比共享的变量,还要检查stamp是否一致。

只操作一个共享变量

可使用AtomicReference,将多个变量封装在一起,对AtomicReference进行CAS

ThreadLocal

如何跨线程传递 ThreadLocal 的值?

TransmittableThreadLocal

线程池

拒绝策略

有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢?

任务持久化的思路

线程池在提交任务前,可以提前创建线程吗?

线程池中线程异常后,销毁还是复用?

使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。

如何设定线程池的大小?

Future

Future 接口

// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
    // 取消任务执行
    // 成功取消返回 true,否则返回 false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成
    boolean isDone();
    // 获取任务执行结果
    V get() throws InterruptedException, ExecutionException;
    // 指定时间内没有返回计算结果就抛出 TimeOutException 异常
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutExceptio
}

Semaphore 有什么用?

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。

回忆:哲学家问题。

IO

Stream

I/O模型

同步阻塞I/O

用户程序请求IO, 一直阻塞等待内核返回数据到用户空间

同步非阻塞I/O

用户程序请求IO, 不阻塞,但它会不断轮询内核是否将数据准备就绪,直到内核的数据准备完成,这个时候线程阻塞,内核拷贝数据到用户空间

I/O多路复用

核心:一个选择器负责多个client, 只需要一个线程便可以管理多个客户端连接。线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

前提是基于Linux内核的select和epoll系统调用才能实现。

Selector原理

一个Selector会注册多个Channel(文件,tcp/udp connection),Selector 会不断地轮询注册在其上的 Channel。如果某个channel有新连接/有数据可读或可写E,就通知相应的应用程序。

AIO

用户程序请求IO, 不阻塞,内核拷贝完数据之后才通知用户程序。

零拷贝

可以实现直接将数据从磁盘发到网络,无需经过cpu和内存。主要依赖于Linux的sendfile系统调用。

JVM

JIT vs AOT

JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。

AOT(Ahead of Time Compilation)在程序被执行前就将其编译成机器码,属于静态编译。可以提高 Java 程序的启动速度,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改)。AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等

GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。因此使用GraalVM能大幅提升启动速度并降低内存占用。

内存区域

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

数组

数组本质上是一个对象,所以是在堆上分配的。如果该数组又是局部变量,则线程私有栈上会保留一份它的引用。

从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

对象的结构

对象创建的过程

哪些对象可以作为 GC Roots 呢?

引用类型

垃圾回收算法

垃圾回收器

Serial 收集器

单线程。 新生代采用标记-复制算法,老年代采用标记-整理算法。

ParNew 收集器

Serial 收集器的多线程版本。

Parallel Scavenge 收集器

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old。

Parallel Scavenge 收集器与 ParNew 收集器的主要区别如下:

  1. 设计目标不同 Parallel Scavenge:以高吞吐量为核心目标,适合后台运算、批处理等对交互延迟不敏感的场景。通过最大化应用程序运行时间占比来优化性能。

ParNew:以低停顿时间为目标,通常与 CMS 收集器配合,用于需要快速响应的应用(如 Web 服务),减少垃圾回收对用户体验的影响。

  1. 搭配的老年代收集器 Parallel Scavenge:通常与 Parallel Old 收集器(多线程标记-整理算法)组合使用,形成吞吐量优先的全堆回收方案。

ParNew:主要与 CMS(Concurrent Mark Sweep) 收集器搭配,实现低停顿的老年代并发回收。

CMS

并发的标记清除算法。 主要优点:并发收集、低停顿。 但是它有下面三个明显的缺点:

Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。

G1

G1 不再严格划分新生代和老年代的物理边界,而是将堆划分为 多个等大小的 Region(默认 2048 个,每个 Region 1MB~32MB)。每个 Region 可动态充当以下角色:

这种设计使得内存分配和回收更加灵活,避免了全堆垃圾回收(Full GC)的长时间停顿。

要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。

新特性