Java面试黄金宝典13
1. 对象在内存中的初始化过程
- 定义
当在 Java 里创建对象时,其在内存中的初始化要经历以下几个步骤:
- 类加载检查:JVM 会先确认对象对应的类是否已经加载。若未加载,就会开展类加载操作。类加载包含加载、验证、准备、解析和初始化这几个阶段。加载是把类的字节码文件加载到内存;验证是保证字节码文件的格式正确;准备阶段为类的静态变量分配内存并设置初始值;解析是把符号引用转换为直接引用;初始化则是执行类的静态代码块和静态变量的赋值操作。
- 分配内存:类加载完成后,JVM 会为对象分配内存。有两种分配方式:
- 指针碰撞:适用于内存规整的情况。内存的一边是已使用的空间,另一边是空闲空间,中间有一个指针作为分界点。分配内存时,只需将指针向空闲空间移动相应的距离即可。
- 空闲列表:适用于内存不规整的情况。JVM 会维护一个空闲内存块列表,分配内存时,从列表中找到合适的空闲块进行分配。
- 初始化零值:内存分配好后,JVM 会把分配到的内存空间(不包含对象头)初始化为零值。这就保证了对象的实例字段在 Java 代码里不赋初始值也能直接使用。例如,
int
类型会初始化为 0,boolean
类型会初始化为false
。 - 设置对象头:JVM 会设置对象的对象头,对象头包含对象的哈希码、分代年龄、锁状态等信息。这些信息对于 JVM 管理对象和实现同步机制非常重要。
- 执行
init
方法:最后,JVM 会执行对象的构造函数,完成对象的初始化。构造函数里可以进行一些业务逻辑的初始化,如给对象的字段赋值等。
- 原理
这个过程的目的是确保对象在使用前被正确初始化,并且在内存中合理分配和管理。类加载能让 JVM 了解对象的类信息,分配内存为对象提供存储空间,初始化零值保证字段有默认值,设置对象头方便管理对象状态,执行构造函数完成业务初始化。
- 要点
- 类加载是对象初始化的前提条件,只有类加载完成后才能创建对象。
- 内存分配方式由内存的规整性决定,不同的分配方式适用于不同的内存状态。
- 初始化零值和设置对象头由 JVM 自动完成,开发者无需干预。
- 构造函数是对象业务初始化的关键,开发者可以在构造函数中进行自定义的初始化操作。
- 应用
- 深入了解类加载机制的细节,如双亲委派模型。双亲委派模型可以保证类的加载顺序和安全性,防止恶意代码的加载。
- 研究内存分配的并发问题,如 CAS(Compare-And-Swap)机制。CAS 机制可以在多线程环境下保证内存分配的原子性。
- 了解对象头的具体信息和作用,如不同锁状态下对象头的变化。
2. 对象的强、软、弱和虚引用
- 定义
- 强引用:这是最常见的引用类型,例如
Object obj = new Object();
。只要强引用存在,垃圾回收器就不会回收被引用的对象。即使内存不足,JVM 抛出OutOfMemoryError
异常,也不会回收强引用指向的对象。 - 软引用:通过
SoftReference
类实现,示例代码为SoftReference<Object> softRef = new SoftReference<>(new Object());
。在系统即将发生内存溢出异常之前,会把这些软引用所引用的对象列入回收范围进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。软引用常用于实现内存敏感的缓存。 - 弱引用:通过
WeakReference
类实现,例如WeakReference<Object> weakRef = new WeakReference<>(new Object());
。只要垃圾回收器进行垃圾回收,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用常用于解决内存泄漏问题,如ThreadLocal
中就使用了弱引用。 - 虚引用:通过
PhantomReference
类实现,示例代码为PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
。虚引用并不会决定对象的生命周期,它的作用是在对象被垃圾回收时收到一个系统通知。虚引用必须和引用队列(ReferenceQueue
)一起使用。
- 原理
不同的引用类型对应着不同的垃圾回收策略,这样开发者就能根据不同的场景管理对象的生命周期,提高内存的使用效率。强引用保证对象的存活,软引用在内存不足时才会被回收,弱引用在垃圾回收时必定被回收,虚引用用于监控对象的垃圾回收。
- 要点
- 强引用会阻止对象被回收,使用时要注意避免内存泄漏。
- 软引用在内存不足时会被回收,适合用于实现缓存。
- 弱引用在垃圾回收时一定会被回收,可用于解决内存泄漏问题。
- 虚引用主要用于监控对象的垃圾回收,必须和引用队列一起使用。
- 应用
- 了解引用队列(
ReferenceQueue
)的作用。当软引用、弱引用或虚引用所引用的对象被垃圾回收时,引用对象会被加入到引用队列中,开发者可以通过引用队列来处理这些引用对象。 - 在实际开发中,利用不同的引用类型优化内存使用,如实现缓存时可以使用软引用或弱引用。
3. 如何减少 GC 的次数
- 定义
以下是一些减少 GC 次数的方法:
- 合理设置堆内存大小:依据应用程序的实际情况,合理设置堆内存的初始大小和最大大小。如果堆内存设置过小,会导致频繁的 GC;如果设置过大,会浪费系统资源。可以通过
-Xms
和-Xmx
参数来设置堆内存的初始大小和最大大小。 - 避免创建过多的临时对象:尽量复用对象,减少不必要的对象创建。例如,使用
StringBuilder
代替String
进行字符串拼接。因为String
是不可变对象,每次拼接都会创建一个新的String
对象,而StringBuilder
是可变对象,可以在原对象上进行拼接操作。 - 及时释放对象引用:当对象不再使用时,及时将引用置为
null
,这样垃圾回收器可以及时回收对象。例如,在方法结束时,将局部变量置为null
。 - 使用对象池:对于一些创建和销毁开销较大的对象,可以使用对象池来复用对象,如数据库连接池、线程池等。对象池可以预先创建一定数量的对象,当需要使用时从池中获取,使用完后放回池中,避免了频繁的创建和销毁对象。
- 优化数据结构:选择合适的数据结构,避免使用过大的数组或集合,减少内存占用。例如,如果只需要存储少量元素,可以使用
ArrayList
;如果需要存储大量元素,并且需要频繁的插入和删除操作,可以使用LinkedList
。
- 原理
GC 的主要目的是回收不再使用的对象,减少 GC 次数可以提高应用程序的性能。通过合理设置堆内存大小、避免创建过多临时对象等方法,可以减少内存中的对象数量,降低垃圾回收的频率。
- 要点
- 堆内存大小的合理设置是关键,要根据应用程序的实际情况进行调整。
- 复用对象和及时释放引用可以减少对象数量,提高内存使用效率。
- 对象池和优化数据结构可以降低对象的创建和销毁开销,减少内存占用。
- 应用
深入了解不同的垃圾回收器的特点和适用场景,根据应用程序的特点选择合适的垃圾回收器。例如,对于吞吐量要求较高的应用程序,可以选择 Parallel 垃圾回收器;对于响应时间要求较高的应用程序,可以选择 CMS 或 G1 垃圾回收器。
4. 什么是新生代、老年代、永久代
- 定义
- 新生代:对象刚创建时通常存放在新生代,大部分对象在新生代中被创建和销毁。新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor)。新创建的对象首先会被分配到 Eden 区,当 Eden 区满时,会触发 Minor GC,将存活的对象复制到其中一个 Survivor 区,当这个 Survivor 区满时,会将存活的对象复制到另一个 Survivor 区,经过多次 Minor GC 后,仍然存活的对象会被晋升到老年代。
- 老年代:用于存放存活时间较长的对象。当新生代中的对象经过多次垃圾回收仍然存活时,会被晋升到老年代。老年代的空间一般比新生代大,垃圾回收的频率相对较低。老年代的垃圾回收称为 Major GC 或 Full GC,通常会导致较长的停顿时间。
- 永久代:在 Java 8 之前,永久代用于存放类的元数据信息,如类的字节码、常量池等。永久代有固定的大小限制,容易出现内存溢出异常。在 Java 8 及以后,永久代被元空间(Metaspace)所取代,元空间使用本地内存,不再受 JVM 堆内存的限制。
- 原理
将堆内存分为新生代、老年代和永久代(元空间)是为了更好地管理对象的生命周期和垃圾回收。不同代的对象具有不同的特点,采用不同的垃圾回收策略可以提高垃圾回收的效率。例如,新生代中的对象大部分都是朝生夕死的,适合使用复制算法进行垃圾回收;老年代中的对象存活时间较长,适合使用标记 - 清除或标记 - 整理算法进行垃圾回收。
- 要点
- 新生代主要存放新创建的对象,垃圾回收频繁,使用复制算法进行垃圾回收。
- 老年代存放存活时间较长的对象,垃圾回收频率较低,使用标记 - 清除或标记 - 整理算法进行垃圾回收。
- 永久代(元空间)存放类的元数据信息,Java 8 及以后使用元空间取代永久代。
- 应用
了解不同代的垃圾回收算法,如新生代的复制算法、老年代的标记 - 清除算法和标记 - 整理算法等。复制算法的优点是效率高,缺点是需要额外的内存空间;标记 - 清除算法的优点是不需要移动对象,缺点是会产生内存碎片;标记 - 整理算法的优点是可以避免内存碎片,缺点是效率相对较低。
5. 什么是 JUC
- 定义
JUC 是 java.util.concurrent
包的简称,它是 Java 5 引入的一个用于处理并发编程的工具包。JUC 提供了一系列的类和接口,用于简化并发编程的开发,提高并发程序的性能和可靠性。
- 原理
JUC 基于 Java 的多线程机制,通过提供高级的并发工具和数据结构,封装了底层的线程同步和通信机制,使得开发者可以更方便地编写并发程序。例如,ExecutorService
接口提供了线程池的功能,ReentrantLock
类提供了可重入锁的功能,AtomicInteger
类提供了原子操作的功能。
- 要点
- JUC 提供了线程池、锁、原子类、并发集合等工具,这些工具可以帮助开发者更方便地编写并发程序。
- 使用 JUC 可以提高并发程序的开发效率和性能,减少线程同步和通信的复杂性。
-
应用
深入学习 JUC 中的各个类和接口的使用方法,如 ExecutorService
、ReentrantLock
、AtomicInteger
等。了解这些类和接口的底层实现原理,如 ReentrantLock
是如何实现可重入锁的,AtomicInteger
是如何实现原子操作的。
6. 什么是 CountDownLatch
- 定义
CountDownLatch
是 JUC 中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作后再继续执行。CountDownLatch
内部维护了一个计数器,在创建 CountDownLatch
对象时需要指定计数器的初始值。当一个线程完成操作后,可以调用 countDown()
方法将计数器减 1,当计数器的值为 0 时,等待的线程会被唤醒继续执行。
- 原理
CountDownLatch
基于 AQS(AbstractQueuedSynchronizer)实现,通过计数器来控制线程的等待和唤醒。当计数器不为 0 时,调用 await()
方法的线程会被阻塞,直到计数器的值为 0。AQS 是一个用于构建锁和同步器的框架,它提供了一个先进先出的队列来管理等待的线程。
- 要点
- 适用于一个或多个线程等待其他线程完成操作的场景,如主线程等待多个子线程完成任务后再进行汇总。
- 计数器的值只能减少,不能增加。一旦计数器的值为 0,就不能再被重置。
- 使用
await()
方法让线程等待,使用countDown()
方法减少计数器的值。
- 应用
了解 CountDownLatch
在实际项目中的应用场景,如多线程任务的并行执行和结果汇总。可以结合实际项目,编写使用 CountDownLatch
的示例代码,加深对其的理解。
7. 什么是 CyclicBarrier
- 定义
CyclicBarrier
是 JUC 中的一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点后再继续执行。CyclicBarrier
在创建时需要指定一个屏障值,表示需要等待的线程数量。当一个线程到达屏障点时,会调用 await()
方法进行等待,当所有线程都到达屏障点后,所有线程会同时被唤醒继续执行。
- 原理
CyclicBarrier
基于 ReentrantLock
和 Condition
实现,通过内部的计数器来记录到达屏障点的线程数量,当计数器的值达到屏障值时,会唤醒所有等待的线程。ReentrantLock
是一个可重入锁,Condition
是一个条件变量,用于线程之间的通信。
- 要点
- 适用于一组线程需要相互等待的场景,如多个线程需要同时开始执行某个任务。
- 可以重复使用,即屏障可以被重置。当所有线程都通过屏障后,计数器会被重置,可以再次使用。
- 使用
await()
方法让线程等待,当所有线程都调用await()
方法后,线程会同时被唤醒。
- 应用
了解 CyclicBarrier
和 CountDownLatch
的区别,以及在实际项目中的应用场景,如多线程任务的分阶段执行。可以编写使用 CyclicBarrier
的示例代码,对比它和 CountDownLatch
的使用方式。
8. 什么是 Semaphore
- 定义
Semaphore
是 JUC 中的一个同步工具类,它用于控制同时访问某个资源的线程数量。Semaphore
在创建时需要指定一个许可证数量,表示允许同时访问资源的最大线程数。当一个线程需要访问资源时,会调用 acquire()
方法获取许可证,如果许可证数量大于 0,则线程可以获取许可证并访问资源,同时许可证数量减 1;如果许可证数量为 0,则线程会被阻塞,直到有其他线程释放许可证。当线程访问完资源后,会调用 release()
方法释放许可证,许可证数量加 1。
- 原理
Semaphore
基于 AQS 实现,通过内部的计数器来控制许可证的数量。当计数器的值大于 0 时,线程可以获取许可证;当计数器的值为 0 时,线程需要等待。AQS 提供了一个先进先出的队列来管理等待的线程。
- 要点
- 适用于需要控制并发访问资源的场景,如数据库连接池的并发控制、限流等。
- 可以实现限流的功能,通过控制许可证的数量来限制同时访问资源的线程数量。
- 使用
acquire()
方法获取许可证,使用release()
方法释放许可证。
- 应用
了解 Semaphore
在实际项目中的应用场景,如数据库连接池的并发控制。可以编写使用 Semaphore
的示例代码,模拟限流的场景。
9. 什么是 Exchanger
- 定义
Exchanger
是 JUC 中的一个同步工具类,它允许两个线程在某个同步点交换数据。当一个线程调用 exchange()
方法时,会进入等待状态,直到另一个线程也调用 exchange()
方法,此时两个线程会交换各自的数据。
- 原理
Exchanger
基于 CAS(Compare-And-Swap)和 LockSupport 实现,通过内部的状态变量来控制线程的等待和交换操作。CAS 是一种无锁算法,用于实现原子操作;LockSupport 是一个用于阻塞和唤醒线程的工具类。
- 要点
- 适用于两个线程之间的数据交换场景,如数据的生产者 - 消费者模式中的数据交换。
- 只能用于两个线程之间的交换,不支持多个线程之间的交换。
- 使用
exchange()
方法进行数据交换。
- 应用
了解 Exchanger
在实际项目中的应用场景,如数据的生产者 - 消费者模式中的数据交换。可以编写使用 Exchanger
的示例代码,模拟两个线程之间的数据交换。
10. 什么是 CopyOnWriteArrayList、CopyOnWriteArraySet 和 ConcurrentSkipListSet
- 定义
- CopyOnWriteArrayList:是 JUC 中的一个并发集合类,它是
ArrayList
的线程安全版本。CopyOnWriteArrayList
在进行写操作(如add
、remove
等)时,会先将原数组复制一份,然后在新数组上进行操作,操作完成后再将新数组赋值给原数组的引用。读操作则直接在原数组上进行,不需要加锁。 - CopyOnWriteArraySet:是 JUC 中的一个并发集合类,它是
HashSet
的线程安全版本。CopyOnWriteArraySet
内部使用CopyOnWriteArrayList
来实现,保证了元素的唯一性。 - ConcurrentSkipListSet:是 JUC 中的一个并发集合类,它是
TreeSet
的线程安全版本。ConcurrentSkipListSet
基于跳表(Skip List)数据结构实现,支持并发的插入、删除和查找操作,并且元素会按照自然顺序或指定的比较器进行排序。
- 原理
CopyOnWriteArrayList
和CopyOnWriteArraySet
通过写时复制的方式实现线程安全,牺牲了一定的写性能来换取读性能的提升。因为写操作需要复制数组,所以写操作的开销较大;而读操作不需要加锁,所以读操作的性能较高。ConcurrentSkipListSet
通过跳表数据结构和 CAS 机制实现并发操作,保证了线程安全和高效的查找性能。跳表是一种有序的数据结构,它可以在 O(logn) 的时间复杂度内完成插入、删除和查找操作。
- 要点
CopyOnWriteArrayList
和CopyOnWriteArraySet
适用于读多写少的场景,因为写操作的开销较大,所以不适合写操作频繁的场景。ConcurrentSkipListSet
适用于需要并发操作和有序集合的场景,如需要对元素进行排序的并发场景。- 了解这些集合类的性能特点和适用场景,根据实际需求选择合适的集合类。
- 应用
了解这些并发集合类的性能特点和适用场景,以及在实际项目中的应用案例。可以编写使用这些集合类的示例代码,对比它们的性能和使用方式。
友情提示:本文已经整理成文档,可以到如下链接免积分下载阅读
https://download.csdn.net/download/ylfhpy/90528591