Java八股

Java

基础

集合

ArrayList

动态数组

实现接口
  • List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  • RandomAccess:这是一个标志接口,表明实现这个接口的 List 集合是支持 快速随机访问 的。在 ArrayList 中,我们就可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • Cloneable:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
  • Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
主要成员变量
  • Object[] elementData:存放元素
  • int size:所包含元素个数
  • static final int DEFAULT_CAPACITY:默认初始容量
  • static final Object[] EMPTY_ELEMENTDATA:用于空实例的共享空数组实例
  • static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
扩容机制

第一次扩容容量为Math.max(10, 插入的元素数量),之后扩容都是之前容量的1.5倍

java8

1
2
3
4
5
6
7
add()

ensureCapacityInternal() // 计算添加元素后需要的容量

ensureExplicitCapacity() // 容量检查,判断是否需要扩容

grow() // 实际扩容

java11

1
2
3
add()						// 容量检查

grow() // 实际扩容
迭代器

当在使用迭代器时有其它线程修改数组时有两种策略

  1. fail-fast:在使用迭代器的线程立即报错
  2. fail-safe:发现有人修改了则采取一些对策,例如牺牲一些一致性来让整个遍历完成

原理

​ fail-fast是在创建迭代器时记录数组被修改的次数,每次获取下一个元素时判断现在数组的修改次数是否和之前记录的保持一致,如果不一致则报错(类似乐观锁)。

ArrayList使用的第一种策略,CopyOnWriteArrayList是第二种策略(对策是添加把原集合复制出来,在新集合上进行修改,修改好后指向新集合。创建迭代器时会指向当前集合,遍历的是旧集合不影响(读写分离));

数组和List之间的转换

数组转List(浅拷贝)

1
List<String> list = Arrays.asList(new String[]{"a", "b"});

List转数组(深拷贝)

1
2
3
4
5
6
List<String> list = new ArrayList<>();

list.add("a");
list.add("b");

String[] strs = list.toArray(new String[list.size()]);

LinkedList

与ArrayList的比较
  • 底层数据结构

    ArrayList基于Object数组,需要连续内存

    LinkedList基于双向链表,无需连续内存

  • 效率

    ArrayList的随机访问、末尾插入/删除为O(1),指定位置插入/删除为O(n)

    LinkedList的头尾插入/删除为O(1),指定位置插入/删除为O(n)

  • 占用空间

    ArrayList需要预留一定的容量

    LinkedList每个元素都需要额外空间(前后指针)

  • 线程安全

    ArrayListLinkedList不是线程安全的,可以通过

    1
    2
    List<Object> list = Collections.synchronizedList(new ArrayList<>());
    List<Object> list = Collections.synchronizedList(new LinkedList<>());

    包装一下,变成线程安全的

HashMap

