自己整理的一些常见面试题和知识点

Day1:Java 集合

目标:

1
2
3
4
Collection
├── ArrayList
├── LinkedList
└── HashMap

1. 为什么 ArrayList 查询快,而 LinkedList 查询慢?

ArrayList 底层是连续内存空间,可以通过数组下标直接计算出元素地址,因此随机访问时间复杂度是 O(1)。LinkedList 底层是双向链表,每个节点只知道前后节点的位置,因此访问第 n 个元素需要从头或尾开始遍历,时间复杂度是 O(n)。

2. 为什么 LinkedList 插入快,但项目中却很少使用?

实际场景中基本都是查询和遍历,例如分页查询,获取数组中第几个元素什么的。链表虽然插入和删除快,但是也要先查找遍历到指定元素才可以。而且从中间插入的需求实际业务中很少遇到。我基本没有用过linkedList

2.1 那 LinkedList 真有比 ArrayList 快的时候吗?

有。
如果已经拿到了链表节点,例如 ListIterator 已经定位到了当前位置,或者频繁在队列头尾插入删除,那么 LinkedList 确实比 ArrayList 更合适。
但普通业务开发很少直接操作链表节点,所以实际项目中使用率很低。

3. ArrayList 为什么要自动扩容,而不是固定长度?

如果没有动态数组,就需要自己 System.arraycopy。
并且数组是 Java 语言的基础类型。
Java 希望:

数组:

简单
高效
固定长度

ArrayList:

自动扩容。

所以

数组:

负责性能。

ArrayList:

负责易用。

4. HashMap 为什么比遍历 List 查找快?

假设有100万人。现在找张三。
第一种:一个一个找O(n)
第二种:按姓氏放。张一个桶。李一个桶。王一个桶。

是不是快很多?

这就是:

Hash。

它本质就是:

通过计算规则,把数据快速定位到某个桶(Bucket)。

所以 HashMap 的第一层不是链表,而是:

1
Bucket[]

一个桶数组。

4.1 为什么 HashMap 不是按姓氏分组?

姓氏只是举例。
实际 HashMap 是调用 key 的 hashCode(),经过扰动函数计算,再通过 (n - 1) & hash 定位到桶,而不是按照某种业务规则分组。

5. 为什么 HashMap 不直接把冲突元素都放在链表里,而是后来又引入了红黑树?

链表查找只能一个一个遍历,太慢了。红黑树的查找效率为o(log2n)。
红黑树不是查找最快的树,但它在查找、插入、删除三者之间取得了最好的平衡,因此 Java 选择了红黑树。

6. ArrayList 为什么扩容 1.5 倍?

扩容太小会导致频繁复制数组,扩容太大会浪费内存,因此 JDK 采用 1.5 倍作为空间利用率和扩容成本之间的折中。

7. ArrayList 为什么线程不安全?

例如:

1
list.add(a)

两个线程:

同时:

add。

可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
size = 10

线程A

准备放10

线程B

准备放10



覆盖

size++


数据丢了

Day2:HashMap

目标:

1
2
3
4
5
HashMap
├── 一、为什么要有 hashCode()
├── 二、equals() 和 hashCode() 为什么必须一起重写
├── 三、ConcurrentHashMap 为什么线程安全
└── 四、Iterator 和 fail-fast

1. 为什么 HashMap 查找时不能只用 equals()?为什么还需要 hashCode()?

hashcode可以快速定位到hash表的某个桶,算法类似于取余(实际上是位运算)。定位到桶后再用equals确定具体的元素。
如果直接用equals,因为 equals 是 O(n),需要遍历整个集合。而 hashCode 可以通过哈希算法快速定位到某个 Bucket,把查找范围缩小到一个桶内,然后再调用 equals 比较,大部分情况下查找复杂度接近 O(1)。

2. 为什么重写了 equals(),就必须重写 hashCode()?如果不重写会发生什么?

因为hashmap是通过hashcode定位桶的,如果要保证hashmap正常工作,必须保证equals相同的话,hashcode也必须相同。

2.1 那 hashCode 相同,equals 一定相同吗?

不是。

例如:

1
2
"Aa".hashCode()
"BB".hashCode()

hash一样。但是equals为false。并且hash也有可能冲突。

3. HashMap 和 ConcurrentHashMap 最大的区别是什么?

HashMap 本身没有任何同步机制,多线程同时 put 或扩容可能导致数据覆盖、数据丢失等问题。ConcurrentHashMap 在 JDK8 中结合 CAS 和 synchronized 实现线程安全,并且采用更细粒度的同步策略,而不是对整个 Map 加锁,因此既保证了线程安全,又具有较好的并发性能。

