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 节省一半的内存空间
异常
类继承结构:
- Throwable
- Exception: 可以catch
- Checked Exception
- FileNotFoundException
- RuntimeException
- NullPointerException
- IllegalArgumentException
- NumberFormatException
- ArrayIndexOutOfBoundsException
- ClassCastException
- ArithmeticException
- SecurityException
- Checked Exception
- Error: 可以catch,但是不建议,应由JVM处理
- OutOfMemoryError
- StackOverflowError
- Exception: 可以catch
不要在 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?
如果没有手动指定 serialVersionUID,Java 会根据类的结构(字段、方法、类名等)自动生成一个。将有可能导致反序列化失败。
自动生成的 serialVersionUID 依赖于类的细节(如字段名、方法名等),而这些细节可能会因为不同的 JVM 实现或编译器版本而有所不同。手动指定 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);
代理
代理类和实现类都要实现同一个接口,一般用于实现类的代码无法修改的情况。
动态代理
- JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
- CGLIB则可以代理没有实现接口的类。
- Spring AOP,如果该类实现了接口,则使用JDK动态代理,否则使用CGLIB动态代理
- CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- JDK动态代理的性能一般更好
BigDecimal
RoudingMode
- ROUND_UP: 入位,远离0,不管舍去的数字是多少,都入位
- ROUND_DOWN: 舍去,趋向0
- ROUND_HALF_UP: 四舍五入
- ROUND_HALF_DOWN: 五舍六入
- ROUND_HALF_EVEN: 银行家舍入法,四舍六入,如果丢弃数字的前一位是奇数,则入(ROUND_HALF_UP),如果是偶数,则舍(ROUND_HALF_DOWN)
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);
操作的是堆外内存,需要手动释放。
为什么要使用堆外内存?
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。提升程序 I/O 操作的性能。
- 通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
内存屏障
类似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);
}
}
集合
类继承关系:
- Collection
- Set
- HashSet
- LinkedHashSet
- TreeSet
- SortedSet
- HashSet
- List
- ArrayList
- LinkedList
- Vector
- Stack
- Queue
- PriorityQueue
- Deque
- ArrayDeque
- Set
- Map
- HashMap
- LinkedHashMap
- HashTable
- SortedMap
- TreeMap
- HashMap
Comparable和Comparator的区别
- Comparable用于集合中对象进行实现,接着实现compareTo方法。
// 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;
}
}
- Comparator是用于Collections.sort方法里面,可用lambda表达式,传进2个对象,再实现里面的逻辑进行比较
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
HashMap 的长度是 2 的幂次方的原因
- 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,hash % length 等价于 hash & (length - 1)。
- 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
- 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。
并发
线程和进程
一句话概括 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
线程池
拒绝策略
- AbortPolicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢?
任务持久化的思路
- 实现RejectedExecutionHandler接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。
- 继承BlockingQueue实现一个混合式阻塞队列,该队列包含 JDK 自带的ArrayBlockingQueue。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写take()方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue中去取任务。
线程池在提交任务前,可以提前创建线程吗?
- prestartCoreThread():启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true;
- prestartAllCoreThreads():启动所有的核心线程,并返回启动成功的核心线程数。
线程池中线程异常后,销毁还是复用?
使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。
如何设定线程池的大小?
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
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
InputStream
- FileInputStream
- BufferedInputStream
- DataInputStream: 可读具体类型:int, boolean等
- ObjectInputStream: 可读具体某个对象
OutputStream
- FileOutputStream
- BufferedOutputStream
- DataOutputStream
- ObjectOutputStream
- PrintStream
Reader
- InputStreamReader
- FileReader
- BufferedReader
- InputStreamReader
Writer
- OutputStreamWriter
- FileWriter
- PrintWriter
- OutputStreamWriter
RandomAccessFile
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 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
对象的结构
- 对象头:
- 标记字段
- 类型指针
- 对象数据
- Padding
对象创建的过程
- 类加载检查:类是否在常量池中有符号引用,类是否已经加载,如未加载则需要加载
- 分配内存:在堆上分配。并发情况下,采用CAS来解决
- 指针碰撞:适用于内存规整无碎片的场景。内存区域分为已分配区域和未分配区域,直接往未分配区域分配
- 空闲列表:适用于有内存碎片的场景。从维护的空闲列表中选取一个或多个进行分配
- 初始化零值:往对象数据区域填充默认零值
- 设置对象头:标志位,类型指针,锁标志
- 初始化:若给定了初始值,这个时候给该对象赋值
哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象JNI(Java Native Interface)引用的对象
引用类型
- 强引用:GC不会回收
- 软引用: GC仅在内存不足时回收
- 弱引用: GC发现了就会回收,但是因为GC优先级低,不一定会发现它
- 虚引用: 任何时候都会被回收。----还是没懂
垃圾回收算法
- 标记清除:标记内存块,清除该内存块
- 存在问题:产生内存碎片
- 标记复制: 分成2个大内存块A和B,A中内存块标记完后,将存活的放到另外一个内存块B中,一次性清除A
- 存在问题: 可用内存减少一半。老年代存活对象多,复杂的效率低
- 标记整理:标记完后,将需要清除的内存块移到后面,直接对非存活的部分(以及可用内存)一次清除。
- 存在问题:整理的效率低,适合老年代,因为老年代的gc频率不高
垃圾回收器
Serial 收集器
单线程。 新生代采用标记-复制算法,老年代采用标记-整理算法。
ParNew 收集器
Serial 收集器的多线程版本。
Parallel Scavenge 收集器
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old。
Parallel Scavenge 收集器与 ParNew 收集器的主要区别如下:
- 设计目标不同 Parallel Scavenge:以高吞吐量为核心目标,适合后台运算、批处理等对交互延迟不敏感的场景。通过最大化应用程序运行时间占比来优化性能。
ParNew:以低停顿时间为目标,通常与 CMS 收集器配合,用于需要快速响应的应用(如 Web 服务),减少垃圾回收对用户体验的影响。
- 搭配的老年代收集器 Parallel Scavenge:通常与 Parallel Old 收集器(多线程标记-整理算法)组合使用,形成吞吐量优先的全堆回收方案。
ParNew:主要与 CMS(Concurrent Mark Sweep) 收集器搭配,实现低停顿的老年代并发回收。
CMS
并发的标记清除算法。 主要优点:并发收集、低停顿。 但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。CMS 垃圾回收器在
Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。
G1
G1 不再严格划分新生代和老年代的物理边界,而是将堆划分为 多个等大小的 Region(默认 2048 个,每个 Region 1MB~32MB)。每个 Region 可动态充当以下角色:
- Eden Region(新生代)
- Survivor Region(存活区)
- Old Region(老年代)
- Humongous Region(存储大对象,超过 Region 50% 容量)
- 空闲 Region
这种设计使得内存分配和回收更加灵活,避免了全堆垃圾回收(Full GC)的长时间停顿。
要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。