问题
  • 链表过长时的解决方法

    桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率

    1. 数组扩容
      1. put元素后检查,当元素总数大于阈值(阈值=容量*负载因子)时会发生一次扩容
      2. 每次扩容都是之前容量的2倍
      3. 扩容之后会创建一个新的数组,把老数组中的元素迁移过去
        • 没有hash冲突的节点,直接e.hash & (newCap - 1)计算索引下标
        • 红黑树则走红黑树逻辑
        • 如果是链表,则需要遍历链表,可能需要拆分链表,判断e.hash & oldCap == 0是否成立,如果成立则仍在这个位置,否则移动到原始位置+oldCap
    2. 树化(jdk1.8):若数组容量小于64时会先尝试扩容,当数组容量超过64且链表长度大于8(阈值)时会树化
  • 底层数据结构,1.7和1.8有何不同?

    • 1.7为数组+链表,1.8为数组+(链表 | 红黑树)
    • 1.7为头插法,1.8为尾插法
    • 1.7先扩容后插入,1.8先插入后扩容(减少频繁转换的概率)
  • 为何要用红黑树,为何不一上来就树化,树化阈值为何是8,何时会树化,何时会退化成链表

    1. 大部分情况下链表长度是不会超过8的,只有在像Dos攻击时被注入大量key相同的元素时链表才会过长。红黑树用来防止链表过长时的性能下降。平衡二叉树是比红黑树更严格的平衡树,所以平衡二叉树插入和删除的效率 比红黑树要低,故用红黑树。

    2. hash表查找和更新的时间复杂度为O(1),红黑树为O(log2(n)),且红黑树的TreeNode比链表的Node占用内存大,所以尽量使用链表

    3. 选择8是为了让树化几率足够小;红黑树再转回链表的阈值是6,目的是防止节点数在8附近徘徊导致频繁发生转换

    4. 树化条件:链表长度>=阈值8 且 数组容量>=64

    5. 退化情况1:扩容时树被拆分,树元素个数<=6会退化成链表;

      退化情况2:remove节点时,检查若root,root.left,root.right,root.left.left中有一个为null,则退化

  • 索引是如何计算的?hashCode都有了为何还要hash()方法?数组容量为何是2^n?

    1
    2
    3
    4
    5
    6
    static final int hash(Object key) {
    int h;
    // key的hashCode和key的hashCode右移16位做异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    • 调用对象的hashCode()方法获得原始hash,再调用HashMap的hash()方法二次哈希,最后tab[i = (n - 1) & hash]获取到对应的位置,n为table的长度
    • 为了提高综合高位数据,让哈希分布更加均匀,减少冲突
    • 计算索引时,如果是2的n次幂可以使用位与运算代替取模,提高效率;扩容时hash & oldCap == 0的元素留在原处,否则新位置=旧位置 + oldCap
  • 介绍一下put方法流程,1.7和1.8的有何不同?

    1. 判断table是否为null,是的话执行resize()扩容(懒惰初始化)
    2. 计算索引(hashCode() –> hash() –> 和桶大小求模)
    3. 如果桶下标没人占用,则创建Node占位返回
    4. 如果有人占用了
      1. 如果是TreeNode走红黑树逻辑
      2. 如果是普通Node走链表逻辑,插入新节点后,如果需要树化则走树化逻辑
      3. 遍历链表或红黑树时发现key已存在则覆盖原先的值
    5. 返回前检查容量是否超过阈值,超过则扩容

    不同:

    1. 1.7是头插法,1.8是尾插法
    2. 1.7是元素数量>=阈值且没有空位时才扩容,1.8是>阈值时就扩容
    3. 1.8在扩容计算索引时有优化
  • 介绍一下get方法流程

    1. 先判断table是否非空或大小为0,是的话直接返回null
    2. 判断第一个元素的key是否和要找的key相同,是则返回
    3. 否则判断节点的类型
      1. 如果是树节点,则走红黑树的逻辑
      2. 否则遍历链表来找对应key的节点
  • 加载因子为何默认0.75?

    在空间占用和查询时间之间取得较好的平衡

    大于0.75,节省空间,但链表长了影响性能

    小于0.75,冲突减少,但扩容频繁空间占用更多

  • 多线程下会有什么问题?

    1. 扩容死链(1.7):1.7扩容是采用头插法,当两个线程同时进行扩容操作,若一个线程先扩容完,另一个线程再去扩容,会使得链表变成循环链表,造成死链
    2. 数据错乱(1.7,1.8):put逻辑是先判断是否为空,后插入数据。并发条件下可能出现两个线程都判断一个位置为空,后都往一个位置插入数据,造成数据丢失
  • key能否为null,作为key的对象有什么要求?

    1. HashMap的key可以为null。但Map的其它实现则不然
    2. 作为key的对象,必须实现hashCode()(为了key在整个HashMap中有更好的分布性)和equals()(当key相同时判断是否为同一个对象),并且key的内容不可以修改
  • 初始化时传入的容量不是2的倍数会怎样?

    会向上寻找离得最近的2的倍数

hashCode和equals

为什么重写 equals 时必须 重写hashCode 方法?

hashCode() 的作⽤是获取哈希码,也称为散列码,主要在哈希表这类集合映射的时候用到

  • 如果两个对象相等,则 hashcode ⼀定也是相同的

  • 如果两个对象有相同的 hashcode 值,它们也不⼀定是相等的

Java的IO模型

BIO

Blocking IO,阻塞式IO

应用发起系统调用后,程序会一直阻塞,直到内核把数据拷贝到用户空间

NIO

Non blocking IO,非阻塞IO

非阻塞式IO类型

  • 同步非阻塞:线程发起一个 read 调用,如果没有数据,会立即返回。这个时候应用程序可以不阻塞等待,而是切换去做一些小的计算任务,然后很快回来继续发起 read 调用
    • 优点:比BIO高效
    • 缺点:轮询消耗CPU资源
  • 多路复用(Java使用这个):所有请求都会注册到Selector选择器上,该线程不断轮询,只有真正有数据到达时,线程才会读取
    • 优点:通过减少无效的系统调用,减少了对 CPU 资源的消耗

NIO核心组件

  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
  • Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

AIO

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作

设计模式

单例模式

5种实现方式

1.饿汉式

要点:构造方法私有、静态成员变量、静态方法

1
2
3
4
5
6
7
8
9
10
11
public class Singleton1 implements Serializable {
private Singleton1() {

}

private static final Singleton1 INSTANCE = new Singleton1();

public static Singleton1 getInstance() {
return INSTANCE;
}
}

破坏单例的场景:1.反射获取私有构造方法,2.反序列化破坏单例,3,unsafe破坏单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.反射获取私有构造方法
// 预防方法:在构造方法里判断对象是否已创建
Class<Singleton1> clazz = Singleton1.class;

Constructor<Singleton1> constructor = clazz.getDeclaredConstructor();// 获取构造方法
constructor.setAccessible(true);// 设置私有构造方法也可以使用
constructor.newInstance();// 反射创建实例

// 2.反序列化破坏单例
// 把对象写到输出流
// 用输入流读取这个对象,还原出来的对象为新的对象
// 预防方法:重写readResolve()方法
public Object readResolve() {
return INStANCE;
}

// 3.unsafe破坏单例
// unsafe可以分配内存、释放内存。通过Singleton1的字节码对象可以创建一个新的
2.枚举类
1
2
3
enum Singleton2 {
INSTANCE;
}

反射和反序列化不会破坏单例

3.懒汉式

需要考虑线程安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton3 implements Serializable {
private Singleton3() {

}

private static final Singleton3 INSTANCE = null;

public synchronized static Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
:star:4.DCL懒汉式

double check lock,第三种的优化

volatile:解决共享变量的有序性和可见性。

​ 这里使用volatile主要解决有序性问题:new对象时主要步骤为分配内存、调用构造、给静态变量赋值。jvm可能会对创建对象步骤进行重排优化,导致先给景泰变量赋值再调用构造方法,别的线程会获得一个没有初始化完的对象。volatile修饰后则不会发送重排。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton4 implements Serializable {
private Singleton4() {

}

private static volatile final Singleton4 INSTANCE = null;

public static Singleton4 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}
}
5.内部类式

懒汉式的,推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton5 implements Serializable {
private Singleton5() {

}

private static class Holder {
static Singleton5 INSTANCE = new Singleton5();
}

public synchronized static Singleton5 getInstance() {
return Holder.INSTANCE;
}
}

jdk中用到单例的地方

  1. Runtime类:饿汉式。在System的exit()和gc()方法中有使用到

  2. Console类:DCL式。控制台的抽象

  3. Collections中用到一些空集合时,这个空的集合用到了单例,如EmptyNavigableSet,为内部类式

并发

线程有哪些状态

  • java线程分成六种状态

    1. NEW-新建:线程刚刚创建出来,还没有跟操作系统底层的线程关联起来
    2. RUNNABLE-可运行:java线程调用start()后,跟操作系统的线程相关联,代码由操作系统交给CPU执行
    3. TERMINATED-终结:代码执行完毕
    4. BLOCKED-阻塞:线程没获取到锁时进入阻塞状态
    5. WAITING-等待:获取到锁了,但发现运行所需条件不满足,调用wait()进入等待状态,会释放锁;当条件满足时,由其他线程调用notify()唤醒线程
    6. TIMED_WAITING-有时限等待:是等待状态的有时限版。还有调用sleep()也是进入有时限等待状态

  • 操作系统层面分为五种状态

    1. 运行:分到CPU时间片
    2. 就绪:可以分到CPU时间片
    3. 阻塞:分不到CPU时间片

    Java中的RUNNABLE涵盖就绪、运行、阻塞I/O

线程池的核心参数

线程池:管理一组线程,执行提交给线程池的任务

线程池主要参数

  1. corePollSize 核心线程数
    • 最多保留的线程数
  2. maximumPoolSize 最大线程数
    • 核心线程 + 救急线程
  3. keepAliveTime 生存时间
    • 针对救急线程,阻塞队列满时创建救急线程
  4. unit 时间单位
    • 针对救急线程
  5. workQueue 阻塞队列
    • 没有空闲的核心线程时任务的暂存区
  6. threadFactory 线程工厂
    • 可以给线程起名
  7. handler 拒绝策略
    • 没有空闲线程且阻塞队列满的时候的策略,四种

拒绝策略

  1. AbortPolicy (默认)
    • 丢弃任务,抛出异常
  2. CallerRunsPolicy
    • 由提交任务的线程执行任务
  3. DiscardPolicy
    • 丢弃任务
  4. DiscardOldPolicy
    • 丢弃阻塞队列中放的最久的任务,把新的加入进去

sleep & wait

共同点

  • wait()、wait(long)、sleep(long) 都是让线程放弃CPU的使用权,进入阻塞状态

不同点

  • 方法归属不同
    1. sleep(long)是Thread的静态方法
    2. wait()、wait(long)是Object的成员方法
  • 醒来时机不同
    1. sleep(long)和wait(long)在等够时间后被唤醒
    2. wait()和wait(long)还可以被notify()/notifyAll()唤醒
    3. 都可以通过interrupt()打断来唤醒(唤醒,抛出异常)
  • 锁特性不同
    1. 调用wait()方法必须先获取到锁
    2. wait()方法执行后会释放锁
    3. 而sleep()和synchronized代码块一起使用则不会释放锁

Lock & synchronized

语法层面

  • synchronized是关键字(C++实现),Lock是接口
  • synchronized退出代码块时会自动释放锁,Lock要手动调用unLock()方法

功能层面

  • 都是悲观锁,都具备互斥、同步、锁重入功能

  • Lock有更多的功能,如获取等待状态、公平锁、可打断、可超时、多条件变量

    公平锁:按照入阻塞队顺序获取锁

    非公平锁:还未入队的线程可能先获取到锁(插队)

    条件变量:多个等待队列,调用Condition.await()进入

  • Lock有适合不同场景的实现,如ReentrantLock(可重入锁),ReentrantReadWriteLock(适合读多写少锁)

性能层面

  • 在竞争少时synchronized做了很多优化,性能好
  • 竞争多时Lock性能更好

volatile能否保证线程安全

线程安全要考虑三个方法:

  1. 可见性:对共享变量的修改其他线程能看到最新结果
  2. 有序性:代码按编写顺序执行
  3. 原子性:多行代码以一个整体运行,期间不能有其他线程插队

可见性问题:线程频繁读取一个同一个值的变量时JIT编译器会对这段代码进行优化,把代码编译成机器码,这时修改内存中的变量对于这个线程来说是不可见的。

加volatile关键字JIT就不会对其优化

有序性:jvm会通过指令重排来进行优化。

加volatile会使用写屏障保证上面的代码不到当前变量的下面,使用读屏障保证下面的代码不到上面来

volatile只能保证可见性和有序性

乐观锁 & 悲观锁

  1. 悲观锁的代表是synchronized和Lock
    1. 核心是只有占有了锁才能操作共享变量
    2. 线程从运行->阻塞->唤醒涉及上下文切换,频繁的话影响性能
    3. 获取锁时判断已被上锁,会尝试重试几次,减少阻塞的机会(自旋)
  2. 乐观锁的代表是AtomicInteger(原子整数),使用cas保证原子性
    1. 核心是无需加锁,每次只有一个线程成功修改共享变量,其他线程不断重试直至成功(CAS的赋值操作是原子性的)
    2. 没有阻塞,不涉及上下文切换
    3. 需要多核cpu支持,且线程数不应超过cpu核数(超过了仍然会出现上下文切换)

Hashtable vs ConcurrentHashMap

  1. 都是线程安全的Map集合
  2. Hashtable并发度低,整个Hashtable只有一把锁,同时只能一个线程操作它
  3. 1.8开始ConcurrentHashMap将每个数组的头结点作为锁,多个线程访问不同的头结点不会产生冲突

ConcurrentHashMap

  1. 结构:数组+链表|红黑树

  2. 到达容量的0.75就会扩容

  3. 属性:

    • capacity:假定capacity为要放进来的元素的个数,通过capacity来计算数组的长度。(HashMap的capacity意思为初始数组长度)
    • factor:扩容的阈值,只在初始化时使用,之后都按0.75计算
  4. 因为每个数组的头结点都分别有一把锁,故并发度高

  5. 扩容流程:

    • 从最后一个数组节点开始扩容;
    • 链表迁移是通过复制新的链表元素到新数组中,防止迁移过程中链表的next指针发生变动造成的影响影响;
    • 扩容完后给原数组节点的头结点置forwardingNode,表示已经扩容完毕。
    • 必须等整个map扩容完才能执行put操作,如果来put的线程发现map正在扩容,可以来帮忙扩容。

谈谈对ThreadLocal的理解

作用:可以实现资源对象线程间的隔离,线程内的共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Utils{
// 线程间使用不同的资源,线程间隔离
// 线程内共享同一个资源
private static final ThreadLocal<Connection> t1 = new Thread<>();
public static Connection getConnection() {
Connection conn = t1.get();
if (conn == null) {
conn = innerGetConnection(); // 创建新的连接对象
t1.set(conn);
}
return conn;
}

private static Connection innerGetConnection() {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test?userSSL=false", "root", "123456");
}
}

原理

  • 每个线程有一个ThreadLocalMap,用来存储资源
  • 调用ThreadLocal的set()方法时,就是把ThreadLocal作为key,资源对象作为value存入到map中
  • 调用ThreadLocal的get()方法时,就是去map中找以当前ThreadLocal为key的资源
  • 调用ThreadLocal的remove()方法时, 就是去map中删除以当前ThreadLocal为key的资源
  • 如果产生hash冲突解决方法是开放寻址法
  • 垃圾回收:
    • ThreadLocalMap的key是弱引用(如果对于对象的引用中有强引用,则不能被回收,如果都是弱引用,则可以被回收),用于解决当其它地方都没有使用这个资源时,这个资源的key可以被垃圾回收
    • 在get()时发现key为null,value就会被释放;
    • 在set()时发现key为null,会把周围的null key也顺手释放
    • 在remove()时会释放value

快速失败(fail-fast)和安全失败(fail-safe)

快速失败

  • java.util包下的集合类都是快速失败的,如ArrayList
  • 迭代器中使用modCount记录原集合被修改的次数,每遍历一个元素时,都会和原数组中的modCount值进行比较,不一致直接抛出Concurrent Modification Exception异常

安全失败

  • java.util.concurrent包下的容器都是安全失败,如CopyOnWriteArrayList
  • 遍历时不是直接在集合内容上访问的,而是先 复制原有集合内容,在拷贝的集合上进行遍历
  • 缺点:迭代器遍历的是开始遍历那一刻拿 到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的

CopyOnWriteArrayList采用了一种读写分离的并发策略。CopyOnWriteArrayList容器 允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元 素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容 器的引用指向新容器。

JVM

JVM内存结构

  • Java Source:源代码
  • Java Class:字节码
  • 类加载子系统:把字节码文件加载到方法区,存放类的信息
  • Heap堆:分配对象的内存,存放对象的信息
  • JVM Stacks虚拟机栈:从虚拟机栈中给线程分配内存,存放方法内的局部变量、对象的引用
  • PC程序计数器:存放当前线程执行到了第几行代码
  • Interpreter解释器:逐行解释执行代码,效率低
  • JIT Compiler 即时编译器:字节码编译为机器码,效率高

方法区与永久代、元空间的关系

  1. 方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  2. 永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)
  3. 元空间是Hotspot虚拟机对JVM规范的实现(1.8之后)
  4. 永久代和元空间最大的区别是元空间使用本地内存作为信息的存储空间