4. 为什么 ConcurrentHashMap 的性能通常比给整个 HashMap 加一把大锁更高?

因为ConcurrentHashMap是分桶加锁的,操作A桶的时候,没有必要把B桶加锁。
锁粒度越小,并发越高。

5. 为什么增强 for 循环里直接 remove() 会抛 ConcurrentModificationException?

因为Iterator里面有一个expectedModCount,当进行遍历的时候,expectedModCount一直在和list的modCount进行比较。
遍历过程中集合结构发生变化,Iterator 无法保证遍历结果正确,因此直接失败。

6. 为什么使用 Iterator.remove() 就不会抛这个异常?

因为Iterator内部会进行维护expectedModCount。

7. HashMap容量都是16 32 64 128 (跟(n-1)&hash有关)

容量必须是 2 的幂,位运算才能均匀分布到每一个桶。

8. 为什么HashMap扩容都是2倍?

扩容迁移的时候很方便。只需要看扩容新增的那个高位是1还是0,0的话不用动,1的话移动到 原索引 + 旧容量的位置,不用重新计算hash

9. HashMap 为什么允许 null Key

HashMap规定了null.hashCode()=0。
ConcurrentHashMap不允许是因为会产生歧义,不知道是因为key不存在还是value为null。Hashmap也有这个问题,但是可以用containsKey。
ConcurrentHashMap value也不允许null,是为了降低实现的复杂度。ConcurrentHashMap 作为高并发容器,希望 API 尽量简单、一致,不提供 HashMap 中对 null 的特殊支持。

10. HashSet 为什么不重复

HashSet实际就是hashmap,value是固定的present(new Object())
HashSet 底层就是 HashMap,它利用 HashMap 的 Key 唯一性保证元素不重复。

Day3:并发

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
Java 并发

├── volatile
│ │
│ ├── Memory Barrier
│ ├── Happens-Before
│ └── 可见性

├── CAS

│ │
│ ├── CompareAndSet
│ ├── 自旋
│ ├── ABA
│ └── AtomicInteger

├── AQS
│ │
│ ├── state
│ ├── CLH等待队列
│ ├── park/unpark
│ └── CAS

└── JUC

├── ReentrantLock
├── Semaphore
├── CountDownLatch
├── FutureTask
└── ReadWriteLock

目标:

1
2
3
4
5
6
并发
├── 一、为什么需要线程?
├── 二、Thread、Runnable、Callable 有什么区别?
├── 三、Future 是什么?
├── 四、为什么不能直接 new Thread()?
└── 五、为什么要线程池?

1. 为什么 Java 已经有 Thread,还要设计 Runnable?

Runnable还可以做到线程与任务解耦。任务可以复用线程。

1.1 什么叫任务和线程解耦?

创建一个Thread的时候,就把任务和线程绑定在了一起。

2. 为什么 Runnable 之后又设计了 Callable?

Runnable 没有返回值,如果需要返回执行结果,只能借助共享变量或回调等方式实现,不够优雅。因此 Java 又提供了 Callable,支持返回值和受检异常。

3. Future 到底是什么?为什么需要它?

Future 表示一个异步任务未来的执行结果。提交任务后,主线程不需要立即等待,可以在需要的时候通过 get() 获取结果。CompletableFuture 是 Future 的增强版本,支持任务编排、回调和组合,因此我项目中主要使用 CompletableFuture。

4. 为什么不能大量使用 new Thread()?

线程是系统资源,不能无限获取,并且创建、销毁、线程上下文切换耗费性能。

4.1 为什么线程切换耗性能?

cpu执行任务的时候需要寄存器、计数器、栈等信息,当发生线程切换的时候,需要保存现场,恢复现场等操作,这些都是耗时的。

5. 线程池最大的作用是什么?除了复用线程,还有什么作用?

  1. 最大的作用就是复用线程,防止创建线程的开销。还有一个主要目的就是控制线程数量。防止无限创建线程导致系统崩溃。可以统一管理,监控线程。比如线程名字,关闭线程等。

6. 结合你的 GIS 平台,说说为什么你选择 Spring Async + 自定义线程池,而不是直接在 Controller 里执行分析

上传下载shp/gdb,分析叠加等是一个耗时的操作,如果不用线程池,提交任务后一直等待,首先是用户操作层面就不合理。然后还会一直占用tomcat的线程,可能会导致其他请求阻塞。放到后台异步执行任务的话,不影响前台操作。

Day4:并发-线程池

目标:

