Java面试必会50题
1、Java堆空间内存溢出
Java的堆空间内存溢出是对象数量增长到对空间没有内存可用,即超出了堆的最大可用空间,导致无法分配新的对象而发生的错误。
堆空间内存溢出的常见原因包括:
- 内存泄漏:长时间持有对象的引用而不释放,导致无法被垃圾收集器回收的对象占满了堆空间。
- 对象生命周期不当:某些对象的生命周期过长,使得它们在堆中长时间存活,占用了大量的内存空间。
- 大对象:尝试分配一个比可用内存更大的对象,超出了堆的剩余空间。
- 并发内存溢出:在多线程应用程序中,如果每个线程分配的对象超出了堆的总可用空间,就可能导致内存溢出。
如何解决堆空间内存溢出的问题?
- 优化内存使用:检查代码,确保对象只在需要时创建,并在不再需要时及时释放。
- 增加堆空间:通过调整JVM参数(如-Xmx和-Xms),增加堆的最大和初始大小,以适应应用程序的需求。
- 分析内存泄漏:使用工具如Heap Dump分析工具(如Eclipse Memory Analyzer)来分析堆转储文件,找出内存泄漏的原因并进行修复。
- 减少对象的生命周期:确保对象的生命周期尽可能短,及时释放不再需要的对象的引用。
- 优化代码和数据结构:使用更高效的算法和数据结构,减少内存占用。
- 考虑使用更大的物理内存:如果可能的话,考虑在运行应用程序的机器上增加物理内存,以减少堆内存溢出的风险。
2、GC开销超过限制,引起OOM(outofmemory)内存溢出
- 频繁的Full GC:
- Full GC 是指对整个堆空间进行垃圾收集,它通常比部分收集(如新生代或老年代)更耗时。如果应用程序频繁执行Full GC,并且每次Full GC 的执行时间较长,会导致应用程序停顿时间过长,影响性能。
- 内存泄漏:
- 如果应用程序中存在内存泄漏,即无法访问的对象仍然占用内存,垃圾收集器将无法释放这些内存,最终导致堆内存耗尽。
- 对象存活时间过长:
- 有些对象可能会被错误地保持在内存中,其生命周期比预期更长。这可能是由于长时间持有对象引用或者缓存机制设计不当等原因。
解决方法:
- 调整GC参数:
- 调整垃圾收集器的参数和行为,例如调整新生代和老年代的比例、堆大小、GC算法选择等,以减少GC执行的频率和时间。可以通过
-Xmx
、-Xms
、-XX:NewRatio
等JVM参数进行调优。
- 调整垃圾收集器的参数和行为,例如调整新生代和老年代的比例、堆大小、GC算法选择等,以减少GC执行的频率和时间。可以通过
- 分析GC日志:
- 使用JVM提供的GC日志功能(如
-verbose:gc
、-Xlog:gc
等),分析GC活动和应用程序行为。根据日志分析找出哪些GC导致了较高的开销,以及是否存在频繁的Full GC情况。
- 使用JVM提供的GC日志功能(如
- 内存泄漏分析:
- 使用内存分析工具(如Eclipse Memory Analyzer、VisualVM、MAT等)来检测和解决内存泄漏问题。通过分析堆转储文件(heap dump),找出泄漏对象的引用链,识别出导致内存泄漏的代码位置。
- 代码优化:
- 优化应用程序代码,确保对象的生命周期尽可能短,避免不必要的对象持有。例如,及时释放不再需要的对象引用、避免过度使用静态变量等。
- 增加物理内存或者升级硬件:
- 如果应用程序的内存使用量已经接近硬件限制,可以考虑增加物理内存或者升级服务器硬件,以提供更大的堆空间和更好的性能。
3、请求的数组大小超过虚拟机限制,引起OOM?
请求的数组大小超过虚拟机的限制可能会导致OutOfMemoryError(OOM),具体来说,这种情况通常与Java虚拟机的堆内存大小有关。Java数组在内存中是以连续的存储空间存放的,因此数组的大小受到堆内存大小的限制。
可能的情况和原因:
- 堆内存大小限制:
- Java应用程序的堆内存大小由JVM参数
-Xmx
和-Xms
控制,分别指定了堆的最大和初始大小。如果请求的数组大小超过了可用的堆内存空间,JVM会抛出OutOfMemoryError。
- Java应用程序的堆内存大小由JVM参数
- 单个数组大小限制:
- Java中,数组的大小由
int
类型的索引来表示,因此最大可索引的数组大小为Integer.MAX_VALUE
,即2^31 - 1
,约为2GB。如果尝试创建一个超过这个大小的数组,会导致OutOfMemoryError
。
- Java中,数组的大小由
- 堆空间的其他使用:
- 堆内存不仅仅用于存放Java对象,还需要考虑其他因素如线程栈、类信息、常量池等的占用。因此,实际可用于数组分配的内存空间可能会比
-Xmx
指定的堆内存大小稍小。
- 堆内存不仅仅用于存放Java对象,还需要考虑其他因素如线程栈、类信息、常量池等的占用。因此,实际可用于数组分配的内存空间可能会比
解决方法:
- 增加堆内存大小:
- 如果应用程序需要处理大量数据或者大型数组,可以通过增加
-Xmx
参数来增加堆内存大小。这样可以提供更多的内存空间来存放大数组,从而避免OOM。
- 如果应用程序需要处理大量数据或者大型数组,可以通过增加
- 优化数据结构:
- 考虑是否可以通过其他数据结构或者算法来代替大数组,以减少内存使用。有时候可以使用流式处理或者分批次处理数据,而不是一次性加载所有数据到一个巨大的数组中。
- 检查内存使用情况:
- 使用JVM监控工具如VisualVM、JConsole等来监控应用程序的内存使用情况,查看堆内存的分配情况和趋势,及时调整
-Xmx
参数。
- 使用JVM监控工具如VisualVM、JConsole等来监控应用程序的内存使用情况,查看堆内存的分配情况和趋势,及时调整
- 分析内存泄漏:
- 如果怀疑存在内存泄漏导致堆内存不断增长,应该使用堆转储文件和内存分析工具来分析内存泄漏问题,并进行修复。
4、永久代(Perm gen) 空间,引起OOM?
在旧版的Java虚拟机(JVM)中,比如Java 7及以前的版本,存在一个称为永久代(Permanent Generation,PermGen)的区域,用于存放类的元数据、常量池等信息。然而,PermGen空间的使用过度也可以导致OutOfMemoryError(OOM)异常,尽管它与普通的堆空间(Heap)不同。
引起PermGen OOM的情况:
1.类的元数据过多:
**2.每个类在内存中都有一些元数据,包括类的结构、方法信息等。**如果应用程序动态加载了大量的类,或者使用了大量的第三方库,可能会导致PermGen空间的耗尽。例如,一些框架或者应用服务器在运行时动态生成或者加载类,会增加PermGen的使用量。
3.字符串常量池:
4.字符串常量池中的字符串对象也存储在PermGen空间中。如果应用程序大量使用字符串,并且在运行时动态生成了大量的字符串常量,也可能导致PermGen空间耗尽。
5.未正确配置PermGen大小:
**6.默认情况下,PermGen空间的大小是有限的,并且可能比较小。**如果应用程序加载了大量的类或者有大量的字符串常量,而PermGen空间配置不足以容纳这些数据,就会发生OOM。
解决PermGen OOM的方法:
1.增加PermGen空间的大小:
**2.可以通过JVM参数 -XX:MaxPermSize 来增加PermGen空间的大小。**例如,-XX:MaxPermSize=256m 表示将PermGen空间的最大大小设置为256MB。但需要注意,Java 8及更新版本已经移除了PermGen空间,改为使用元数据区(Metaspace)。
3.优化类加载和卸载:
4.确保应用程序只加载需要的类,并且能够及时卸载不再需要的类。一些应用服务器如Tomcat、WebLogic等提供了类加载器的监控和调优功能,可以帮助优化类加载行为。
5.升级到较新的Java版本:
**6.Java 8及更高版本使用元数据区(Metaspace)替代了PermGen空间。**Metaspace的大小由操作系统的内存来管理,默认情况下不再有固定的上限,可以更灵活地管理类的元数据。
7.检查应用程序的类加载和字符串使用:
8.分析应用程序的类加载情况和字符串使用情况,优化代码,尽量减少不必要的类加载和字符串常量的创建。
总之,虽然PermGen空间不再在Java 8及更新版本中存在,但在使用旧版Java时,理解和适当管理PermGen空间仍然是避免OOM异常的重要一步。
5、Metaspace元空间耗尽,引起OOM?
,Metaspace(元空间)的耗尽也可以导致OutOfMemoryError(OOM)异常。Metaspace是Java 8及更新版本中取代了永久代(PermGen)的区域,用于存储类的元数据。与PermGen不同,Metaspace的大小不再由 -XX:MaxPermSize
参数控制,而是由操作系统的虚拟内存限制(如物理内存和交换空间)来管理。
引起Metaspace OOM的情况:
- 动态生成和加载类过多:
- 如果应用程序动态生成了大量的类(比如通过反射或者动态代理),或者加载了大量的第三方库和依赖,可能会导致Metaspace空间的耗尽。
- 字符串常量池和静态变量过多:
- Metaspace还包括存储字符串常量池和静态变量的空间。如果应用程序大量使用字符串常量或者有大量的静态变量,也会增加Metaspace的使用量。
- 未正确配置Metaspace大小:
- 虽然Metaspace的大小由操作系统的虚拟内存限制来管理,但可以通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
参数来调整Metaspace的初始大小和最大大小。如果未能合理配置这些参数,可能会导致Metaspace OOM。
- 虽然Metaspace的大小由操作系统的虚拟内存限制来管理,但可以通过
解决Metaspace OOM的方法:
- 增加Metaspace大小:
- 可以通过调整
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
参数来增加Metaspace的初始大小和最大大小。例如,-XX:MaxMetaspaceSize=256m
表示将Metaspace的最大大小设置为256MB。
- 可以通过调整
- 优化类的加载和卸载:
- 确保应用程序只加载需要的类,并且能够及时卸载不再需要的类。合理使用类加载器、避免不必要的动态类生成等可以减少Metaspace的压力。
- 检查字符串和静态变量的使用:
- 分析应用程序中字符串常量池和静态变量的使用情况,优化代码,尽量减少不必要的字符串常量和静态变量的创建。
6、无法新建本机线程?
引起无法新建本机线程的原因:
- 操作系统资源限制:
- 操作系统对于每个进程的线程数量有限制。如果应用程序请求创建的线程数超过了操作系统允许的限制,就会导致无法新建本机线程的错误。不同的操作系统和配置可能有不同的限制。
- 内存资源不足:
- 创建一个新的线程需要分配内存资源,包括栈空间和其他数据结构所需的内存。如果系统内存资源已经耗尽或者达到了操作系统的限制,就无法再创建新的线程。
- 线程资源泄漏:
- 如果应用程序存在线程资源泄漏,即创建了大量的线程但未正确释放,会导致线程池或系统资源被耗尽,进而导致无法创建新的线程。
解决方法:
- 增加操作系统的线程限制:
- 可以通过调整操作系统的配置参数(如ulimit)来增加允许每个进程的线程数量。具体操作取决于使用的操作系统。
- 优化线程使用:
- 确保应用程序合理使用线程,避免创建过多的线程。可以使用线程池来管理和重用线程,以减少线程创建和销毁的开销。
- 检测和修复线程泄漏:
- 使用性能分析工具来检测是否存在线程泄漏问题,及时修复并释放不再需要的线程资源。
- 升级硬件和优化资源分配:
- 如果是因为系统资源不足导致的问题,考虑升级硬件(如增加内存)或者优化应用程序的资源使用方式。
7、为什么wait\notify\notifyall是在 Object 而不是 Thread 中?
wait()
, notify()
, 和 notifyAll()
方法是定义在 Java 中所有对象的基类 Object
中,而不是 Thread
类中的原因主要有两点:
- 等待/通知模型的本质:
- Java 中的等待/通知(wait/notify)机制是基于对象之间的协作而不是线程之间的协作。每个对象都有一个相关的锁(或监视器),线程可以通过获取该锁来进入对象的同步代码块或方法。
wait()
,notify()
, 和notifyAll()
方法实际上是用来管理线程在对象上的等待和通知关系,而不是直接控制线程的执行。
- Java 中的等待/通知(wait/notify)机制是基于对象之间的协作而不是线程之间的协作。每个对象都有一个相关的锁(或监视器),线程可以通过获取该锁来进入对象的同步代码块或方法。
- 对象的监视器和等待集合:
- 每个对象在 Java 中都与一个监视器(monitor)相关联,该监视器包含一个等待集合(wait set)。当一个线程调用对象的
wait()
方法时,它将释放该对象的锁并进入等待集合,直到其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒等待线程。因此,这些方法是操作对象的等待集合和通知等待线程的机制。
- 每个对象在 Java 中都与一个监视器(monitor)相关联,该监视器包含一个等待集合(wait set)。当一个线程调用对象的
因此,将 wait()
, notify()
, 和 notifyAll()
方法定义在 Object
类中使得任何对象都可以作为同步的信号量或锁,并允许线程在等待和唤醒之间进行协作。这种设计的优势在于它的通用性和灵活性,使得 Java 中的线程同步和协作机制更加简洁和直观。
8、为什么 String 在 Java 中是不可变的?
在 Java 中,String 对象是不可变的,这意味着一旦创建了一个 String 对象,它的值就不能被修改。这种设计选择有以下几个重要的原因:
1、安全性:
字符串常常作为用于密码、数据库连接等敏感信息的容器。如果字符串是可变的,那么在程序的任何地方都有可能修改它们的内容,这可能导致安全漏洞。通过使字符串不可变,可以确保一旦创建,它们的值在整个程序执行期间保持不变,从而避免了意外的修改。
2、线程安全:
不可变的字符串是线程安全的,因为多个线程可以同时访问它们而无需担心数据竞争或需要同步措施。如果字符串是可变的,那么在并发环境中需要额外的同步操作来确保多线程访问时不会导致不一致的状态。
3、缓存优化:
不可变字符串可以被缓存,因为它们的值在内存中是唯一的,可以安全地共享。这种共享可以提高性能,因为不需要为相同的字符串分配额外的内存。
4、Hash值缓存:
因为字符串是不可变的,所以它们的 hash 值可以在第一次计算后进行缓存。这种优化提高了字符串作为 HashMap 的键时的性能,因为不需要每次访问都重新计算 hash 值。
5、简化并提高效率:
不可变对象的设计使得编码更加简单和可靠。它们也更容易进行优化,因为编译器可以在编译时对字符串的处理进行更多的静态优化,而不必担心在运行时可能发生的意外修改。
总之,Java 中字符串不可变的设计是出于安全性、线程安全、性能优化以及代码可靠性的考虑。这种设计选择是 Java 语言中的一个重要特性,被广泛应用于各种开发场景中。
9、什么是线程安全的单例,你怎么创建它?
线程安全的单例是指在多线程环境下,保证只有一个实例被创建,并且所有线程都能够安全地访问该实例。在 Java 中,创建线程安全的单例通常有以下几种方式:
- 饿汉式单例模式(Eager Initialization)
在类加载时就创建单例实例,保证线程安全。适合于单例对象较小且使用频繁的情况。
public class Singleton {
// 在类加载时即创建单例实例
private static final Singleton instance = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
- 懒汉式单例模式(Lazy Initialization)
延迟实例化,在首次被调用时才创建单例实例。可以通过加锁保证线程安全,但性能稍差。
2.1. 简单的懒汉式单例(非线程安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种简单的懒汉式单例在多线程环境下会有问题,可能会创建多个实例。
2.2. 加锁的懒汉式单例(线程安全)
使用双重检查锁定(double-checked locking)确保在多线程环境下也只创建一个实例。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
1.volatile 关键字确保在多线程情况下,instance 的修改能够立即被其他线程可见。
2.双重检查锁定可以减少锁的使用次数,提高性能。
- 静态内部类单例模式
利用 Java 类加载的特性,结合静态内部类实现懒加载,并且保证线程安全。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类的方式利用了类加载时的线程安全性,同时实现了延迟加载,保证了高效和线程安全。
总结
选择合适的单例模式取决于应用的具体需求,饿汉式和静态内部类方式通常是首选,因为它们都能够保证线程安全且实现简单高效。在实现线程安全单例时,要注意考虑性能、延迟加载需求以及并发访问的情况,以选择最适合的实现方式。
10. Java中的CAS算法?
在Java中,CAS(Compare and Swap)是一种乐观锁机制,通常用于实现多线程环境下的并发控制。CAS算法的核心思想是,当多个线程尝试更新同一个变量时,只有一个线程能够成功,其他线程失败,失败的线程可以根据失败的情况进行重试或者其他逻辑处理。
CAS的基本原理
CAS操作包含三个操作数:内存位置(通常是一个变量的内存地址)、期望值(即当前变量的预期值)、新值(即希望更新后的新值)。操作的过程如下:
1.读取当前内存中的值(期望值)。
2.比较内存中的值与期望值是否相等。
3.如果相等,则更新内存中的值为新值。
4.如果不相等,则不做任何操作(或者根据业务逻辑进行重试等)。
Java中的CAS操作通常由 java.util.concurrent.atomic 包中的类提供,例如 AtomicInteger, AtomicLong, AtomicReference 等。
示例
以 AtomicInteger 为例,它使用CAS来实现线程安全的自增操作:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
// 多线程同时自增操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // CAS操作,自增并获取结果
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // CAS操作,自增并获取结果
}
});
thread1.start();
thread2.start();
// 等待两个线程执行完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终结果
System.out.println("Final counter value: " + counter.get());
}
}
CAS的优缺点
优点:
5.比传统的锁机制(如synchronized)具有更好的性能,因为它是乐观锁,不会阻塞线程。
6.避免了死锁的发生。
缺点:
7.自旋重试会消耗CPU资源,特别是在高并发时。
8.ABA问题:CAS操作基于当前值,无法检测到值被多次修改后恢复原值的情况,可以通过版本号或标记来解决。
在Java并发编程中,CAS算法在实现非阻塞算法和高性能并发容器时被广泛应用,例如ConcurrentHashMap等。
11、Aio,Nio,Bio的作用和区别?
在Java中,AIO(Asynchronous I/O)、NIO(Non-blocking I/O)、BIO(Blocking I/O)是三种不同的I/O模型,各自具有不同的特点和适用场景。
- BIO(Blocking I/O)
作用:
1.BIO是最传统的I/O模型,也称为阻塞I/O。
2.每个I/O操作(如读或写)都会阻塞当前线程,直到数据准备好或者操作完成。
3.适合于低并发、简单易理解的场景,如单线程服务器或者客户端编程。
特点:
4.每个连接都需要独立的线程进行处理,如果连接数目较大,需要大量线程,会造成线程资源消耗。
5.阻塞I/O的方式简单直观,但在高并发环境下效率低下,因为大量线程会竞争系统资源而导致性能下降。
- NIO(Non-blocking I/O)
作用:
6.NIO引入了非阻塞概念,使得一个线程可以管理多个连接(Channel)。
7.NIO支持Selector选择器,通过一个线程可以监听多个Channel上的事件,实现了多路复用。
8.适合于高并发、连接数多的网络应用,如聊天服务器、游戏服务器等。
特点:
9.通过单一的线程管理多个连接,避免了大量线程的创建和维护,提高了系统的扩展性和性能。
10.编程模型较复杂,需要处理事件的分发和管理,但可以通过合理的事件驱动模型实现高效的网络通信。
- AIO(Asynchronous I/O)
作用:
11.AIO是在NIO的基础上进一步封装的一种异步非阻塞I/O模型。
12.AIO的关键在于操作系统提供的异步通知机制,使得I/O操作完全异步完成,不需要通过轮询方式等待数据就绪。
13.适合于处理大量并发连接,并且每个连接都有较少的数据交互但需要快速响应的场景,如文件操作、大文件传输等。
特点:
14.完全异步的处理方式,不会阻塞线程,可以大幅提升系统的吞吐量和并发能力。
15.相对于NIO,AIO对于大文件读写和网络编程的支持更为强大,但在某些情况下可能带来更高的系统开销。
区别总结
BIO:每个连接都需要独立的线程处理,阻塞I/O,适合低并发的场景。
NIO:通过单一线程管理多个连接,非阻塞I/O,使用选择器实现多路复用,适合高并发的网络应用。
AIO:完全异步非阻塞I/O,利用操作系统提供的异步通知机制,适合大量连接和大文件操作。
选择合适的I/O模型取决于具体的应用场景和性能要求。
12、ReetrantLock和synchronized的区别和原理?
ReentrantLock
和 synchronized
是 Java 中用于实现线程同步的两种机制,它们有一些区别和原理上的不同点。
区别和原理
- 原理
- synchronized:
synchronized
是 Java 中的关键字,用于实现原子性的同步操作。synchronized
依赖于 JVM 内置的锁机制,即 monitor 锁。- 当一个线程获取了对象的锁(monitor),其他试图获取该对象锁的线程将被阻塞,直到持有锁的线程释放锁。
synchronized
保证了线程的可见性和原子性,简单易用,编写和维护代码相对容易。
- ReentrantLock:
ReentrantLock
是java.util.concurrent.locks
包中提供的锁实现类。ReentrantLock
提供了比synchronized
更多的灵活性和额外的功能。ReentrantLock
是基于 AQS(AbstractQueuedSynchronizer)实现的,使用了 CAS(Compare and Swap)操作来实现非阻塞的同步。- 它支持可重入性、公平锁和非公平锁、定时锁等高级特性,可以实现更复杂的同步需求。
- 区别
- 可重入性:
synchronized
是可重入的,同一个线程可以多次获取同一个锁,而不会出现死锁。ReentrantLock
也是可重入的,同一个线程可以多次获取锁,但需要注意在使用完毕后正确释放锁,否则可能造成死锁。
- 灵活性和功能:
ReentrantLock
比synchronized
提供了更多的功能,如定时锁、公平锁和非公平锁的选择、可中断的锁获取、多条件变量等。synchronized
是内置的语言特性,功能相对简单,不能灵活控制锁的行为。
- 性能:
- 在 JDK 6 之前,
synchronized
的性能比ReentrantLock
差一些,但是从 JDK 6 开始,JVM 对synchronized
进行了很多优化,性能逐渐接近。 ReentrantLock
在高并发情况下可能比synchronized
更高效,特别是对于公平锁和大量线程竞争的场景。
- 在 JDK 6 之前,
- 使用场景
- synchronized:
- 对于简单的同步需求,如方法内部的临界区同步,通常优先考虑使用
synchronized
,因为它简单、安全且性能不错。 - JDK 中的大部分类库都是使用
synchronized
进行同步的,比如ArrayList
、HashMap
等。
- 对于简单的同步需求,如方法内部的临界区同步,通常优先考虑使用
- ReentrantLock:
- 需要更高级功能的同步,比如可定时的、可中断的锁、公平锁和非公平锁的选择等,可以考虑使用
ReentrantLock
。 - 在需要细粒度控制锁的获取和释放时,或者需要使用
Condition
来进行线程间的协作时,适合使用ReentrantLock
。
- 需要更高级功能的同步,比如可定时的、可中断的锁、公平锁和非公平锁的选择等,可以考虑使用
综上所述,synchronized
是 Java 中最基本的线程同步方法,而 ReentrantLock
则提供了更多的高级功能和灵活性,适合复杂的并发控制需求。在实际开发中,根据具体的场景和需求选择合适的同步机制非常重要。
*Synchronized原理:*
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
*ReenTrantLock实现的原理:*
CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
CLH队列:带头结点的双向非循环链表
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。先用两张表格介绍一下AQS。第一个讲的是Node,由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点
13、HashMap实现原理
1、JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
2、新增数据和扩容方式
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
3、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
14、HashMap把链表转化为红黑树的阈值是8,而不是7也不是20?
理解HashMap在Java中如何工作以及为什么会选择在链表长度超过一定阈值时转换为红黑树,需要涉及以下几个关键点:
- HashMap 概述
HashMap是Java中常用的数据结构,用于存储键值对。它通过哈希表实现,可以提供快速的插入、删除和查找操作。 - 哈希表的桶(Buckets)
HashMap内部由一个数组(称为桶)组成,每个桶用来存储具有相同哈希码的键值对。当我们将一个键值对插入HashMap时,首先计算键的哈希码,然后根据哈希码确定存放位置。 - 解决哈希冲突
由于不同的键可能具有相同的哈希码(哈希冲突),因此每个桶实际上是一个链表或红黑树。当新的键值对需要插入到一个已经存在键的桶中时,新的键值对会添加到该桶对应的链表或红黑树中。 - 链表转换为红黑树的条件
为了提高HashMap在大量数据情况下的性能,JDK 8 引入了将链表转换为红黑树的优化策略。具体来说,当一个桶中的链表长度超过一定阈值时,HashMap会将这个链表转换为一个更高效的红黑树。这个阈值是8。 - 为什么选择阈值为8?
1.性能考虑:当链表长度较长时,查找、插入和删除操作的时间复杂度可能会变得较高(O(n),n为链表长度),转换为红黑树后,这些操作的时间复杂度可以降低到O(log n)。
2.空间利用:红黑树相比于链表会占用更多的空间,但当链表长度较长时,时间性能的提升通常会超过额外的空间消耗。
3.平衡考量:红黑树在高负载因子(大量元素存储在HashMap中)下更能保持平衡,从而提供更稳定的性能。
- 红黑树转换过程
一旦链表长度超过阈值8,HashMap会将该链表转换为一个红黑树。这个转换过程涉及重新计算哈希码、重新插入所有元素等操作,但由于红黑树的性能优势,这种转换对整体性能有积极的影响。 - JDK 8 之后的优化
JDK 8中还对HashMap进行了其他优化,例如引入了树化与树退化的机制,以及对键值对的查找逻辑进行了优化,这些都是为了在各种负载因子下提供更好的性能和稳定性。
总之,HashMap在设计上综合考虑了时间复杂度、空间利用和性能稳定性,通过合理选择链表转换为红黑树的阈值,有效地提高了在大数据量情况下的操作效率。
15、哈希表如何解决Hash冲突, key若Object类型,怎么办?
哈希表在解决哈希冲突时通常会使用以下几种方法,不论是什么类型的键(Object类型或其他):
解决哈希冲突的方法:
1.链地址法(Separate Chaining):
2.这是最常见的解决冲突的方法之一。
3.每个桶(存储位置)维护一个链表或者更优化的红黑树结构,用于存储哈希冲突的键值对。
4.当发生哈希冲突时,新的键值对被插入到对应桶的链表或红黑树中。
5.Java的HashMap在JDK 8之后,对链表长度超过一定阈值(默认为8)的桶会将链表转换为红黑树,以提高查找效率。
6.开放地址法(Open Addressing):
7.在这种方法中,所有的数据项都存放在哈希表的桶数组中,而不是单独的链表或树结构。
8.当发生哈希冲突时,根据特定的探测序列(如线性探测、二次探测等),在其他的空桶中寻找可以存放该数据项的位置。
9.这种方法要求在哈希表中有足够的空桶,以保证能够在冲突发生时找到合适的位置。
Key为Object类型的情况:
如果键的类型是Object类型(比如自定义的类),Java中通常会依赖该对象的hashCode()方法和equals()方法来计算哈希值和比较键的相等性。具体步骤如下:
10.计算哈希值:
11.调用键对象的hashCode()方法来获取哈希码。
12.hashCode()方法的实现需要保证对于相等的对象返回相同的哈希码,但不同的对象也可以返回相同的哈希码(这就是哈希冲突)。
13.确定存储位置:
14.根据哈希码计算出存储位置(桶的索引)。
15.如果该位置已经有其他键值对,则需要使用上述的解决冲突方法来处理。
16.处理冲突:
17.如果发生了哈希冲突,HashMap会根据具体的实现选择合适的解决冲突方法。
18.对于链地址法,将键值对添加到对应桶的链表或红黑树中。
19.对于开放地址法,根据探测序列寻找下一个可用的空桶。
总结来说,无论键的类型是什么,在哈希表中都需要解决哈希冲突。Java中的HashMap通过链地址法来解决冲突,并且对于Object类型的键,依赖对象的hashCode()和equals()方法来计算哈希值和比较键的相等性。
16、ArrayList实现原理?
ArrayList 是 Java 中常用的动态数组实现,它的实现原理如下:
1.内部数组存储:
ArrayList 内部通过一个数组来存储元素,数组的默认初始化大小是 10。
如果元素数量超过了当前数组的容量,ArrayList 会进行扩容操作。扩容的策略是创建一个新的更大容量的数组,并将旧数组中的元素复制到新数组中。
4.动态扩容:
当添加新元素导致当前数组容量不足时,ArrayList 会执行扩容操作。
扩容的大小通常是当前容量的 1.5 倍(在一些实现中也有不同的倍数,但通常不会是线性增长),以减少频繁扩容的开销。
.扩容涉及数组的复制操作,时间复杂度为 O(n),其中 n 是当前 ArrayList 中的元素数量。
8.随机访问:
通过数组下标进行快速访问,时间复杂度为 O(1)。
因为内部是数组实现,所以支持通过索引快速访问和修改元素。
插入和删除:
在尾部插入元素的时间复杂度为 O(1),因为不涉及移动其他元素。
在中间或头部插入元素则涉及到将插入点后的元素向后移动,时间复杂度为 O(n)。
删除操作类似,尾部删除的时间复杂度为 O(1),而删除中间或头部的操作涉及到元素的移动,时间复杂度也是 O(n)。
15.迭代器:
ArrayList 提供了迭代器 Iterator 接口的实现,可以通过迭代器遍历 ArrayList 中的元素。
迭代器的操作是基于数组的索引,因此迭代访问的时间复杂度也是 O(n)。
18.线程不安全:
ArrayList 不是线程安全的,不适合在多线程环境下进行并发操作。如果需要在多线程环境中使用,可以考虑使用线程安全的 Vector 或者通过 Collections.synchronizedList() 方法包装实现线程安全的 List。
总结来说,ArrayList 的实现基于数组,通过动态扩容和数组复制来实现动态大小。它适合于需要频繁随机访问元素和尾部插入、删除元素的场景,但对于频繁插入、删除中间元素的操作效率较低。
17、ConcurrentHashMap实现原理?
ConcurrentHashMap
是 Java 中线程安全的哈希表实现,它的实现原理相比于普通的 HashMap
更为复杂和精细化,主要目的是支持高并发的读写操作而不需要显式的同步措施。以下是 ConcurrentHashMap
的主要实现原理和特点:
- 分段锁(Segmentation)
ConcurrentHashMap
内部将数据结构分成多个段(Segment),每个段类似于一个小的HashMap
,拥有自己的锁。- 初始时,
ConcurrentHashMap
包含多个段,每个段中包含一部分桶(buckets)。 - 目的是通过减小锁的粒度,使得多个线程可以并发地操作不同的段,从而提高并发性能。
- 桶(Buckets)
- 每个段中包含多个桶(buckets),每个桶存储多个键值对。
- 桶的数量是可以动态调整的,可以根据实际需求进行扩展。
- put 操作
- 对于
put
操作,会先根据键的哈希值确定所属的段,然后对该段加锁。 - 在加锁的段内,会根据键的哈希值确定存放位置,将键值对存放到对应的桶中。
- 如果需要扩展桶的数量或者段的数量,会涉及到复制原有数据结构的操作。
4. get 操作
- 对于
get
操作,同样先根据键的哈希值确定所属的段,然后对该段加锁。 - 在加锁的段内,根据键的哈希值找到对应的桶,然后查找桶中的键值对。
ConcurrentHashMap
在读取时可以不加锁,但可能会读取到更新中的数据(弱一致性),因此可能需要通过 volatile 或者 CAS(Compare and Swap)操作来确保读取的正确性。
5. remove 操作
- 对于
remove
操作,同样需要先定位所属的段并加锁。 - 然后在锁定的段内进行删除操作,需要注意处理可能的冲突和扩容情况。
6. CAS 和 volatile
ConcurrentHashMap
内部使用了 CAS 和 volatile 等机制来保证在并发环境下的数据一致性和更新操作的原子性。
7. 扩容
ConcurrentHashMap
在扩容时不会复制整个数据结构,而是仅复制需要扩容的段,这样可以减小扩容的开销和影响。- 扩容时,每次只会处理一个段,而其他段仍然可以被访问,因此整体的并发性能得到了提高。
8. 性能和并发度
ConcurrentHashMap
的设计旨在提高并发度和性能,特别适用于读多写少的场景。- 由于分段锁的设计,多个线程可以同时访问不同段,因此在多线程并发读写时能够提供比普通
HashMap
更好的性能。
总结来说,ConcurrentHashMap
的实现利用了分段锁和精细化的操作来支持高并发环境下的安全操作,是Java中线程安全且高效的哈希表实现之一。
18、java8新特性?
Java 8 引入了许多新特性和改进,主要集中在语言、库和工具方面,其中一些最显著的新特性包括:
- Lambda 表达式
Lambda 表达式是 Java 8 中最重要的新特性之一,它提供了一种简洁而功能强大的方法来传递和使用匿名函数。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
- Stream API
Stream API 提供了一种更简洁、更易读的方式来处理集合数据。它支持函数式编程的风格,可以进行过滤、映射、归约等操作。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * 2)
.sum();
- 接口的默认方法和静态方法
接口现在可以包含默认方法(default methods),这些方法可以在接口中直接定义实现,而不需要所有实现类重新实现。还引入了静态方法。
public interface MyInterface {
default void defaultMethod() {
System.out.println("Default method implementation");
}
static void staticMethod() {
System.out.println("Static method in interface");
}
}
4、方法引用
方法引用提供了一种更简洁地调用现有方法的语法,支持静态方法、实例方法和构造函数的引用。
Function<String, Integer> parseIntFunction = Integer::parseInt;
5、新的日期和时间 API
java.time 包提供了全新的日期和时间 API,解决了旧 java.util.Date 和 java.util.Calendar 的不足,设计更为清晰和易用。
LocalDate today = LocalDate.now();
LocalDateTime dateTime = LocalDateTime.of(2024, Month.JUNE, 17, 10, 30);
6、CompletableFuture
CompletableFuture 是一种新的异步编程机制,支持非阻塞的回调风格编程,能够更方便地处理异步任务和组合多个异步操作。
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println);
7、函数式接口
新增了 @FunctionalInterface 注解,用于标记函数式接口,接口中只能有一个抽象方法。
@FunctionalInterface
interface MyFunctionalInterface {
void myMethod();
}
8、默认方法允许在接口中提供方法实现。
19、TCP的三次握手和四次挥手?
TCP 三次握手(Three-Way Handshake)
TCP 三次握手是建立一个 TCP 连接的过程,确保双方都能够同步初始化序列号(Sequence Number)和确认通信双方的能力。
- 第一步:客户端发送 SYN 报文
- 客户端(Client)向服务器(Server)发送一个特殊的TCP报文段,其中SYN标志位被置为1,表明客户端希望建立连接。
- 客户端选择一个初始序列号(Sequence Number),用来标识从客户端到服务器的数据。
- 第二步:服务器响应 SYN-ACK 报文
- 如果服务器愿意建立连接,它会回复一个TCP报文段,其中SYN和ACK标志位都被置为1。
- 服务器选择自己的初始序列号,同时确认客户端的SYN报文。
- ACK标志表示确认号(Acknowledgement Number),确认服务器收到了客户端的SYN报文。
- 第三步:客户端发送 ACK 报文
- 最后,客户端向服务器发送另一个TCP报文段,其中ACK标志位被置为1。
- 客户端确认了服务器的SYN-ACK报文,同时服务器的序列号也被确认。
- 至此,TCP连接建立完成,双方可以开始传输数据。
TCP 四次挥手(Four-Way Handshake)
TCP 四次挥手是安全地关闭一个TCP连接,确保数据能够完整传输和释放连接资源。
- 第一步:客户端发送 FIN 报文
- 客户端决定关闭连接时,发送一个TCP报文段,FIN标志位被置为1。
- 客户端不再发送数据,但仍可以接收数据。
- 第二步:服务器响应 ACK 报文
- 服务器收到客户端的FIN报文后,发送一个TCP报文段作为确认,ACK标志位被置为1。
- 服务器确认收到了客户端的关闭请求,但此时可能还有数据需要发送给客户端。
- 第三步:服务器发送 FIN 报文
- 当服务器所有数据都发送完毕后,会发送另一个TCP报文段,FIN标志位被置为1。
- 服务器希望关闭连接,不再发送数据。
- 第四步:客户端响应 ACK 报文
- 最后,客户端收到服务器的FIN报文后,发送一个TCP报文段作为确认,ACK标志位被置为1。
- 客户端确认了服务器的关闭请求,并进入TIME-WAIT状态。
- TIME-WAIT状态是为了确保最后一个ACK报文能够被服务器收到,以及处理可能出现的延迟报文段。
通过TCP四次挥手,双方确认了数据传输完毕,并释放了连接资源,完成了TCP连接的安全关闭过程。
20、TCP相关面试题?
*连接是三次握手关闭却是四次握手?*
当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
*TIME_WAIT状态经过2MSL才能返回到CLOSE状态?*
四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文
*不能用两次握手连接?*
3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
*如果建立了连接,但是客户端突然出现故障了怎么办?*
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
21、生产环境排查定位思路
熟悉linux基本命令,排查日志你首先要知道关键字,比如产品ID等等ID,
命令|搜索关键字|定位到日志信息log|看堆栈信息|分析具体异常原因(结合你实际问题场景)
如果是分布式系统,有一个关键字叫traceId,为每次请求分配一个流水号traceId,在日志打印处加上这个traceId,模块调用时亦将traceId往下传,直至整条消息处理完成,返回时清除traceId
模块内部日志串联:在网关接到订单时生成唯一流水号traceId,并将其放入threadLocal线程上下文里面,修改日志包在输出时取出threadLocal的traceId,一同输出,这样模块内部的同个请求的日志便可以串联起来;(也可以直接使用org.slf4j.MDC进行设置)
模块之间调用传递traceId:由于项目使用的是阿里的dubbo框架,利用dubbo的RpcContext分別实现DubboConsumerFilter和DubboProviderFilter,前者负责将traceId放入RPC上下文,后者则取出traceId放入threadLocal;
22、Spring Bean的生命周期?
Spring Bean 的生命周期包括以下几个关键阶段:
- 实例化(Instantiation):
- Spring 根据配置或注解创建 Bean 的实例。
- 依赖注入(Dependency Injection):
- Spring 将依赖注入到 Bean 的相应属性中。
- 初始化方法调用(Initialization):
- 如果 Bean 实现了
InitializingBean
接口,或者在方法上标注了@PostConstruct
注解,Spring 将调用其初始化方法。
- 如果 Bean 实现了
- Bean 可用(Ready for Use):
- Bean 初始化完成,可以被应用程序使用。
- 销毁方法调用(Destruction):
- 如果 Bean 实现了
DisposableBean
接口,或者在方法上标注了@PreDestroy
注解,Spring 在销毁 Bean 之前会调用其销毁方法。
- 如果 Bean 实现了
Spring 容器负责管理整个生命周期过程,确保 Bean 的依赖注入、初始化和销毁都按照配置和规范进行。
23、Spring的核心接口
*BeanFactory*
BeanFactory接口负责创建和分发各种类型的Bean。
在Spring中有几种BeanFactory的实现,其中最常用的是org.springframework.bean.factory.xml.XmlBeanFactory。它根据XML文件中的定义装载Bean。
要创建XmlBeanFactory,需要传递一个InputStream对象给构造函数,用来提供XML文件给工厂
*ApplicationContext*
ApplicationContext(应用上下文)继承自BeanFactory,表面上看两者功能差不多,都是载入Bean定义信息,装配Bean,根据需要分发Bean,但是ApplicationContext提供了更多功能:
1、应用上下文提供了文本信息解析工具,包括对国际化的支持
2、应用上下文提供了载入文本资源的通用方法,如载入图片
3、应用上下文可以向注册为监听器的Bean发送事件
在ApplicationContext的诸多实现中,有如下三个常用的实现。
ClassPathXmlApplicationContext:从类路径中的XML文件载入上下文定义信息,把上下文定义文件当成类路径资源。
FileSystemXmlApplicationContext:从文件系统中的XML文件载入上下文定义信息
XmlWebApplicationContext:从Web系统中的XML文件载入上下文定义信息
24、SpringMVC的运行流程?
Spring MVC 的请求流程可以简要概括为以下几个步骤:
- 请求到达 DispatcherServlet:
- 客户端的请求首先被 DispatcherServlet 接收。
- HandlerMapping 定位处理器(Controller):
- DispatcherServlet 通过 HandlerMapping 确定请求对应的处理器(Controller)。
- 处理器执行(Controller):
- 根据 HandlerMapping 的映射结果,DispatcherServlet 调用相应的 Controller 处理请求,并执行相关的业务逻辑。
- ModelAndView 的生成:
- Controller 处理完成后,返回一个 ModelAndView 对象,其中包含处理结果数据以及视图名称。
- ViewResolver 解析视图:
- DispatcherServlet 通过 ViewResolver 解析视图名称,确定最终的视图对象。
- 视图渲染:
- 最终的视图对象负责渲染返回给客户端的内容,通常是 HTML 页面或者其他类型的响应数据。
- 响应返回给客户端:
- 渲染后的视图内容或者响应数据返回给客户端,完成请求处理过程。
在这个过程中,DispatcherServlet 充当中央调度器的角色,负责协调整个请求的处理流程,从接收请求到最终的响应返回,整个过程利用了各种配置组件(如 HandlerMapping、Controller、ViewResolver 等)来实现请求的转发和处理。
25、ThreadLocal具体怎么使用?使用在什么场景?
ThreadLocal 是 Java 中的一个类,它提供了线程局部变量的功能。具体来说,ThreadLocal 实例通常被用来在每个线程中存储一些数据,使得这些数据对于线程是独立的,线程之间互不干扰。
如何使用 ThreadLocal?
-
创建 ThreadLocal 变量:
javaprivate static ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();
-
向 ThreadLocal 设置值:
javathreadLocal.set(new MyObject());
-
从 ThreadLocal 获取值:
javaMyObject obj = threadLocal.get();
-
清理 ThreadLocal 中的值(可选):
threadLocal.remove();
ThreadLocal是线程本地存储,在每个线程中都创建了一个ThreadLocalMap对象,
每个线程可以访问自己内部ThreadLocalMap对象内的value
经典的使用场景是为每个线程分配一个JDBC连接Connection。
这样就可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection
还有Session管理等问题,在线程池中线程的存活时间太长,往往都是和程序同生共死的
这样Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用(WeakReference)
所以只要ThreadLocal结束了自己的生命周期是可以被回收掉的。
Entry中的Value是被Entry强引用的,即便value的生命周期结束了,value也是无法被回收的,导致内存泄露
标准应用是在finally代码块中手动清理ThreadLocal中的value,调用ThreadLocal的remove()方法
26、了解反射吗?怎么用?用在哪里?
反射(Reflection)是 Java 编程语言的一个重要特性,它允许在运行时检查和操作类、方法和属性。通过反射,可以在程序运行时获取类的信息、调用对象方法、操作字段等,而不需要在编译时就确定这些操作。
反射的基本概念:
- 获取 Class 对象:
- 反射的起点是获取类的 Class 对象。可以通过类的
.class
属性、Class.forName()
方法或者对象的.getClass()
方法获取。
- 反射的起点是获取类的 Class 对象。可以通过类的
- 操作类信息:
- 可以通过 Class 对象获取类的名称、修饰符、父类、实现的接口等信息。
- 操作字段(Field):
- 可以获取类中定义的字段信息,包括字段名称、类型、修饰符,并能够动态修改字段的值。
- 操作方法(Method):
- 可以获取类中定义的方法信息,包括方法名称、参数类型、返回类型、修饰符,并能够动态调用这些方法。
- 操作构造方法(Constructor):
- 可以获取类中定义的构造方法信息,并能够动态创建对象实例。
反射的用法:
- 框架和库的开发:
- 许多框架和库(如 Spring、Hibernate)利用反射来实现自动化配置、依赖注入、ORM(对象关系映射)等功能,使得程序的结构更加灵活和易于扩展。
- 动态代理:
- 反射可以用于创建动态代理,这在 AOP(面向切面编程)中特别有用,能够在不修改原有代码的情况下,为类动态地添加额外的行为。
- 序列化和反序列化:
- Java 的序列化机制就是利用反射来实现的,它可以在对象和字节流之间进行相互转换,用于对象的持久化和网络传输。
- 单元测试:
- 在单元测试中,反射可以帮助测试框架动态地调用私有方法、设置私有字段的值,以进行更全面和深入的测试。
- 工具和调试器:
- 开发工具和调试器通常使用反射来提供关于类、对象和方法的详细信息,帮助开发人员理解和调试代码。
示例:
javaimport java.lang.reflect.*;
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 获取 Class 对象
Class<?> clazz = Class.forName("java.lang.String");
// 获取类的名称
System.out.println("Class Name: " + clazz.getName());
// 获取类的所有公共方法并输出
Method[] methods = clazz.getMethods();
System.out.println("Public Methods:");
for (Method method : methods) {
System.out.println(method.getName());
}
// 创建对象实例(通过默认构造方法)
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println("New Instance: " + obj);
}
}
上述示例演示了如何使用反射获取类的信息、调用类的方法以及创建对象实例。反射在需要在运行时动态获取和操作类信息的场景中特别有用,但也需要注意其对性能的影响和潜在的安全性问题。
27、HTTP状态码、3xx和4xx区别,404是啥?
HTTP状态码是在客户端与服务器进行通信时返回的一种状态指示。它们分为几个类别,其中包括:
- 1xx:信息响应 - 表示接收到请求并且正在处理。
- 2xx:成功 - 表示请求已成功被服务器接收、理解、并接受。
- 3xx:重定向 - 表示需要客户端进一步的操作才能完成请求。
- 4xx:客户端错误 - 表示客户端发送了无效的请求,服务器无法处理。
- 5xx:服务器错误 - 表示服务器在处理请求的过程中发生了错误。
现在来详细解释 3xx 和 4xx 类别以及 404 状态码:
3xx 重定向
3xx 状态码指示客户端需要执行额外的操作来完成请求。一些常见的 3xx 状态码包括:
- 301 Moved Permanently:永久重定向,请求的资源已永久移到新位置。
- 302 Found:临时重定向,请求的资源暂时移到新位置。
- 303 See Other:告知客户端应使用 GET 方法获取资源。
- 307 Temporary Redirect:与 302 类似,但明确要求客户端使用相同的 HTTP 方法进行重定向。
4xx 客户端错误
4xx 状态码表示客户端发送了无效的请求,服务器因此无法处理。一些常见的 4xx 状态码包括:
- 400 Bad Request:请求无效,服务器不理解客户端的请求。
- 401 Unauthorized:请求未经授权,需要进行身份验证。
- 403 Forbidden:服务器理解请求,但拒绝执行请求。
- 404 Not Found:请求的资源不存在于服务器上。
404 Not Found
404 Not Found 是 HTTP 中最常见的 4xx 状态码之一。它指示客户端请求的资源在服务器上不存在。可能的原因包括:
- 客户端请求了一个不存在的 URL 路径。
- 请求的资源被删除或者从服务器移动到了新的位置,但客户端未能更新请求。
- 服务器配置错误导致资源无法被找到。
通常情况下,当浏览器或其他客户端尝试访问一个不存在的页面时,服务器就会返回 404 错误。这种状态码帮助客户端知晓他们请求的资源无法被找到,从而采取适当的行动,比如显示自定义的错误页面或者重新定位用户。
5xx 状态码表示服务器在处理请求时发生了错误。以下是一些常见的 5xx 状态码及其含义:
- 500 Internal Server Error:通用的服务器端错误,服务器遇到了意料之外的情况,无法完成请求。
- 501 Not Implemented:服务器不支持客户端请求的功能或请求的方法。
- 502 Bad Gateway:作为网关或代理服务器的服务器,从上游服务器收到一个无效的响应。
- 503 Service Unavailable:服务器目前无法处理请求(可能是临时的过载或维护)。
- 504 Gateway Timeout:作为网关或代理服务器的服务器,在等待上游服务器的响应时超时。
这些状态码都表明了服务器在尝试处理请求时遇到了问题,可能需要服务器管理员进行调查和修复。通常情况下,这些错误需要服务器端的干预来解决,以便客户端可以重新尝试请求或者采取其他适当的措施。
28、线程池的类型和区别及拒绝策略?
线程池是一种管理和复用线程的机制,它可以优化多线程应用程序的性能和资源利用率。不同类型的线程池可以根据其特性和行为进行分类,同时线程池在处理任务时可能会面临任务拒绝的情况,这时需要定义拒绝策略。
1. 线程池的类型和区别:
固定大小线程池 (FixedThreadPool):
- 固定大小的线程池会预先创建固定数量的线程,这些线程在整个生命周期中都存在,如果某个线程因为执行任务失败而终止,线程池会补充一个新的线程来替代它。
- 适合于负载稳定的情况,能够提供较好的线程资源控制。
可缓存线程池 (CachedThreadPool):
- 可缓存线程池会根据需求动态地创建新线程,如果线程空闲时间超过设定的超时时间(如60秒),则被终止并移出线程池。
- 适合于处理大量短期异步任务的情况,能够灵活地调整线程数量。
单线程化线程池 (SingleThreadExecutor):
- 单线程化线程池只会使用一个工作线程来执行任务,保证所有任务按顺序执行。
- 适合于需要顺序执行任务并且线程不需要并发的情况。
定时线程池 (ScheduledThreadPool):
- 定时线程池可以延迟或定时执行任务,并且支持周期性执行任务。
- 适合于需要定时执行任务的场景,比如定时任务调度。
2. 拒绝策略(Rejected Execution Policy):
线程池在任务队列已满或者达到线程上限时,可能会拒绝接收新的任务。这时就需要定义拒绝策略来处理这种情况:
- AbortPolicy(默认策略):直接抛出 RejectedExecutionException 异常,表示拒绝执行新任务。
- CallerRunsPolicy:让提交任务的线程自己去执行该任务,相当于任务提交者自己执行该任务。
- DiscardPolicy:直接丢弃被拒绝的任务,没有任何处理。
- DiscardOldestPolicy:丢弃最早进入队列的任务(即队列头部的任务),然后尝试重新提交被拒绝的任务。
总结:
选择合适的线程池类型取决于具体的应用场景和需求,例如任务的性质、执行的频率、对资源的控制需求等。拒绝策略则可以根据业务需求和系统特性进行选择,以保证系统在高负载或异常情况下的稳定性和可靠性。
29、Http和Https区别,Https的加密过程?
HTTP:超文本传输协议 (HTTP-Hypertext transfer protocol),http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。
HTTPS协议可以分为两种:一是通过建立一个信息安全通道,来保证数据传输的安全;二是通过确认网站的真实性。
HTTPS在HTTP的基础上加入了SSL/TLS协议,依靠SSL证书来验证服务器的身份,并为客户端和服务器端之间建立“SSL加密通道”,确保用户数据在传输过程中处于加密状态,同时防止服务器被钓鱼网站假冒,而HTTP协议无法加密数据,所有通信数据都在网络中明文“裸奔”。通过网络的一些技术手段,就可还原HTTP报文内容。
30、线程池的工作原理、七个参数?
线程池的七个参数:
- 核心线程数(Core Pool Size):
- 线程池中始终保持存活的线程数量。即使这些线程是空闲的,它们也不会被回收,除非设置了线程存活时间。
- 最大线程数(Maximum Pool Size):
- 线程池中最多能容纳的线程数量。当任务队列已满,并且当前线程数小于最大线程数时,线程池会创建新的线程来处理任务。
- 线程存活时间(Keep Alive Time):
- 当线程池中的线程数量超过核心线程数时,多余的空闲线程在被回收之前等待新任务的最长时间。
- 任务队列(Work Queue):
- 用于存放等待执行的任务的队列。如果当前线程数达到核心线程数,并且任务队列已满,新提交的任务可能会被拒绝执行,或者采取其他的处理方式(如使用拒绝策略)。
- 拒绝策略(Rejected Execution Handler):
- 当任务无法被线程池接收执行时的处理策略。常见的拒绝策略包括直接抛出异常、丢弃任务、丢弃最旧的任务(即最先进入任务队列的任务),或者由提交任务的线程执行任务。
- 存活时间单位(Time Unit):
- 线程存活时间的单位,通常是秒、毫秒等。这个参数指定了在Keep Alive Time中指定的时间长度的单位。
- 线程工厂(Thread Factory):
- 用于创建新线程的工厂。默认情况下,线程池会使用默认的线程工厂来创建线程,但是你可以通过实现 ThreadFactory 接口来定义自己的线程创建方式,如设置线程的名称、优先级等。
线程池的工作原理:
线程池是一种用于管理和复用线程的机制,其核心目的是通过有效地管理线程的生命周期来提高系统的性能和资源利用率。下面是线程池的基本工作原理:
- 线程池的创建和初始化:
- 在程序启动或者需要使用线程池时,会创建一个线程池对象。在创建线程池时,需要指定一些参数,如核心线程数、最大线程数、任务队列等。
- 任务提交:
- 当有任务需要执行时,可以将任务提交给线程池。任务可以是实现了
Runnable
接口或Callable
接口的对象。
- 当有任务需要执行时,可以将任务提交给线程池。任务可以是实现了
- 任务的执行:
- 线程池会根据当前的状态和参数来决定如何执行任务:
- 如果当前运行的线程数小于核心线程数,线程池会立即创建一个新线程来执行任务。
- 如果运行的线程数达到核心线程数,并且任务队列未满,任务将被放入任务队列中等待执行。
- 如果任务队列已满,但是运行的线程数未达到最大线程数,则创建新线程来处理任务。
- 如果任务队列已满并且运行的线程数已达到最大线程数,根据指定的拒绝策略来处理新提交的任务,如抛出异常、丢弃任务等。
- 线程池会根据当前的状态和参数来决定如何执行任务:
- 任务执行和线程复用:
- 当线程池中的某个线程完成了一个任务后,它不会立即销毁,而是继续等待并执行队列中的下一个任务。这种机制避免了线程的频繁创建和销毁,节省了系统资源。
- 线程的回收:
- 如果线程池中的某个线程空闲时间超过了设定的存活时间(Keep Alive Time),则该线程可能会被销毁,以节省资源。
31、ACID是什么? 可以详细说一下嘛?
A = atomicity 原子性
就是保证事务的原子性,要么全部成功,要么全部失败,不可能只执行一部分操作
C = consistency 一致性
系统(数据库)总是从一个一致性状态转移到另一个一致性的状态,不会存在中间状态
I = isolation 隔离性
一个事务在完全提交之前,对其他事务都是不可见的
D = durability 持久性
一旦事务提交,那么就永远就是这样子了,哪怕系统崩溃也不会影响到这个事务的结果
32、怎么解决这些问题呢?MySQL 的事务隔离级别了解吗?
MySQL 的四种隔离级别如下:
未提交读( READ UNCOMMITTED )
这个隔离级别下,其他事务可以看到本事务没有提交的部分修改,因此就会造成脏读的问题,(读取到了其他事务未提交的部分,之后该事务进行了回滚) 这个隔离级别的性能没有足够大的优势,但是又有很多的问题,尽量不要使用!
已提交读 ( READ COMMITTED )
其他事务只能读取到本事务已经提交的部分 ,这个隔离级别 有 不可重复的的问题 在同一个事务内两次的读取 拿到的结果不一样,因为另一个事务对数据进行了修改。
可重复读 ( REPEATABLE READ)
可重复读 隔离级别解决了上面不可重复读的问题,但是仍然有一个新问题,就是幻读 ,当你读取 id>10 的数据时,对涉及到的所有行加上了锁,此时例外一个事务新插入了一条 id = 11 的数据时会发现有一条 id=11 的数据,而上次的查询操作没有获取到,再进行插入就会有主键冲突的问题,
串行化读/序列化读 ( SERIALIZABLE )
这是最高的隔离级别,可以解决上面的所有的问题,因为他有强制将所有的操作串行化执行。这会导致并发性能急速下降,因此也不是很常用。
33、MySQL 都有哪些锁呢?
从锁的类别上讲,有共享锁和排他锁。
**共享锁:**又叫读锁,当用户进行数据读取的时候,对数据加上共享锁,共享锁可以同时加上多个。
**排他锁:**又叫写锁,当用户要进行数据的写入时,对数据加上排他锁,排他锁只可以加一个,他和其他的排他锁,共享锁都互斥。
用上面的例子来说,就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。一种是真正的入住一晚,在这期间无论是想入住,还是看房都不可以的。
锁的粒度取决于具体的存储殷勤,InnoDB 实现了行级锁,页级锁,表级锁。
他们的加锁开销从大到小,并发能力也是从大到小。
34、MySQL支持哪些存储引擎
MySQL 支持多种存储引擎,比如InnoDB,MyISAM,Memory,Archive 等等,在大多数的情况下,直接选择使用 InnoDB 引擎就是最合适的,InnoDB也是MySQL 的·默认存储引擎。
InnoDB 和 MyISAM 的区别:
InnoDB支持事务,但是MyISAM不支持事务
InnoDB支持行级锁,页级锁,表级锁,但是MyISAM支持表级锁
InnoDB支持MVCC(多版本并发控制),但是MyISAM不支持
InnoDB支持外键,但是MyISAM不支持外键
InnoDB不支持全文索引,但是MyISAM支持全文索引
35.数据库超大分页怎么处理?
在查询时如果使用 limit 分页查询,在查询时,越往后,分页查询的效率就会越低,因为,在进行分页查询的时候,如果执行 Limit 900000,10 此时需要MySQL排序前90000010条数据,但是仅仅返回900000-9000010的记录,代价会很大。
我们可以通过覆盖索引 加上 子查询来解决的
先通过 分页查询数据的 id 字段,确定了id之后,用子查询来过滤,只查询这个id列表中的数据就可以了,因为查 id 的时候,走的覆盖索引,不会回表查询,所以效率就会提升很多。
select * from tb_sku t
(select id from tb—sku order by id limit 900000,10) a
where t.id = a.id
36、统计过慢查询吗?对慢查询怎么优化过?
a、错误日志:记录启动、运行或停止mysqld时出现的问题。
b、通用日志:记录建立的客户端连接和执行的语句。
c、更新日志:记录更改数据的语句。该日志在MySQL 5.1中已不再使用。
d、二进制日志:记录所有更改数据的语句。还用于主从复制。
e、慢查询日志:记录所有执行时间超过long_query_time秒的所有查询或不使用索引的查询。
f、Innodb日志:innodb redo log
MySQL会记录下查询超过指定时间的语句,我们将超过指定时间的SQL语句查询称为慢查询,都记在慢查询日志里.
我们开启后可以查看究竟是哪些语句在慢查询开启慢查询日志.
分析日志 – mysqldumpslow
分析日志,可用mysql提供的mysqldumpslow,使用很简单,参数可–help查看
推荐用分析日志工具 – mysqlsla
优化:
1、索引没起作用的情况
使用LIKE关键字的查询语句
使用多列索引的查询语句
2、优化数据库结构
将字段很多的表分解成多个表
增加中间表
3、分解关联查询
将一个大的查询分解为多个小查询是很有必要的
4、优化LIMIT分页
在系统中需要分页的操作通常会使用limit加上偏移量的方法实现,同时加上合适的order by 子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。
5、分析具体的SQL语句
37、什么是存储过程?有哪些优缺点?
存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合
优点:
存储过程是一个预编译的代码块,执行效率比较高
存储过程在服务器端运行,减少客户端的压力
允许模块化程序设计,只需要创建一次过程,以后在程序种就可以调用该过程任意次,类似方法的服用
一个存储过程替代大量T_SQL语句,可以降低网络通信量,提高通信速率就
可以一定程度上确保数据安全
缺点:
调试麻烦 (没有开发程序那样容易)
可移植性不灵活 (因为存储过程依赖于具体的数据库)
38、说一说数据库三范式?
第一范式保证每一列的原子性
第二范式保证其他列对主键列的绝对依赖
第三范式保证,不能对主键的传递依赖。
39、Java 类加载的过程
JVM 将类描述数据 从 .class 文件中 加载到内存,并对数据进行,解析和初始化,最终形成被 JVM 直接使用的 Java 类型。类从被加载到 JVM 中开始,到卸载为止,整个生命周期包括:
加载、验证、准备、解析、初始化、使用、卸载七个阶段。
1、加载(Loading):类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对应哪个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。加载阶段由类加载器负责,过程见类加载器;简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对应哪个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。加载阶段由类加载器负责,过程见类加载器;
链接(Linking)链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
2、验证:验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
格式验证,验证是否符合class文件规范
语义验证,检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证,在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
3、准备:准备阶段负责为类中static变量分配空间,并初始化(与程序无关,系统初始化);被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值。
4、解析:解析阶段负责将常亮池中所有符号引用转换为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写);
5、初始化:对类的静态变量,静态代码块执行初始化操作初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值。
②使用静态代码块为类变量指定初始值。
40、Java 类的加载器有几种?
类加载器分类 四种
*启动类加载器/Bootstrap ClassLoader*****,****在HotSpot虚拟机中,Bootstrap ClassLoader用C++语言编写并嵌入JVM内部,主要负载加载JAVA_HOME/lib目录中的所有类,或者加载由选项-Xbootcalsspath指定的路径下的类.
*拓展类加载器/ExtClasLoader*****,****ExtClassLoader继承ClassLoader类,负载加载JAVA_HOME/lib/ext目录中的所有类型,或者由参数-Xbootclasspath指定路径中的所有类型.
*应用程序类加载器/AppClassLoader*****,****ExtClassLoader继承ClassLoader类,负责加载用户类路径ClassPath下的所有类型,一般情况下为程序的默认类加载器.
*自定义加载器*****,****Java虚拟机规范将所有继承抽象类java.lang.ClassLoader的类加载器,定义为自定义类加载器. Loc++
41、GC如何判断一个对象是否存活?
1、引用技术算法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器就 +1; 当引用失效时,计数器值就减1;任何时刻计数器为0 的对象就是不可能再被使用的。
2、可达性分析算法:
这算法的基本思想就是通过 一系列的成为 “GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain) 当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的.
42、Java中会存在内存泄漏吗。请简单描述?
内存泄漏 就是指一个不再被使用的对象或变量一直被占据在内存当中。Java中没有垃圾回收机制,它可以保证一个对象不再被引用的时候,对象将自动被垃圾回收器从内存中清除掉,由于 Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要他们和根进程不可达,那么GC就可以回收他们。java中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
检查java中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
43、深拷贝和浅拷贝
在编程中,深拷贝(deep Copy)和浅拷贝 (shallow Copy)是两种复制对象的方式,它们的主要区别就是在于如何处理对象中的引用类型的数据。
浅拷贝:
浅拷贝是创建一个新的对象,然后将原有对象的所有可枚举的属性值复制到新对象之中,对于基本数据类型(字符串、数字、布尔值等),会直接复制其值; 然而对于引用数据类型来说,复制的是其内存地址,即旧对象和通过浅拷贝出来的新对象都是共享同一个引用类型的实例。
浅拷贝可以通过 Cloneable 接口和 Object 类的 clone()方法来实现,特别注意(浅拷贝的新对象的引用数据类型和旧对象都是共享一个内存地址的)
public static void main(String[] args) throws CloneNotSupportedException {
List<Integer> list =new ArrayList<>();
list.add(2);
list.add(3);
Qiankaobei qiankaobei = new Qiankaobei();
qiankaobei.setName("小明");
qiankaobei.setAge(18);
qiankaobei.setSex("男");
qiankaobei.setSkills(list);
System.out.println("qiankaobei = " + qiankaobei); //注意不要改变String类型的,因为他是线程安全的常量
Qiankaobei qiankaobei1 = (Qiankaobei) qiankaobei.clone();
System.out.println("qiankaobei1 = " + qiankaobei1);
list.remove(1);
System.out.println("qiankaobei = " + qiankaobei);
System.out.println("qiankaobei1 = " + qiankaobei1);
}
深拷贝:
深拷贝不仅复制了对象的所有可枚举属性,而且递归地复制了每个属性中的对象,直到所有的数据都是独立的副本。也就是说,对于引用数据类型,深拷贝会重新创建一个新的实例,并且这个实例和原始对象中的实例不共享任何内存。
深拷贝可以通过序列化机制来实现,将对象转换为字节流再从字节流中读取出来,这样可以得到一个完全独立的新对象。
public static Shenkaobei deepclone(Shenkaobei shenkaobei) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(byteArrayOutputStream);
oss.writeObject(shenkaobei);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (Shenkaobei) objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("克隆失败");
}
}
public static void main(String[] args) {
Shenkaobei shenkaobei = new Shenkaobei();
shenkaobei.setAge(18);
shenkaobei.setName("小明");
shenkaobei.setSex("男");
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
shenkaobei.setSkills(list);
Shenkaobei deepclone = Shenkaobei.deepclone(shenkaobei);
System.out.println(deepclone);
list.remove(0);
System.out.println(shenkaobei);
System.out.println(deepclone);
}
44、finalize()方法什么时候调用,析构函数(finalization) 的目的?
fianlize() 方法是 Java 对象生命周期中的一个特殊方法,它属于 java.lang.Object 类,是垃圾回收机制的一部分。finalize() 方法 在对象即将被垃圾回收器回收之前被调用,提供了一次机会让对象在被永久地销毁之前,执行一些清理工作,比如释放资源,关闭文件句柄或者网络连接等。
但是并不可靠,不能依赖于 finalize()
方法一定会被调用,也不能控制它何时被调用。垃圾回收器可能会在任何时候回收对象,也可能根本不会调用 finalize()
方法。
45、Java内存分配与回收策略以及 Minor GC 和 Major GC?
Java内存模型主要包括以下几个部分:
①**堆(heap):**这是所有线程共享的内存区域,用于存储对象实例和数组。堆被划分为不同的代(Generations),通常包括年轻代(Young Generation)(三分之一)和老年代(Old Generation)(三分之二).
②年轻代(Young Generation):包含 Eden 空间和 两个 Survivor 空间(S0 和 S1).
新创建的对象首先会在Eden空间分配,当Eded空间满时,会发生一次 Minor GC。
③老年代(Old Generation):存储长时间存活的对象,当对象在年轻代经过多次GC之后,他们会被移动到老年代。
④永久代(Permanent Generation):存储类元数据、常量池等信息。在Java8中,永久代被元空间(Metaspace)取代,后者位于本地内存之中。
内存分配策略:
①TLAB(Thread Local Allocation Buffer)**:**每个线程有自己的缓冲区,用于快速分配对象,减少线程间的竞争。
②**对象分配:**小对象通常分配在 年轻代的 Eden 空间,大对象 (由JVM参数配置)直接在老年代分配。
垃圾回收(GC)
①Minor GC
- 触发条件:当 Eden 空间不足时,或者 Survivor 空间不足以存放 Eden 空间中存活的对象时,就会触发 Minor GC。
- 作用:主要就是在 年轻代 进行,清理不再使用的对象,将存活的对象从 一个 Survivor 空间复制到·另外一个 Survivor 空间 或者直接晋升到 老年代
②Major GC / Full GC
- 触发条件:当老年代空间不足时,或者系统显示调用 例如 Systm.gc() 时,就会触发 Major GC 但是在现代的JVM之中,显示调用 System.gc() 是不推荐的。
- 作用:清理整个堆内存,包括年轻代和老年代,通常比 Major GC 更加耗时,因为涉及到更多的内存区域
GC测率
GC测率(Garbage Collection Rate) 是指在 一定时间内,GC所消耗的时间占总运行时间的比例。高的 GC 测率就意味着 GC 活动频繁,可能回影响到应用程序的响应时间和吞吐率。优化GC测率通常涉及调整 JVM 参数,悬着合适的 GC 算法,以及减少对象的创建和提升对象的重用。
46、类加载双亲委派模型机制?
类加载双亲委派模型(Parent Delegation Model)是Java 类加载机制的核心部分,它确保了 Java 平台的稳定性和安全性。
双亲委派模型是一种类加载器之间的协作机制,其中每个类加载器都有一个父类加载器,当一个类加载器收到类加载请求时,他首先不会尝试加载该类,而是将请求委托给父类加载器,只有当父类加载器无法加载该类时,子类加载器才会去尝试加载。
类加载器层次结构
Java中主要有三种内置的类加载器:
- Bootstrap ClassLoader(启动类加载器):这是最顶层的类加载器,负责加载Java 核心类库(例如:rt.jar)它没有父类加载器。
- Extension ClassLoader(扩展类加载器):他继承自Bootstrap ClassLoader ,负责加载/lib/ext 目录下的类库。
- Application ClassLoader(应用程序类加载器):这是默认的类加载器,用于加载用户应用程序的类路径(ClassPath)上的类。
(应用程序还可以定义自己的自定义加载器,这些自定义类加载器通常继承自 java.lang.ClassLoader)
工作流程
当应用程序尝试加载一个类时,类加载请求将按照以下顺序处理:
- 请求委派给父加载器:应用程序类加载器接收到加载类的请求时,它不会立即加载类,而是将请求传递给父类加载器——扩展类加载器。
- 父加载器继续向上委派:如果扩展类加载器也无法加载,它将继续委派给Bootstrap ClassLoader。
- 到达顶层或成功加载:如果Bootstrap ClassLoader也无法加载,或者类在某个级别被成功加载,加载过程停止。
- 子加载器尝试加载:如果类没有在父级加载器中找到,请求将返回到原始的子加载器,此时子加载器将尝试加载该类。
双亲委派模型的好处
- 安全性:确保了核心类库不会被随意覆盖,防止恶意代码篡改核心类库、核心API库。
- 稳定性:确保了类的一致性,避免了类加载的冲突、重复,确保了Java平台的稳定运行。
- 可扩展性:允许应用程序定义自定义类加载器,同时保持了类加载机制的统一性和安全性。
47、什么是锁消除和锁粗化?
锁消除:
JVM在编译时期,通过对同步代码块进行分析,判断出某些锁实际上时没有必要存在的,从而在运行时完全去除掉这些锁的操作。锁消除可以显著减少,由于获取锁和释放锁带来的性能开销,尤其是当锁持有的时间很短时,这种开销尤为明显。
锁消除的原理基于:
- 不变性检测:如果 JVM 能够证明在一段代码中,某个对象的引用不会被外部修改,那么可以认为该对象不需要加锁。例如,局部变量、私有成员变量在方法体内时不可变的,可以进行锁消除。
- 线程封闭:如果一个对象只在一个线程内访问,那么也不需要加锁,例如:当一个线程在处理一个局部变量 或者 方法内的临时对象时,JVM 可以推断出无需加锁。
锁粗化:
锁粗化就是指将多个连续的细粒度 锁合并成一个更粗粒度的锁,从而减少锁的获取和释放次数,提高并发的性能。当多个连续的操作都对同一把锁进行加锁和解锁时,锁的开销可能会变大,通过锁粗化,JVM 可以将这些操作和并在一起,旨在开始处加锁,在结束处解锁,从而减少所得切换次数。
锁粗化使用场景:
- 连续的同步操作:如果一系列连续的操作都需要对同一个对象进行加锁,那么可以考虑这些操作合并到一个更大的代码块中,只在开始和结束处加锁。
- 频繁的上下文切换:锁粗化可以减少所得锁的获取和我释放频率,从而减少线程上下文切换的次数,提高整体的并发效率。
锁粗化和锁消除都是JVM层面的优化,通常不需要程序员手动干预。JVM的即时编译器(JIT Compiler)会在运行时自动分析代码并应用这些优化。不过,了解这些优化有助于编写更高效、更易于优化的代码,以及在性能调优时做出更好的决策。例如,避免不必要的锁,合理设计数据结构和算法,以减少锁的竞争和提升并发性能。
48、谈谈volatile 有什么特点?
volatile是一个关键字
主要有三个特性:可见性(Visbility)、禁止指令重排序(Prevent Instruction Reordering)、不保证原子性(Does Not Guarantee Atomicity)
-
可见性(Visibility):
当一个线程修改了volatile修饰的变量的值之后,新值对其他线程都是立即可见的。这也就意味着多线程的环境下,一旦一个线程修改了一个volatile变量的值,那么这个修改后的值将会被立即放映带所有的线程缓存之后,这保证了数据的最新状态,能在各个线程间传播,解决了数据一致性的问题。
-
禁止指令重排(Prevect Instruction Recording)
在编译器和处理器层面上,为了优化程序的执行效率,可能会对指令进行重排序。但是,如果涉及到volatile变量,那么编译器和处理器必须遵循变量的原本顺序,不能对其进行重排序,这保证了程序的执行顺序符合程序员的预期,避免了因指令重排导致的错误。
-
不保证原子性(Does Not Guarantee Atomicity):
虽然 volatile 关键字可以确保变量的可见性和禁止指令重排序,但是它不能保证对变量的所有操作都是原子性的。例如 对volatile变量 的简单读取和写入操作都是原子的,但是符合操作例如(i++ 或者 a = a+b )在多线程环境中可能不是原子的,因为这些操作涉及到了读取、计算、和写回多个步骤,为了保证符合操作的原子性,可以使用锁或者其他同步机制。
使用场景
volatile 关键字通常用于以下几种场景:
- 状态标记变量:例如,用于指示一个线程是否应该停止运行的标志变量,通常被声明为volatile。
- 单例模式中的双重检查锁定(Double -Checked Locking):在单例模式中,volatile 可以用来确保实例的正确初始化和可见性。
- 发布无锁队列中的节点:在无锁数据结构中,volatile 可以用来确保新节点的可见性和正确性。
49、请谈谈ThreadLocal 是怎么解决并发安全的?
ThreadLocal 是 Java中的一个类,位于 java.lang 包中,它提供了一种在每一个线程中拥有独立变量副本的机制。这使得 ThreadLocal 变量,在多线程的环境中可以避免数据竞争和同步问题,从而实现线程安全。
ThreadLocal的工作原理如下:
- 变量副本:
当你创建一个 ThreadLocal 变量时,实际上是在每个使用它的线程中创建了该变量的一个副本,这意味着每个线程都有自己的变量值,互不影响。 - 线程局部存储:
每个线程都有一个与之关联的 ThreadLocalMap,这个Map 用于存储该线程所有的 ThreadLocal 变量的副本。ThreadLocal 对象通过 ThreadLocalMap 来维护线程局部变量的映射关系。 - get() 和 set() 方法:
ThreadLocal 类提供了get() 方法来获取当前线程中特定 ThreadLocal 变量的值,以及 set() 方法设置当前线程中特定的 ThreadLocal 变量的值。这两个方法确保了操作发生在当前线程的局部副本上。 - 线程隔离:
由于每个线程都有独立的变量副本,因此一个线程对 ThreadLocal 变量的操作不会影响到其他线程的线程副本,这就意味着不需要加锁或者其他同步手段来防止线程间的干扰,从而简化了代码并提高了性能。 - 内存管理:
当线程结束时,ThreadLocalMap 中的条目也会被垃圾回收器回收,以避免内存泄漏,但是,如果ThreadLocal变量没有正确地调用remove() 方法清理不再需要的线程局部实例,可能会导致 ThreadLocalMap 中的条目成为孤儿,即为线程已经结束,这些条目也不会被垃圾回收,从而造成内存泄漏。 - 初始化:
当首次在一个线程之中访问某个ThreadLocal 变量时,如果该变量尚未在该线程中初始化,则会调用ThreadLocal 的initiaValue() 方法来获取初始化值(默认情况下返回null ,但可以通过重写此方法来提供自定义的初始值。)
通过以上机制,ThreadLocal
能够有效地解决多线程环境下的并发安全问题,同时避免了同步所带来的性能开销。
50、Tomcat性能调优?
Tomcat 是一个由 Apache 软件基金会开发的开源 Java Servlet 容器和 Web 服务器。它主要用于运行 Java Servlets 和 JavaServer Pages (JSP),使得开发者可以用 Java 编写动态网页。以下是 Tomcat 的一些关键点:
- 核心功能:
- Servlet 容器:运行 Java Servlets,处理 HTTP 请求和响应。
- JSP 引擎:解析和运行 JSP 页面,生成动态网页内容。
- 主要组件:
- Catalina:核心 Servlet 容器。
- Coyote:HTTP 连接器,支持 HTTP/1.1 协议。
- Jasper:JSP 引擎,将 JSP 编译为 Servlets。
- 配置与管理:
- 通过配置文件
server.xml
和web.xml
进行服务器和应用配置。 - 提供 Web 管理控制台,用于部署和管理 Web 应用。
- 通过配置文件
- 性能优化:
- 支持连接池、线程池、异步处理和 GZIP 压缩等优化技术。
- 使用场景:
- 常用于中小型 Java Web 应用的开发和部署。
- 在微服务架构中,作为轻量级的服务容器使用。
- 社区支持:
- 作为一个开源项目,Tomcat 拥有活跃的社区和丰富的文档资源。
总的来说,Tomcat 是一个轻量级、高效且灵活的 Java Web 服务器,非常适合开发和部署 Java Web 应用。
性能调优
在Tomcat性能调优方面,我有丰富的经验,主要从以下几个方面入手:
- 增加最大连接数:
- 通过调整
Connector
元素中的maxConnections
(最大连接数)和acceptCount
(接受队列长度)属性,来允许更多的并发连接。
- 通过调整
- 调整工作模式:
- 使用 NIO(Non-blocking I/O,非阻塞I/O)或 NIO2(新版非阻塞I/O)作为连接器的协议,比传统的 BIO(Blocking I/O,阻塞I/O)更高效。
- 采用 Async(Asynchronous,异步)模式来处理长时间运行的请求,以减少线程阻塞。
- 启用 GZIP 压缩:
- 在
web.xml
中启用 GZIP(GNU Zip 压缩算法)压缩,或者在server.xml
中为Connector
元素添加 compression 相关配置,减少网络传输的数据量,加快响应速度。
- 在
- 调整 JVM 内存大小:
- 设置合适的
Xms
(初始堆大小)和Xmx
(最大堆大小),以满足应用需求,避免频繁的垃圾收集。 - 根据 Java 版本,调整
PermSize
(永久代大小)和MaxPermSize
(最大永久代大小)或MetaspaceSize
(元空间大小)和MaxMetaspaceSize
(最大元空间大小),优化元空间的使用。
- 设置合适的
- 使用外部 Web 服务器:
- 将 Tomcat 与 Apache(Apache HTTP Server)或 Nginx(Nginx HTTP 服务器)等高性能 Web 服务器结合使用,通过反向代理处理静态内容和负载均衡。
- 优化 JSP 和 Servlet:
- 尽量减少 JSP(JavaServer Pages,Java服务器页面)页面数量和复杂度,优先使用 Servlet(Java Servlets,Java小程序)。
- 缓存 JSP 编译结果和静态资源。
- 数据库连接池优化:
- 使用高效的连接池如 C3P0(C3P0数据库连接池)或 HikariCP(Hikari Connection Pool,Hikari连接池),合理设置最大和最小连接数。
- 缓存策略:
- 实现有效的缓存机制,如使用 Ehcache(Ehcache缓存框架)或 Infinispan(Infinispan数据网格)。
- 减少日志级别:
- 在生产环境中,将日志级别设为 ERROR(错误日志级别)或 WARN(警告日志级别),以减少日志输出的开销。
- 禁用不必要的服务和模块:
- 关闭不使用的 Tomcat 服务和模块,减少启动时间和运行时资源消耗。
- 监控和分析:
- 使用 JMX(Java Management Extensions,Java管理扩展)、VisualVM(VisualVM性能监控工具)或 Profiler(性能分析器)等工具来监控应用和服务器性能,根据监控结果调整配置。
- 优化应用程序代码:
- 避免在循环中进行昂贵的数据库查询。
- 使用线程安全的集合和数据结构,减少对共享资源的访问,尽量使用 ThreadLocal(线程局部变量)变量。
- 硬件升级:
- 增加 CPU(Central Processing Unit,中央处理器)核心数、RAM(Random Access Memory,随机存取存储器)和更快的磁盘系统,可以显著提升性能。
通过这些调优措施,我能够有效提高Tomcat服务器的性能和稳定性,确保应用在高并发情况下依然能够流畅运行。
51、你们公司Git分支?
Git企业级应用:多分支
master -主支 正式版代码 每一次都是一次版本的更新
test -分支 测试分支 主要是测试工程师用来进行测试的 都是完整代码
dev -分支 研发分支 研发工程师每次上传和下来代码的分支
bug -分支 项目缺陷分支管理 主要是日常项目有bug的代码 主要是测试工程师的反馈
error -分支 紧急错误分支 一般都是线上代码出了bug 需要从master上同步代码
ZIP 压缩**:
- 在
web.xml
中启用 GZIP(GNU Zip 压缩算法)压缩,或者在server.xml
中为Connector
元素添加 compression 相关配置,减少网络传输的数据量,加快响应速度。
- 调整 JVM 内存大小:
- 设置合适的
Xms
(初始堆大小)和Xmx
(最大堆大小),以满足应用需求,避免频繁的垃圾收集。 - 根据 Java 版本,调整
PermSize
(永久代大小)和MaxPermSize
(最大永久代大小)或MetaspaceSize
(元空间大小)和MaxMetaspaceSize
(最大元空间大小),优化元空间的使用。
- 设置合适的
- 使用外部 Web 服务器:
- 将 Tomcat 与 Apache(Apache HTTP Server)或 Nginx(Nginx HTTP 服务器)等高性能 Web 服务器结合使用,通过反向代理处理静态内容和负载均衡。
- 优化 JSP 和 Servlet:
- 尽量减少 JSP(JavaServer Pages,Java服务器页面)页面数量和复杂度,优先使用 Servlet(Java Servlets,Java小程序)。
- 缓存 JSP 编译结果和静态资源。
- 数据库连接池优化:
- 使用高效的连接池如 C3P0(C3P0数据库连接池)或 HikariCP(Hikari Connection Pool,Hikari连接池),合理设置最大和最小连接数。
- 缓存策略:
- 实现有效的缓存机制,如使用 Ehcache(Ehcache缓存框架)或 Infinispan(Infinispan数据网格)。
- 减少日志级别:
- 在生产环境中,将日志级别设为 ERROR(错误日志级别)或 WARN(警告日志级别),以减少日志输出的开销。
- 禁用不必要的服务和模块:
- 关闭不使用的 Tomcat 服务和模块,减少启动时间和运行时资源消耗。
- 监控和分析:
- 使用 JMX(Java Management Extensions,Java管理扩展)、VisualVM(VisualVM性能监控工具)或 Profiler(性能分析器)等工具来监控应用和服务器性能,根据监控结果调整配置。
- 优化应用程序代码:
- 避免在循环中进行昂贵的数据库查询。
- 使用线程安全的集合和数据结构,减少对共享资源的访问,尽量使用 ThreadLocal(线程局部变量)变量。
- 硬件升级:
- 增加 CPU(Central Processing Unit,中央处理器)核心数、RAM(Random Access Memory,随机存取存储器)和更快的磁盘系统,可以显著提升性能。
通过这些调优措施,我能够有效提高Tomcat服务器的性能和稳定性,确保应用在高并发情况下依然能够流畅运行。
51、你们公司Git分支?
Git企业级应用:多分支
master -主支 正式版代码 每一次都是一次版本的更新
test -分支 测试分支 主要是测试工程师用来进行测试的 都是完整代码
dev -分支 研发分支 研发工程师每次上传和下来代码的分支
bug -分支 项目缺陷分支管理 主要是日常项目有bug的代码 主要是测试工程师的反馈
error -分支 紧急错误分支 一般都是线上代码出了bug 需要从master上同步代码