元空间加载、释放类信息

加载:当第一次使用类的时候把字节码加载到元空间

释放:当类的实例、Class类不再被使用,且加载它的类加载器也不再使用(类加载器加载的所有类都不再被使用)的时候才会被释放

内存参数

例题-Xmx10260m -Xms10240m -Xmn5120m -XX:SurvivorRatio=3,其最小内存和Survivor区总大小分别是?

-Xmx:虚拟机最大内存

-Xms:虚拟机最小内存

-Xmn:新生代内存大小

-Xss:虚拟机栈中每个线程最大内存

-XX:SurvivorRatio:新生代伊甸区和fromto区的比例

​ 所以这题最小内存为10G,Survivor区(from+to)大小为2G

垃圾回收算法

  1. 标记清除

    1. 标记:一定不会被垃圾回收的对象被作为根对象,沿着根对象的引用进行标记,被标记的对象不能被垃圾回收

    2. 清除:把没有被标记的对象进行内存释放

    3. 缺点:对象不是连续的,容易造成内存碎片,已经没有回收器在使用这个算法

  2. 标记整理

    1. 整理:清除后移动对象往一端靠拢

    2. 缺点:移动对象造成效率低

  3. 标记复制

    1. 复制:标记后把存活对象复制到一块内存中,清空原来的那片内存

    2. 缺点:占用的内存较多