1
2
3
4
5
6
并发
├── 一、为什么需要线程?
├── 二、Thread、Runnable、Callable 有什么区别?
├── 三、Future 是什么?
├── 四、为什么不能直接 new Thread()?
└── 五、为什么要线程池?

1. 为什么线程池不是核心满直接创建新线程,而是核心满先放队列?

线程属于昂贵资源,创建和销毁成本较高。所以java优先复用线程,先排队。并且任务很可能很快就执行完了。当队列也满了说明任务积压严重,需要提高线程数量。

线程池优先使用阻塞队列,是因为线程属于昂贵资源,创建和销毁成本较高。如果核心线程都忙碌,先让任务进入队列等待,很多情况下线程很快就会空闲并继续处理队列中的任务,这样可以减少线程数量,提高资源利用率。只有队列也满了,说明任务积压严重,才会继续创建非核心线程提高吞吐量

2. execute()和submit()最大的区别是什么?除了:Future。还有:什么?

submit 更关注任务生命周期。execute就是执行任务,不管任务是否成功失败。submit可以获取结果,停止任务,捕获异常。

3. 假设GIS线程池core=4,max=8,queue=100。突然来了500个空间分析。会发生什么?如果你是架构师。你怎么优化?

  1. 限制用户提交任务数量
  2. 增加mq削峰,慢慢消费
  3. 分布式部署

Day5:JUC

目标:

1
2
3
4
5
6
JUC(java.util.concurrent)
synchronized
volatile
CAS
AQS
ReentrantLock
AQS实现类 state含义
ReentrantLock 锁重入次数
Semaphore 剩余许可证数量
CountDownLatch 剩余计数
ReentrantReadWriteLock 高16位读锁,低16位写锁(这个了解即可)
1
2
3
4
5
并发安全

├── 为什么会有线程安全问题?
├── synchronized 为什么出现?
└── synchronized 到底锁的是什么?

1. 为什么会出现线程安全问题?本质是什么?

多个线程同时竞争同一个资源。假如两个线程同时对同一个变量a=100进行操作,线程1对a-10,线程b对a-20。结果可能并不是70。因为线程会读取变量a到自己的线程工作内存里,再写回主线程,读到的可能是100,也可能是另一个线程操作后的结果,导致每个线程看到的共享变量可能不是最新值。

2. 为什么 Java 的锁是锁对象,而不是锁代码?

因为多线程操作的是资源,资源需要加锁。对象是资源,代码不是资源。

3. 为什么synchronized(new Object())几乎等于没加锁?

因为每次都是new出来的新对象,每次的锁都不是同一把锁。

4. 分布式系统为什么用分布式锁,不用jvm提供的锁?

synchronized 本质是 JVM 内部锁,它只能保证同一个 JVM 内多个线程互斥。如果系统部署到多台服务器,每个 JVM 都有自己的锁对象,它们彼此独立,因此无法实现跨 JVM 的互斥。而 Redisson 基于 Redis 实现分布式锁,所有节点竞争的是 Redis 中同一个 Key,因此能够保证整个集群范围内的互斥。

分布式系统不在一个jvm下面,获取到的锁对象肯定也不是同一个。根本锁不住。分布式锁可以达到不同的系统使用的同一把锁,例如redisson分布式锁。

5. 为什么synchronized可以重入(Reentrant)?

当发现是同一个线程获取锁的时候就放行了。Monitor 里面维护了 Owner 和 Count