新生代适合标记复制法,老年代适合标记整理

GC和分代回收算法

  • GC的目的在于自动释放无用内存,减少内存碎片,加快分配速度
  • GC要点
    1. 回收区域是堆内存,虚拟机栈内存在方法调用结束会自动释放
    2. 使用可达性算法三色标记法标记存活对象,释放未标记对象
    3. GC大都在用分代回收思想,将回收区域分成新生代和老年代,应用不同的策略
    4. 格局GC规模可以分成MinorGCMixedGCFullGC
  • 分代回收
    1. 伊甸区eden:分配新对象
    2. 幸存区survivor:分成from和to,存放幸存对象
    3. 老年代old,存放扛过多次回收后晋升的对象(幸存区内存不足或大对象会导致提前晋升)
  • GC规模
    1. MinorGC发生在新生代的垃圾回收,时间短
    2. MixedGC为新生代+部分老年代的垃圾回收,G1收集器特有
    3. FullGC为新生代+老年代,回收时间长

三色标记与并发漏标

  • 用三种颜色标记对象的标记状态

    1. 黑色 - 已标记
    2. 灰色 - 标记中
    3. 白色 - 未标记

  • 漏标问题

    1. 垃圾回收线程在标记时用户线程并行执行,导致对象间的引用关系发生改变,从而影响标记的过程
    2. 解决方案:
      1. Incremental Update:记录引用关系被修改的对象(黑->灰),在最后stop the world时重新标记这些对象
      2. Snapshot At The Beginning:记录新加入的对象和被删除引用关系的对象,在最后stop the world时重新标记这些对象

垃圾回收器

  • (并行回收器)Parallel GC
    1. enden内存不足时发生MinorGC,标记复制STW
    2. old内存不足时发生FullGC,标记整理STW
    3. 多线程进行垃圾回收,注重吞吐量
  • (并发标记整理)Concurrent Mark Sweep GC
    1. old并发标记,重新标记时需要STW,并发清除
    2. Failback Full GC 并发失败时触发Full GC
    3. 大部分时间和用户线程并发,注重响应时间
  • G1 GC
    1. 兼顾响应时间和吞吐量
    2. 划分多个区域,每个区域都可以充当eden,survivor,old,humongous
    3. 过程:
      1. 新生代回收:eden标记复制STW
      2. 并发标记:old并发标记,重新标记时需STW
      3. 混合收集:eden + 优先收集回收价值高的old,复制时STW
      4. Failback Full GC:兜底

内存溢出

除了程序计数器,其他都有可能产生溢出

类型

OutOfMemoryError:

  1. 堆内存耗尽
  2. 方法区内存耗尽
  3. 虚拟机栈累积 - 每个线程最多占用1M内存,线程个数太多导致溢出

StackOverflowError:

  1. 虚拟机栈内部 - 方法调用过多导致线程的内存溢出

溢出情况

情况1:误用固定大小线程池

  • 使用Executors.newFixedThreadPool()创建的线程池的任务队列是无界队列(链表),可以放无数的任务,任务对象太多导致堆内存溢出
  • 解决方式:不使用newFixedThreadPool(),而是自己new ThreadPoolExecutor使用有界队列