当发现再次进入synchronized(lock){的时候发现Owner == 当前线程,就不阻塞,而是Count++。当退出同步代码块时进行Count–。当Count为0时释放锁。

1
2
3
Java 的锁(Monitor)里面会记录两个东西:
Owner(当前持有锁的线程)
Count(重入次数)

6. 为什么 Java 要设计可重入?

1
2
3
4
5
6
public synchronized void A(){
B();
}

public synchronized void B(){
}

如果不可重入,执行A的时候获取到锁,然后执行B的时候也需要同一把锁,然后死锁了。

6. 为什么volatile不能保证i++;线程安全?

i++;的执行步骤:

1
2
3
4
5
① 读取 i

② i + 1

③ 写回 i

假设线程1读取i为100,进行+1;线程2也读取100,进行+1。结果是101,少了一次。
volatile只能保证线程每次都是从主内存读取的,但是问题不是读错,而是同时写回会覆盖。

volatile 保证的是每次读取都能看到最新值,但 i++ 并不是一个原子操作,它实际上包含”读取、计算、写回”三个步骤。即使每一步都能看到最新值,多个线程仍然可能在读取同一个值后分别计算并覆盖彼此的结果,因此 volatile 无法保证 i++ 的线程安全。

volatile保证看到最新的值。不是保证整个修改过程不能被别人打断。

7. 既然 synchronized 能保证可见性,为什么还需要 volatile?

因为成本问题,synchronized太重。
假设

1
2
3
4
5
6
7
8
9
boolean running = true;

线程A:
running = false;

线程B:
while(running){
...
}

只需要保证线程B能看到最新running。volatile就够了,根本不用加锁
只有可见性,没有互斥需求。

Day6:CAS

目标:

1
2
3
4
5
6
7
CAS

├── 为什么需要 CAS?
├── CAS 到底是什么?
├── ABA 问题
├── AtomicInteger 为什么线程安全?
└── 为什么 CAS 比 synchronized 快?

volatile

volatile 写 (volatile 写 = 发布(Publish))

1
2
3
4
5
6
7
8
9
10
11
12
13
线程A

普通写

普通写

volatile

Memory Barrier 保证不能重排序

CPU 按规则发布写入

CPU 缓存一致性协议使其它 CPU Cache Line 失效

volatile 读 (volatile 读 = 同步(Synchronize))

1
2
3
4
5
6
7
8
9
线程B

volatile

建立 Happens-Before

volatile 写之前的所有操作

对当前线程可见

1. CAS为什么必须配合volatile?

因为比较的值必须是最新的值。
volatile写会在volatile写操作周围增加memory barrier,然后cpu执行barrier,cpu缓存一致性协议,让其他cpu的Cache line失效。
volatile读可以利用 ‘volatile写 happens-before volatile读’让其他线程得到最新的值。

2. CAS为什么叫乐观锁?

CAS 认为大部分情况下不会发生竞争,所以不会先加锁,而是先尝试修改。如果修改失败,再重新读取最新值继续尝试,因此称为乐观锁。

3. 为什么AtomicInteger内部不是if(){}而是while(true)

getAndIncrement是通过CAS改变的值,cas如果修改失败需要不断的重试,所以需要while。

4. ABA到底有什么危害?为什么版本号能解决?

加入一个变量x,一开始是A。然后线程1要使用cas把x变为B,比较的时候如果x是旧值A就改为B。但是在线程1修改之前,已经有其他线程修改了x,把x变为了B又变回了A。这时候如果不加版本号线程1就不知道x已经被修改过了,并不是他预期的A。

5. 为什么CAS没有线程切换却也可能CPU100%。为什么?

因为CAS不停的自旋读取、比较。而synchronized获取锁失败直接阻塞挂起,cpu反而空闲。CAS是拿 CPU 换线程切换。

6. 假设1000线程全部AtomicInteger.incrementAndGet();CAS是不是一定比synchronized快?为什么?

不一定,如果cas一直发生冲突,那么重试的次数太多,会导致cpu把算力浪费在比较上。

CAS 适合低竞争场景;锁适合高竞争场景

7. CAS 最大的缺点是什么?

CAS 只能保证一个共享变量的原子更新。

8. 超卖问题

假设

1
AtomicInteger stock = new AtomicInteger(10)

卖票:

1
2
3
if(stock.get()>0){
stock.decrementAndGet();
}

请问有没有线程安全问题?
还是会有安全问题,虽然stock.get()是volatile的,每次if读取的时候也都是最新值。但是有可能,线程1读取为1,然后cas成功减为了0。同时线程2也读取为1,然后去执行减没有成功,然后重新读取为0,然后再减1,最终的值可能会比0小。

Day 7:AQS

关键流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Thread

CAS(state)

成功

获得锁
===================
失败

进入AQS队列

park()

unlock()

unpark()

再次CAS(state)

成功

获得锁

1. AQS为什么一定要维护一个volatile int state; 为什么不是boolean?

因为AQS是一个抽象类,他还有很多实现类。各个实现类不只是用两个状态就可以解决问题。

2. 为什么AQS不用CAS一直while(true)而是park()?

因为一直while会浪费cpu,在竞争大的时候,很多情况都会失败。而park()会使线程休眠,释放cpu,当锁释放出来的时候再unpark进行一次cas。

3. ReentrantLock为什么叫Reentrant?它怎么做到可重入?

它会判断是否是当前线程持有锁,当进入加锁的同步代码时,如果是当前线程持有锁,他就会对state进行+1,当退出同步代码时,state会-1。

4. 为什么ReentrantLock功能比synchronized多?

因为ReentrantLock是利用AQS使用Java代码实现的。而synchronized的功能已经写死在了jvm。

5. 假设你的GIS平台。服务器16核。你希望最多只有4个空间分析同时运行。其它任务等待。请问你用AtomicInteger还是Semaphore还是ReentrantLock?

选择Semaphore。因为4个空间分析是4个线程。用ReentrantLock肯定只有一个线程获得锁。AtomicInteger只能保证一个元素是原子的,而启动任务肯定是多个操作,实现起来太复杂。
Semaphore使用起来更方便,启动一个任务就Semaphore-1

6. ReentrantLock: 为什么lock.unlock()必须放finally?

1
2
3
4
5
6
7
lock.lock();

try{
...
}finally{
lock.unlock();
}

不管执行是否出现异常,都必须保证锁最终要释放。否则会造成死锁。

ReentrantLock 不像 synchronized 会自动释放锁,它必须由程序员手动调用 unlock()。如果中间发生异常而没有执行 unlock(),state 永远不会减到 0,其它线程将一直 park 在 AQS 队列中,导致死锁。

7. ReentrantLock: 为什么下面代码会死锁?

1
2
3
lock.lock();
lock.lock();
lock.unlock();

虽然是同一线程可重入,但是这个线程每次加锁的时候都会state++,而它加锁了两次最后只减了一次,所以锁没有释放。

8. 为什么ReentrantLock公平锁比非公平锁慢?

公平锁严格先进先出,即使刚来的线程刚好碰到state为0,也要必须排队。
非公平锁可以插队,刚来的线程碰到state为0可以直接拿锁,少一次拍断排队唤醒。

Day 8:JVM

目标:

1
2
3
4
5
6
JVM
├── JVM 内存结构(★★★★★)
├── 对象创建过程(★★★★★)
├── 垃圾回收为什么需要 GC Root(★★★★★)
├── 常见垃圾回收器(CMS/G1)
└── 类加载机制(先了解)

GC Root有哪些?

  1. 虚拟机栈中局部变量引用的对象。
  2. 方法区(Metaspace)中静态变量引用的对象。
  3. 方法区中常量引用的对象。
  4. JNI(本地方法)引用的对象。

GC Root 一定在栈上吗?

不一定。
GC Root 的本质是 JVM 认为一定存活、作为可达性分析起点的对象引用。栈中的局部变量只是最常见的一类,静态变量、JNI 引用、活动线程、Class 对象、Monitor 等都可以成为 GC Root。

1. 为什么Java对象放Heap。引用放Stack?

对象放在堆中主要有两个原因。
第一,对象通常比较大,而栈空间比较小,栈主要用于方法调用,如果整个对象都放栈中,每次方法调用和返回都会产生大量数据复制,效率很低。
第二,对象的生命周期通常长于方法,例如对象可以作为返回值返回给调用者,如果放在栈中,方法结束对象就没了,所以对象必须放在堆中,而栈中只保存对象引用。

2. 为什么Java不用引用计数。而采用GC Root?

引用计数最大的缺陷是无法处理循环引用,例如 A 引用 B,B 引用 A,即使整个对象图已经不可达,引用计数仍然大于 0。而 GC Root 可达性分析是从根对象开始遍历,只要从任何 GC Root 都无法到达,就会被回收,因此不会受到循环引用影响。

3. 下面代码方法结束。什么时候User真正释放?立即?还是GC?为什么?

1
2
3
public void test(){
User user = new User();
}

方法结束后,局部变量 user 随栈帧销毁,不再是 GC Root。如果堆中的 User 对象已经不可达,那么它就成为垃圾对象。但垃圾对象不会立即释放,而是在下一次垃圾回收时被 GC 回收,具体时间由 JVM 决定。

4. 下面代码是不是立即回收?为什么?

1
2
User user = new User();
user = null;

user = null 只是断开了一个引用,并不会立即触发 GC。对象只是变成了”可能被回收”。真正什么时候回收,由 JVM 根据垃圾回收策略决定。

5. 你的GIS平台。分析一个3GBShape。分析结束为什么Java不会立刻释放3GB内存?为什么有时候你明明已经task = null;JVM内存还是3GB?

方法结束user引用随着栈帧一起被销毁,但是对象还在heap里,如果它真的不可达的话,会在下一次gc回收。但即使gc回收了,内存也不一定立刻把内存还给操作系统

6. 综合题

下面代码

1
2
3
4
5
6
7
8
public void test() {
User user = new User();
user = null;

System.gc();

System.out.println("结束");
}

请问:

1. System.gc() 会不会一定 GC?

System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。

这里要记住一句经典的话:
System.gc() ≠ 立即 GC。

2. User 对象会不会一定被回收?

如果 User 对象不可达,它就成为了垃圾对象(Eligible for GC),但什么时候真正回收,仍然由 JVM 决定。

3. System.gc() 是不是一定会 Full GC?

System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。

Day9:JVM

目标:

1
2
3
4
1. 对象创建过程
2. Minor GC / Major GC / Full GC
3. G1 垃圾回收器
4. JVM 常见参数

对象创建过程

第一步:检查类是否已经加载
第二步:在 Heap 中申请内存
第三步:对象初始化
第四步:执行构造函数
第五步:栈中保存引用

类加载过程

加载(把.class读进jvm)
链接(准备、准备内存、静态变量赋默认值)
初始化(执行静态代码块,静态变量初始化)

JDK9之后heap改变了

G1 不再固定划分新生代和老年代,而是将堆划分为多个 Region,每次优先回收回收收益最高(Garbage First)的 Region,从而更好地控制停顿时间,因此成为 JDK9 以后的默认垃圾回收器。

1. 为什么对象要先赋默认值,再执行构造函数?

JVM 会先把对象初始化为默认状态,保证所有成员变量都有合法初始值,然后再执行构造函数完成业务初始化。

2. 为什么下面代码不会报错?为什么输出的是0?

1
2
3
4
5
6
7
class User{
int age;
}

User user = new User();

System.out.println(user.age);

不会报错,因为创建对象的时候会先对成员变量进行赋默认值再调用构造器。
局部变量不会赋默认值。

3. 为什么 JVM 要把 Heap 分成 Young 和 Old?

JVM 发现绝大多数对象生命周期都很短,因此采用分代思想,让不同生命周期的对象采用不同的回收策略。

4. 为什么 Minor GC 比 Full GC 快很多?

Young 区的大部分对象都是垃圾对象,存活率低,所以复制成本很低。
新生代(Young)最经典的是复制算法(Copying)。不是把垃圾对象删除,而是把活对象复制到另一块 Survivor 区。然后清空整个 Eden。

Full GC使用的标记整理(Mark-Compact)法。

5. 为什么 G1 比 CMS 更适合大内存服务器?

因为G1把heap分成了很多个区域,每次发生gc的时候会挑选垃圾对象较多的区域进行回收。可以更好的控制停顿时间。如果每次gc的时候都清理一整个大heap,停顿时间会很长。

6. 为什么很多线上服务建议 “-Xms = -Xmx”

因为jvm申请内存需要stop the world。

7. 为什么 Java 局部变量没有默认值,而成员变量有?

java创建对象的时候会先赋默认值,再调用构造器。
局部变量需要手动赋默认值,否则容易掩盖开发bug。

8. 为什么 Java 要把heap分代,而不是整个 Heap 全部统一GC?

为了更好的控制gc停顿。jvm发现大部分对象都是存活时间很短的对象,所以对不同生命周期的对象采用不同的回收策略。Minor GC复制成本低,所以快。

JVM 发现大部分对象都是朝生夕死的,因此采用分代思想。新生代对象存活率低,采用复制算法,只复制少量存活对象,回收效率很高;老年代对象存活率高,采用标记整理等算法,避免大量对象复制。因此分代不仅减少了 GC 停顿时间,也让不同生命周期的对象使用最适合的垃圾回收算法。

Day10:JVM

目标:

1
2
3
4
1. GC 算法(★★★★★)
2. G1 回收器(★★★★★)
3. 类加载机制(★★★★★)
4. JVM 参数(★★★★★)

GC 算法

  1. 标记清除(Mark-Sweep)
    产生内存碎片
  2. 复制算法(Copying)
    只能针对young区,存活对象少的合适。因为复制成本低。把存活对象存到survive区,eden区直接清空。
  3. 标记整理(Mark-Compact)
    old区使用的,会把存活对象进行整理,减少内存碎片。但是效率低。

双亲委派

保证了 Java 核心类不会被用户自定义类替换,保证了类加载的安全性和唯一性。

JVM 参数

1
2
3
4
5
6
7
8
9
10
11
-Xms
初始Heap
---------
-Xmx
最大Heap
---------
-Xss
每个线程栈大小
---------
-XX:+UseG1GC
使用G1

为什么标记清除容易OOM?

不是Heap不够。而是连续空间不足。

1.为什么 “static int a = 100;” 不是直接100?为什么先0再100?

因为jvm加载class的时候会先对static变量进行赋默认值,然后再执行静态代码块和初始化静态变量。

2. 为什么标记清除会产生内存碎片?

标记然后删除会产生空洞,这些空洞就是内存碎片。他们不是连续的内存空间,容易发生oom。
容易因为无法找到足够大的连续空间而导致内存分配失败。

3. 为什么Young用复制算法Old用标记整理?

因为young区的对存活率低,复制的对象少,复制成本低,所以使用复制算法。
old区的对象存活率高,使用复制的话每次都要复制很大的数据,效率低。而标记清除法容易产生内存碎片。标记整理也是删除,但是会移动整理,减少内存碎片。
JVM 根据对象不同的存活率,选择不同的垃圾回收算法。

4. 为什么G1比CMS停顿更容易控制?

因为G1把heap分成了很多个区,每次GC的时候会挑选垃圾比例高(Garbage First)的 Region进行回收。停顿时间更可控。

5. 为什么Java需要双亲委派?

防止开发者自己写了类似java.lang.String的类,导致整个Java错乱。
保证一个类在 JVM 中只有一个 Class 对象。

6. 为什么双亲委派可以防止重复加载?

因为ClassLoader加载类的时候会先询问自己上级有没有这个类,并且逐级向上,如果都没有才加载自己的。
已经加载过的类,子加载器不会重复加载,而是复用父加载器已经加载的 Class 对象。

7. String s = “abc”;和 String s = new String(“abc”);有什么区别?

String s = “abc” 会先检查字符串常量池,如果 “abc” 已经存在,则直接让引用指向常量池中的对象,不会重复创建。

new String(“abc”) 会先保证字符串常量池中存在 “abc”,然后再在堆中创建一个新的 String 对象,因此至少会创建一个堆对象,而字符串常量池中的 “abc” 可能是新建的,也可能是复用已有的。

创建流程:

1
2
3
4
5
6
7
8
9
new String("abc")

字符串常量池有没有 "abc"

没有就创建

Heap 创建一个新的 String 对象

Heap 中的 String 对象内部引用 Pool 中的 "abc"

8. String s = new String(“abc”);创建了几个对象?

情况一:字符串常量池已经有 “abc”,就只在heap创建 String 对象。一共创建了一个对象。
情况而:字符串常量池没有 “abc”,先在字符串常量池创建“abc”,然后在heap里创建 String 对象。一共创建了两个对象。

Day11:Spring

目标

1
2
3
4
5
Spring
├── 为什么要有 Spring(★★★★★)
├── IOC(★★★★★)
├── Bean 生命周期(★★★★★)
├── AOP(★★★★★)

Bean 生命周期

1
2
3
4
5
1. 实例化(调用构造方法)
2. 依赖注入(@Autowired
3. 初始化(@PostConstruct、InitializingBean)
4. 使用
5. 销毁(@PreDestroy

AOP

AOP(面向切面编程)就是把日志、事务、权限等横切关注点从业务代码中抽离出来,统一增强目标方法。

1. 为什么Spring要IOC为什么比new好?

第一,降低耦合。对象不再依赖具体实现,而是依赖 Spring 容器管理。

第二,统一管理生命周期。Spring 可以统一管理 Bean 的创建、销毁、作用域(Singleton、Prototype 等)。

第三,方便扩展。例如以后 UserDao 换实现,或者增加代理、事务,都不用修改业务代码。

2. Bean生命周期五步是什么?

创建bean、注入依赖、初始化、使用、销毁

3. 为什么@PostConstruct里面@Autowired已经可以使用?

PostConstruct是在初始化之前,注入依赖之后。所以已经可以使用依赖的对象了。

构造方法里面不能保证@Autowired已经完成但是@PostConstruct一定可以。

4. 为什么AOP适合日志事务权限。为什么不适合业务?

aop适合通用的代码逻辑。具体的业务情况谁和谁都不一样。

5. 你的GIS平台如果不用AOP日志怎么写?用了AOP有什么好处?

如果不用aop就只能每个方法都手写一遍记录日志的代码。使用了aop后只需要写一次。

6. 为什么Spring默认Bean是Singleton为什么不是Prototype?

绝大部分 Bean 都是无状态的,比如service、dao,多个线程共享一个实例没有问题。
创建对象、GC会消耗性能,对象越多越占内存。
只有少数需要保存状态的对象才适合 Prototype。比如dto、entity。

7. 为什么SpringIOC底层大量使用反射为什么不用new UserService()

因为springIOC是在运行时创建的bean,Spring 在编译时根本不知道有哪些 Bean。
用new的话,要求编译时就知道类型。

8. 为什么@Autowired默认按类型(Type)注入而不是按名字(Name)?

原因一:面向接口编程
@Autowired可以注入接口或者抽象类。如果实现类只有一个,spring会把这个实现类注入进来。如果需要按名字注入可以用@Resource或者@Qualifier

原因二:名字容易变

@Autowired 默认按类型注入,是为了支持面向接口编程,降低实现类变更带来的影响。

9. 为什么Spring有时候可以解决循环依赖有时候又不能?

1
2
3
4
5
6
7
8
9
10
11
@Service
class AService {
@Autowired
BService b;
}

@Service
class BService {
@Autowired
AService a;
}

流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
创建A

A先new出来

A放进缓存(虽然还没初始化)

A需要B

开始创建B

B需要A

缓存里发现A已经new出来了

直接拿出来注入

B完成

继续完成A

Spring 能解决的是 Singleton Bean 的属性注入循环依赖。

那什么时候解决不了?

第一种:构造器注入
对象都创建不了

第二种:Prototype
每次都是new,没有三级缓存

Spring 能解决 Singleton Bean 的属性注入循环依赖,因为 Bean 实例化后会提前暴露到三级缓存,后续 Bean 可以先引用这个未完全初始化的对象;而构造器注入和 Prototype Bean 不支持这种机制,因此无法解决循环依赖。

Day12:事务

ACID

Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)

事务保证一组操作具有 ACID 特性,要么全部成功,要么全部失败。

1. 为什么Spring事务使用AOP为什么不用每个方法自己写commit?

事务属于横切逻辑。Spring利用aop已经完成了事务开启和事务提交回滚的通用代码。

事务属于横切关注点,每个业务方法都可能需要事务,因此 Spring 使用 AOP 将事务逻辑统一织入目标方法,避免每个方法重复编写 begin、commit、rollback 等代码。

2. 为什么自己调用this.save()事务失效?

因为 Spring 的事务是通过代理对象实现的。外部调用的是代理对象,所以事务能够生效;而 this.save() 是对象内部调用,没有经过代理对象,而是直接调用目标对象的方法,因此事务拦截器不会执行。

3. 为什么private 方法@Transactional失效?

JDK 动态代理是代理接口,接口里没有private方法。CGLIB代理的是子类,子类不能重写private方法。

private 方法不能被代理对象重写,因此事务增强无法织入。

4. REQUIRED和REQUIRES_NEW区别什么时候用REQUIRES_NEW?

REQUIRED是如果有事务就加入事务。没有就创建一个事务。
REQUIRES_NEW 会挂起当前事务,然后创建一个新事物。
当需要两个不同事物的情况下使用REQUIRES_NEW,比如业务代码和保存日志需要是两个事务,业务代码异常回滚,日志应该要保存下来。

流程:

1
2
3
4
5
6
7
8
9
A事务

暂停

B事务

提交

恢复A事务

5. 为什么Spring 默认只回滚 RuntimeException?为什么throw new IOException();事务默认不回滚?

非RuntimeException需要手动处理,spring就认为开发者已经知道这个异常,应该主动处理。RuntimeException可能是程序出现bug应该回滚。

Spring 默认认为 Checked Exception(受检异常)是开发者可以预期并处理的业务异常,因此默认不回滚;而 RuntimeException 通常表示程序发生了不可预期的错误,因此默认回滚事务。

6. 为什么下面代码事务最后提交了?

1
2
3
4
5
6
7
@Transactional
public void test(){
try{
throw new RuntimeException();
}catch(Exception e){
}
}

因为方法最终没有抛出RuntimeException。

Day13:Spring Boot

1. Spring Boot 和 Spring 有什么区别?

springboot基于spring,由于spring需要大量繁琐的配置,springboot简化了这一流程。他的约定大于配置。会根据依赖自动加载需要的bean。

Spring Boot 是 Spring 的快速开发框架。

2. @SpringBootApplication 为什么只要一个注解?它里面最重要的三个注解是什么?各自负责什么?

@SpringbootConfigurarion
标注配置启动类

@EnableAutoConfiguration
开启自动配置,根据依赖自动配置。

@ComponentScan
开启注解扫描。例如@Configuration @Componet @Service

3. 为什么导入:spring-boot-starter-data-redis RedisTemplate 就自动有了?

因为这个依赖里面有自动装配RedisTemplate的配置类。

4. 为什么Spring Boot不会把所有AutoConfiguration全部加载?

他会根据条件,比如@ConditionalOnClass、@ConditionalOnMissingBean等。

Spring Boot 会根据各种条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等)决定是否加载自动配置,而不是无条件全部加载。

5. 为什么spring-boot-starter-web叫Starter为什么不是spring-web?

Starter就是依赖管理。自动引入需要的全部依赖,本质就是依赖聚合。