情况2:误用带缓冲线程池

  • 使用Executors.newCacheThreadPool()创建的线程池只有救急线程,可以创建无数的线程导致虚拟机栈溢出
  • 解决方式:自己new ThreadPoolExecutor

情况3:一次查询太多数据

  • 数据量太大把物理内存挤爆了
  • 解决方式:给查询限制最大条数

情况4:动态类太多

  • 不断加载新的Class到方法区,把方法区挤爆了
  • 解决方式:降低类的声明周期,如不使用static等

类加载过程,双亲委派机制

类加载方式

  1. 隐式装载:通过new等方式生成对象时,隐式调用类加载器加载对应的类到JVM中
  2. 显示装载:Class.forName(className)ClassLoad.loadClass(className)

类加载器种类

  1. BootStrap Loader(引导类加载器),加载系统类
  2. ExtClass Loader(扩展类加载器),加载扩展类
  3. AppClass Loader(应用类加载器),加载应用类

类加载过程

  1. 加载
    1. 通过类的全限定名来获取到class文件,将字节码文件载入方法区,并创建.class对象(堆内)
    2. 类的父类没有加载,先加载父类
    3. 加载是懒惰执行
  2. 链接
    1. 验证-验证类是否符合Class规范
    2. 准备-为static变量分配空间,设置默认值(零值);将类的元信息加载进元空间
    3. 解析-将常量池的符号引用解析为直接引用
    4. 给final变量赋值,如果调用final变量不会触发初始化
  3. 初始化
    1. 从上往下给非final静态变量的赋值和执行静态代码块
    2. 初始化是懒惰执行

类初始化方法

  • 把静态变量、final修饰的引用类型和静态代码块合成一个static方法,在类的初始化时调用

解析过程

  • 方法内还没有使用到类的时候,常量池里的类为符号引用
  • 当使用到这个类时,这个类被加载到堆中,原先调用这个类的常量池中的符号引用也就被替换为真实的地址

双亲委派

  • 指优先委派上级类加载器进行加载
  • 当上级类加载器找不到要加载的类,下级类加载器才有资格执行加载
  • 上级类加载器加载的类对下级可见
  • 目的:让类加载有优先次序,保证核心类优先加载

对象引用类型

  1. 强引用
    1. A a = new A()就为强引用
    2. 通过GC Root的引用链能找到对象a就不会被回收
  2. 软引用
    1. SoftReference a = new SoftReference(new A())就为软引用
    2. 如果只有软引用一个对象,首次垃圾回收不会回收该对象,如果内存仍然不足,再次回收时才会释放对象
    3. 软引用自身需要配合引用队列来释放
    4. 例子:反射获得的数据是软引用
  3. 弱引用
    1. WeakReference a = new WeakReference(new A())就为弱引用
    2. 如果只有弱引用一个对象,只要垃圾回收就会回收该对象
    3. 弱引用自身需要配合引用队列来释放
    4. 例子:ThreadLocalMap中的Entry对象使用了弱引用
  4. 虚引用
    1. PhantomReference a = new PhatomReference(new A())就为虚引用
    2. 必须配合队列一起使用,当新引用引用的对象被回收时,会将虚引用对象入队,配合Reference Handler线程释放其关联的外部资源(非java的资源)

finalize

  • 对象被垃圾回收时会调用的方法

  • 由FinalizerThread调用finalize方法,而这个线程是守护线程,可能没来得及调用完方法就结束了

  • finalize有异常不会抛出异常

  • 影响性能:垃圾回收时第一次不能释放内存,要等线程调用完finalize方法后下次垃圾回收才能释放


Java八股
http://xwww12.github.io/2026/07/30/八股/Java八股/
作者
xw
发布于
2026年7月30日
许可协议