Java 基础面试题


异常

  1. throw和throws的区别
    throw用来抛出自定义异常,throws是在方法上抛出异常

  2. ArrayIndexOutOfBoundsException(数组下标越界异常)
    ArrayIndexOutOfBoundsException出现的原因:使用不合法的索引访问数组时,会报数组下标越界异常

  3. NullPointerException(空指针异常)
    NullPointerException 出现的原因:是一个运行时异常,调用对象上的方法但在运行的时候这个对象引用为 Null 时会引发该异常

  4. ClassCastException(类型转换异常)
    ClassCastException 出现的原因:是类型不匹配,常见于强制转换、集合操作、泛型和反射等场景


面向对象

  1. 什么是类?什么是对象?
  • 类是模板,定义对象的结构和行为。
  • ​对象是实例,基于类的模板创建,拥有具体的属性和方法。
  • ​面向对象编程通过类和对象,将现实世界中的事物抽象为代码,提高代码复用性、扩展性和维护性。
  1. 说一下面向对象三大特征:封装,继承,多态
  • 封装就是属性和方法写在一个类中,然后可以对外提供 set 和 get 方法
  • 继承就是子类继承父类的属性和方法(使用extends),主要是代码复用的手段
  • 多态的核心是灵活性,重载和重写都是多态的体现
    • 重载与重写的区别,作用?
      • 区别
        重写是父类与子类之间的多态;
        重载是在一个类中多态的体现;
      • 作用
        方法重写是子类的方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)
        方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)
  1. Java中创建对象的方式有几种?何时使用哪种方式?
  • 使用
    1. 使用new关键字
    2. 使用clone方法
    3. 反射机制
    4. 反序列化
  • 区别
    1,3都会明确的显式的调用构造函数
    2是在内存上对已有对象的拷贝 所以不会调用构造函数
    4是从文件中还原类的对象 也不会调用构造函数
  1. Java里可不可以有多继承?
  • 不可以,Java中不存在多继承,只存在多实现;多实现必须使用接口
  1. 抽象类与接口的区别,包括使用(什么情况使用抽象类,什么情况使用接口)
    ​抽象类:
    用于代码复用和部分通用逻辑抽象,解决“是什么”(What)的问题。
    示例:Animal 抽象类包含所有动物的共有属性(如 age)和行为(如 eat())。

​接口:
用于行为契约和多实现能力,解决“能做什么”(What can do)的问题。
示例:Comparable 接口定义 compareTo() 方法,约定实现类必须支持排序。

  1. 包装类的来历?
  • Java是面向对象的编程语言,所以包装类型为了解决基本数据类型无法面向对象编程所提供的
  1. 类的初始化过程
    Student s = new Student();
    上方代码在内存中做了什么

​类加载:Student 类被加载到方法区。
​栈内存:声明引用变量 s(栈中存储对象的地址)。
​堆内存:创建 Student 对象实例,分配内存并初始化。
​引用指向:变量 s 存储对象在堆内存中的地址。


泛型

  1. Java中的泛型是什么
    泛型(Generics)​ 是 Java 语言的一项核心特性,允许开发者通过类型参数编写通用代码,适配多种数据类型,同时确保类型安全。
    简单来说,泛型就是让代码在编译时就知道要操作的具体数据类型,而不是在运行时才确定
  2. 泛型的核心机制
  • ​类型参数(Type Parameters)​
    在类、接口或方法的声明中使用占位符(如 ),表示该类型可以适配任意数据类型。
    示例:class MyClass { … }、interface MyInterface { … }、public void myMethod(T param) { … }
  • ​类型推断(Type Inference)​
    Java 8+ 支持自动推断泛型类型,无需显式声明。
    示例:List list = new ArrayList<>();(编译器自动推断为 ArrayList
  • ​通配符(Wildcard)​
    使用 ? 表示未知类型,支持更灵活的泛型操作。
    示例:void process(List<?> list) { … }
  • ​边界(Bounds)​
    通过 extends 或 super 限制泛型类型的范围
  1. 泛型的优势
    类型安全 List 自动拒绝
    ​代码复用 一个 Box 类适配所有类型
    ​可读性高 processData(T data) 明确处理类型
    ​性能优化 编译时检查类型,减少运行时开销

  2. 使用推荐
    ​优先使用泛型集合​(如 List 而不是 List)。
    ​利用类型推断简化代码(如 new ArrayList<>())。
    ​合理使用通配符,避免过度复杂化类型边界。

  3. Java的泛型的类型擦除 ?
    ​原理:泛型类型在运行时会被擦除为 Object,但编译器通过桥方法(Bridge Methods)保留类型信息


String相关

  1. String类中常用的方法
  • split 将字符串拆分成字符串数组
  • indexOf 获取指定字符或字符串在当前字符串中首次出现的位置,没找到返回-1
  • substring 截取字符串,并返回新的字符串
  • replace(): 替换字符串内匹配的子字符串
  • equals(): 比较字符串的内容是否相同
  • concat(): 将指定字符串连接到此字符串的结尾
  1. String中的==和equals的区别
  • == 默认比较的是地址值是否相同
  • equals比较的也是地址值是否相同,它的内部就是使用了==,但String中的equals进行了重写,比较的是内容是否相同
  1. Java中的String,StringBuilder,StringBuffer三者的区别
  • String是长度不可变的字符序列;一旦创建,内容不能改变;主要用于少量的字符串操作
  • StringBuilder类是一个长度可变的字符序列;线程不安全,效率高;append方法拼接内容,不会创建新的对象
  • StringBuffer类是一个长度可变的字符序列;线程安全,效率低;append方法拼接内容,不会创建新的对象
  1. String s = “hello” 和 String s = new String(“hello”)的区别?
    String s = “hello”; ​
  • 是一个字符串字面量(”hello”)直接存储在方法区的字符串池中,​复用已有的对象。
    • ​高效:直接复用对象,减少内存分配和垃圾回收压力,​省内存:字符串池存储唯一实例
      String s = new String(“hello”);

  • 通过 new 动态创建一个新的 String 对象,存储在堆内存中,​无论字符串池是否存在相同内容都会新建对象
    • ​灵活:明确创建新对象,适用于需要独立修改的场景
    • ​可控:可调用 intern() 方法强制复用字符串池对象。 1. ​低效:重复创建相同内容的对象会占用更多内存

Collecation集合相关

  1. java arrayList的存储结构,初始化的时候创建多大的数组?
  • ArrayList是基于数组实现的,是一个动态数组,容量能自动增长,初始化长度是10, 扩容规则: 扩容后的大小是原始大小的1.5倍
  • ArrayList是线程不安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList
  1. ArrayList与LinkedList区别及使用
  • 数据结构不同;ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构
  • 效率不同
    • 查询时
      LinkedList是线性的数据存储方式,需要移动指针从前往后依次查找
      ArrayList使用数组方式存储数据,根据索引查询数据速度比较快
    • 新增或删除时
      LinkedList使用链表结构存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元素的指针指向即可,操作比较快
      ArrayList使用数组存储数据,在其中进行增删操作时,新增或删除后会对所有数据的下标索引造成影响;这里还涉及到扩容与缩容的规则:若新增数据时,原数组的大小可以容下新的数据则大小不变;若容不下的话会创建一个更大的数组(原始数组的1.5倍),然后将原数组中的元素复制到新数组中;
      删除元素时,​直接在原数组上操作,ArrayList 会将待删除位置之后的元素向前移动一位​(覆盖待删除元素),最后将数组末尾的元素置为 null(帮助GC回收);但是数组的容量不变

HashMap(哈希相关)

HashMap是Java中基于哈希表 (数组+链表/红黑树) 实现的键值对存储结构,核心目标是通过哈希函数快速定位数据,解决哈希冲突,并支持高效增删改查。

什么是哈希冲突(哈希碰撞)

​定义
当多个不同的键(Key)通过哈希函数计算出相同的哈希值时,称为哈希冲突。此时,这些键会被映射到哈希表的同一个“桶”(Bucket)中,导致数据存储和检索效率下降。
例如:Java 中的 “FB” 和 “Ea”

哈希冲突(哈希碰撞)的解决方案

Java的哈希表(如 HashMap)通过数组+链表(或红黑树)来实现的
​数组:存储桶(Bucket),每个桶对应一个链表或红黑树。
​哈希函数:将键(Key)转换为数组索引(index = hash(key) % capacity)。
​冲突解决

  • 链地址法​(默认):同一索引的键存入链表。
  • ​红黑树法​(Java 8+):当链表长度超过阈值(默认8)时,链表转为红黑树,提升查询效率。

HashMap底层实现原理

  • HashMap底层是一个Node[]数组,每个元素存储一个链表或红黑树(Java 8+)
  • 初始容量:默认16,扩容时,扩容后大小是上次大小的2倍
  • ​负载因子:默认0.75,当元素数量超过容量×负载因子时触发扩容
    ​哈希函数hash()的作用
    将键(Key)的hashCode()值映射到数组索引
    目的:均匀分布数据,减少哈希冲突
    链表/红黑树
  • ​链表:解决哈希冲突(同一索引的键值对存入链表)。
  • ​红黑树​(Java 8+):当链表长度≥8时,自动转为红黑树,提升查询效率(O(log n) → O(1)平均)。

优势

​O(1)平均时间复杂度:哈希计算快速,链表/红黑树优化冲突。
​灵活:支持任意对象作为键(需正确实现hashCode()和equals())。

​劣势

​非线程安全:需手动同步或用ConcurrentHashMap。
​不保证有序:遍历顺序与插入顺序无关。
​适用:高频键值存取(如缓存、配置存储)。
​不适用:需要有序遍历或范围查询(改用TreeMap)。

put与get实现

  • put(k,v)实现原理
    (1)首先将k,v封装到Node对象当中(节点)。
    (2)底层会调用K的hashCode()方法得出hash值。
    (3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有链表,就新增个链表把Node添加到这个链表中。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equals比较。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
  • get(k)实现原理
    (1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标
    (2)通过数组下标定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

HashSet集合与Map的关系

  • HashSet类中有一个全局变量HashMap map,然后在HashSet的构造函数中有一句话map=new HashMap(),说明在创建HashSet类对象的时候底层创建了一个HashMap对象

hashCode的本质

  • 帮助HashMap和HashSet集合加快插入的效率,当插入一个数据时,通过hashCode能够快速地计算插入位置,就不需要从头到尾地使用equlas方法进行比较

哈希结构的集合中添加元素

  • 先调用hashCode,唯一则存储,不唯一则再调用equals,结果相同则不再存储,结果不同则散列到其他位置。因为hashCode效率更高(仅为一个int值),比较起来更快

进程,线程,多线程,线程锁相关

进程 vs 线程

特性 ​进程 ​线程
​定义 独立运行的程序实例,占用内存空间。 进程内的执行单元,共享进程资源。
​资源分配 独享CPU、内存、文件句柄等。 共享进程内存、文件句柄等。
​调度单位 操作系统级调度。 CPU级调度(更细粒度)。
​示例 3个Java程序运行 → 3个进程。 1个程序内启动10个线程 → 1进程10线程。

线程状态(Java线程生命周期)

​状态 ​说明 ​触发场景
​NEW(新建状态) 线程对象已创建,未调用start()。 Thread t = new Thread();
​RUNNABLE(正在运行状态或等待CPU) JVM分配CPU时间片,线程正在运行或等待CPU。 正在执行的线程或就绪队列中的线程。
​BLOCKED(阻塞状态) 等待进入或重新进入同步代码块(如synchronized)。 线程A持有锁,线程B尝试获取同一锁。
​WAITING(无限等待) 无限期等待(如调用Object.wait()或Thread.join())。 线程主动等待某个条件满足。
​TIMED_WAITING(限时等待) 限时等待(如Thread.sleep(long ms))。 睡眠指定时间后自动恢复。
​TERMINATED(结束) 线程执行完毕,run()方法退出。 System.out.println(“线程结束”);

多线程实现方式

  1. 继承Thread类
    class MyThread extends Thread 简单,但无法继承其他类(Java单继承限制)
  2. 实现Runnable接口
    class MyTask implements Runnable 更灵活,支持多继承(推荐)
  3. 实现Callable接口
    class MyJob implements Callable<V> 支持返回值和异常处理,需配合Future使用
  4. 线程池
    ThreadPoolExecutor 管理线程生命周期,减少资源消耗(如Executors.newFixedThreadPool())
    ThreadPoolExecutor(
        corePoolSize,       // 核心线程数(空闲时保留)
        maximumPoolSize,    // 最大线程数
        keepAliveTime,      // 空闲线程存活时间
        TimeUnit unit,      // 时间单位
        BlockingQueue<Runnable> workQueue, // 任务队列
        ThreadFactory threadFactory, // 线程工厂
        RejectedExecutionHandler handler // 拒绝策略
    )
    ​常用线程池:
    newFixedThreadPool():固定线程数,无限队列。
    newCachedThreadPool():动态调整线程数,适用于短任务

锁机制

  1. synchronized关键字
    ​特点:
    内置锁,自动释放(进入同步块后自动加锁,退出或抛异常时解锁)。
    可修饰方法或代码块。
    ​示例:
    public synchronized void increment() { // 方法级锁
        count++;
    }
    
    public void incrementBlock() {
        synchronized (lockObject) { // 代码块级锁
            count++;
        }
    }
  2. ReentrantLock
    ​特点:
    显式锁,需手动加锁/解锁(lock()和unlock())。
    支持公平锁、可中断锁、条件变量。
    ​示例:
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock(); // 必须手动释放
    }
  3. CAS(Compare-And-Swap)​
    ​原理:原子操作,通过比较并交换内存值实现无锁编程。
    ​适用场景:计数器、状态标志等简单原子操作。
    ​示例(AtomicInteger)​:
    AtomicInteger atomicCount = new AtomicInteger(0);
    atomicCount.incrementAndGet(); // 原子自增
  4. volatile关键字
    ​特点:
    保证变量可见性(修改后立即刷新到主存,其他线程可见)。
    ​不保证原子性​(复合操作仍需锁)。
    ​示例:
    public volatile boolean flag = false; // 线程A修改flag,线程B能立即看到

死锁与避免

​死锁条件:
​互斥:资源至少被一个线程持有。
​请求与保持:线程A持有资源1,请求资源2,线程B持有资源2,请求资源1。
​不剥夺:资源不能被强制回收。
​循环等待:线程形成环形等待链。
​避免方法:
避免嵌套锁。
使用超时机制(如tryLock(timeout))。
统一锁顺序

总结

​选型建议:
​简单同步 → synchronized。
​复杂锁需求 → ReentrantLock(如公平锁、可中断锁)。
​无锁编程 → Atomic类(CAS操作)。
​线程管理 → ExecutorService线程池。
​核心原则:
​减少锁粒度:尽量缩小同步范围。
​避免阻塞操作:使用非阻塞IO(如NIO)。
​善用并发工具:如CountDownLatch、CyclicBarrier、Semaphore。


内存相关

内存概述

内存是计算机的临时存储设备,当程序被启动时,会被cpu加载到内存中。
Java 的内存区域划分基于​JVM规范

内存区域划分(Java8之后)

  1. 程序计数器
    ​作用:记录当前线程所执行的字节码指令地址(或行号索引),用于线程切换后恢复执行位置。
    ​特点:
    ​线程私有:每个线程独立拥有,互不影响。
    ​唯一不抛出 OutOfMemoryError 的区域:占用的内存极小,仅需保存当前指令地址

  2. Java 虚拟机栈
    ​作用:存储 Java 方法的局部变量、操作数栈、动态链接、方法出口等信息。
    ​特点:
    ​线程私有:每个方法在执行时会创建一个栈帧,方法结束则栈帧销毁。
    ​异常:若栈深度超过虚拟机允许的最大值,抛出 StackOverflowError;若无法申请到足够内存,抛出 OutOfMemoryError。
    ​与本地方法栈的区别:虚拟机栈服务于 Java 方法,本地方法栈服务于 JNI(Java Native Interface)调用的本地方法

  3. 本地方法栈​
    ​作用:与虚拟机栈类似,但为本地方法(如 C/C++ 实现的 JNI 方法)服务。
    ​特点:与虚拟机栈一样,可能抛出 StackOverflowError 或 OutOfMemoryError。


  4. ​作用:存放所有对象实例和数组,是垃圾回收(GC)的主要区域。
    ​特点:
    ​线程共享:所有线程通过 new 创建的对象均分配在此。
    ​核心问题:频繁触发 GC 或内存泄漏可能导致 OutOfMemoryError。
    ​细分区域​(根据垃圾回收器不同划分方式不同):
    新生代(Young Generation):Eden 区 + 两个 Survivor 区(S0/S1)。
    老年代(Old Generation):长期存活的对象最终晋升至此。
    永久代(PermGen,JDK 8 之前)→ ​元空间(Metaspace,JDK 8+)​。

  5. 方法区(Method Area)​
    ​作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
    ​特点:
    ​线程共享。
    ​JDK 8 之前称为永久代(PermGen)​:存储在 JVM 堆内存中,大小固定,易引发 OutOfMemoryError。
    ​JDK 8+ 改为元空间(Metaspace)​:存储在本地内存(非 JVM 堆),默认无上限(可通过 -XX:MaxMetaspaceSize 限制)。
    ​常见异常:类加载过多时可能抛出 OutOfMemoryError。

  6. 运行时常量池(Runtime Constant Pool)​
    ​作用:存放编译期生成的各种字面量和符号引用,运行时动态添加的常量(如 String.intern())。
    ​特点:
    ​属于方法区的一部分。
    ​动态性:支持动态添加常量(如字符串池)。
    ​内存溢出风险:若大量使用 intern() 导致常量池膨胀,可能引发 OutOfMemoryError。

  7. 直接内存(Direct Memory)​
    ​作用:不属于 JVM 运行时数据区,但可通过 NIO(如 ByteBuffer.allocateDirect())直接分配堆外内存。
    ​特点:
    ​不受 JVM 堆大小限制:但受操作系统物理内存限制。
    ​性能优势:减少数据在堆内和堆外的复制,提升 I/O 效率。
    ​内存泄漏风险:未显式释放可能导致 OOM

关键问题与调优

  1. ​堆内存溢出​(java.lang.OutOfMemoryError: Java heap space):
    原因:对象过多或内存泄漏。
    解决:调整 -Xmx(最大堆大小)和 -Xms(初始堆大小)。
  2. ​方法区溢出​(java.lang.OutOfMemoryError: PermGen space 或 Metaspace):
    原因:动态生成过多类(如反射、动态代理)。
    解决:调整 -XX:MaxPermSize(JDK 8 之前)或 -XX:MaxMetaspaceSize。
  3. ​栈溢出​(StackOverflowError):
    原因:递归调用过深或无限循环。
    解决:调整 -Xss(单个线程栈大小)

内存泄漏(Memory Leak)与内存溢出(Out Of Memory)(OOM)

内存泄漏(Memory Leak)

对象无法被回收:程序中已不再使用的对象由于某些原因未被垃圾回收器(GC)回收,导致内存占用持续增加。
​根本原因:存在无效的引用指向对象,使 GC 无法判定对象为“垃圾”。

常见场景:
  • ​集合未清空
    ​示例:静态集合类持有对象

    public class MemoryLeak {
        private static List<Object> list = new ArrayList<>();
        
        public void add(Object obj) {
            list.add(obj); // 对象即使不再使用,仍被静态集合持有
        }
    }

    ​解决:移除不再使用的对象(list.clear())。

  • ​监听器未移除
    ​解决:在组件销毁时解除注册。

  • ​内部类持有外部类引用
    ​解决:将内部类设为静态或弱引用。

内存溢出(Out Of Memory)(OOM)

​内存不足错误:JVM 无法为新对象分配内存空间,抛出 java.lang.OutOfMemoryError。
​直接原因:堆、栈、方法区等内存区域申请内存超过限制。

常见场景:
  • ​堆内存溢出
    ​原因:频繁创建大对象(如new byte[1024 * 1024 * 1024]分配1GB内存)

    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 持续分配1MB对象
        }
    }
  • 栈内存溢出
    原因:方法递归深度过大(如fibonacci(100000))。

    public static int fibonacci(int n) {
        if (n <= 1) return n;
        return fibonacci(n-1) + fibonacci(n-2); // 递归深度指数级增长
    }

    解决:改为循环或尾递归优化(Java不支持尾递归优化)

  • 本地方法栈溢出
    ​原因:JNI调用耗尽本地栈空间(如sun.misc.Signal.handle(int sig))。
    ​解决:减少本地方法调用或增大栈大小(-Xss2m)。

|

关键区别与预防

​特性 ​内存溢出(OOM)​ ​内存泄漏
​触发结果 JVM崩溃(OutOfMemoryError) 内存占用逐渐升高,最终可能引发OOM。
​原因 分配内存过大或无限增长。 对象未被释放,GC无法回收。
​典型场景 大数组、递归、第三方库缺陷。 集合未清空、监听器未移除、静态缓存。
​解决方向 优化内存分配,限制单次申请大小。 找到无效引用并释放对象。

最佳实践

​避免在循环中创建大对象

// ❌ 高频创建大对象
for (int i = 0; i < 1000; i++) {
    new byte[1024 * 1024];
}

// ✅ 使用对象池或复用对象
byte[] buffer = new byte[1024 * 1024];
for (int i = 0; i < 1000; i++) {
    Arrays.fill(buffer, 0);
}

​使用弱引用(WeakReference)​

List<WeakReference<Object>> weakList = new ArrayList<>();
weakList.add(new WeakReference<>(new Object())); // GC会自动回收

​及时关闭资源

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    // 使用资源
} catch (IOException e) {
    // 异常处理
} // finally自动关闭

​定期清理缓存

public static void addToCache(String key, Object value) {
    cache.put(key, value);
    if (cache.size() > 1000) { // 限制缓存大小
        cache.clear();
    }
}

​总结

​内存溢出是急性病,需通过代码优化和资源控制预防。
​内存泄漏是慢性病,需通过工具分析和编码规范根治。
​核心原则:
​谁分配,谁释放​(如try-finally、Closeable接口)。
​避免全局静态集合存储对象。
​善用监控工具​(如JConsole、VisualVM)及时发现问题

GC垃圾回收器和GC垃圾回收算法

Java 中的 ​垃圾回收器与 ​垃圾回收算法是紧密相关的,回收器是算法的具体实现
不同回收器会根据设计目标选择一种或组合多种或分阶段使用多种算法;以优化性能(如吞吐量、延迟)或适应特定场景(如大内存、低延迟)

核心原则:
新生代多用 ​复制算法​(高效)。
老年代多用 ​标记-清除 或 ​标记-整理​(减少碎片)。
现代回收器(如 G1、ZGC)通过分代、分区、并发等技术,灵活组合算法以平衡性能

垃圾回收算法

垃圾回收算法的核心目标是高效、安全地回收不再使用的对象。以下是常见的几种算法:

  1. 标记-清除(Mark-Sweep)​
    ​原理:
    ​标记:从 GC Roots 开始遍历所有可用对象并标记。
    ​清除:遍历整个堆,回收未被标记的对象。
    ​优点:实现简单。
    ​缺点:
    产生内存碎片,可能导致后续大对象分配失败。
    效率较低(需两次遍历)。
    ​适用场景:基础算法,常用于老年代回收。
  2. 复制(Copying)​
    ​原理:
    将内存分为两块(From 和 To),每次只使用一块。当进行 GC 时,将存活对象复制到另一块,然后清空原块。
    ​优点:
    无内存碎片。
    分配内存只需移动指针(效率高)。
    ​缺点:内存利用率低(仅一半内存可用)。
    ​适用场景:新生代(如 Eden 区)。
  3. 标记-整理(Mark-Compact)​
    ​原理:
    ​标记:标记所有存活对象。
    ​整理:将存活对象向一端移动,清理边界外的内存。
    ​优点:无内存碎片。
    ​缺点:需移动对象,效率较低。
    ​适用场景:老年代(如 CMS 回收器的最终步骤)。
  4. 分代收集(Generational Collection)​
    ​原理:基于对象的生命周期将内存划分为新生代(Young Generation)和老年代(Old Generation),采用不同算法:
    ​新生代:对象朝生夕灭,使用 ​复制算法。
    ​老年代:对象存活时间长,使用 ​标记-清除 或 ​标记-整理。
    ​优点:结合多种算法优势,提升整体效率。
    ​适用场景:主流 JVM 实现(如 HotSpot)。
  5. 增量收集(Incremental Collection)​
    ​原理:将 GC 过程分成多个小步骤,减少单次停顿时间(STW)。
    ​缺点:增加总 GC 时间。
    ​适用场景:对延迟敏感的应用。
  6. 引用计数(Reference Counting)​
    ​原理:每个对象维护一个引用计数器,引用数为 0 时立即回收。
    ​优点:实时性高。
    ​缺点:无法处理循环引用(如两个对象互相引用)。
    ​现状:未被主流 JVM 采用。

垃圾回收器

  1. Serial 回收器
    ​特点:
    单线程执行,简单高效。
    使用 ​标记-复制 算法。
    ​适用场景:客户端应用(如桌面程序)。
    ​启动参数:-XX:+UseSerialGC
  2. Parallel Scavenge 回收器
    ​特点:
    多线程并行收集,追求高吞吐量(用户代码运行时间 / 总时间)。
    新生代使用 ​复制算法,老年代使用 ​标记-整理。
    ​适用场景:后台批处理任务。
    ​启动参数:-XX:+UseParallelGC
  3. CMS(Concurrent Mark-Sweep)回收器
    ​特点:
    低延迟,尽量减少 STW 时间。
    分为四个阶段:
    ​初始标记​(STW,短暂)。
    ​并发标记​(与应用线程并行)。
    ​重新标记​(STW,修正并发标记的变动)。
    ​并发清除​(与应用线程并行)。
    使用 ​标记-清除 算法,会产生内存碎片。
    ​缺点:
    对 CPU 资源敏感。
    无法处理浮动垃圾(Concurrent Mode Failure)。
    ​启动参数:-XX:+UseConcMarkSweepGC
  4. G1(Garbage-First)回收器
    ​特点:
    面向大内存的多处理器机器。
    将堆划分为多个大小相等的区域(Region),优先回收垃圾最多的区域(Garbage-First)。
    可预测的停顿时间(通过 -XX:MaxGCPauseMillis 设置目标)。
    使用 ​标记-整理 算法,整体回收效率高。
    ​适用场景:大内存、低延迟要求的服务端应用。
    ​启动参数:-XX:+UseG1GC
  5. ZGC(Z Garbage Collector)​
    ​特点:
    ​低延迟​(停顿时间不超过 10ms)。
    使用 ​染色指针 和 ​读屏障 技术,支持并发标记和整理。
    内存可扩展(支持 TB 级堆)。
    ​适用场景:对延迟极度敏感的大规模应用(如实时系统)。
    ​JDK 支持:JDK 11+。
    ​启动参数:-XX:+UseZGC
  6. Shenandoah GC
    ​特点:
    与 ZGC 类似,停顿时间极短。
    通过 ​转发指针 和 ​读屏障 实现并发回收。
    开源项目,由 Red Hat 主导。
    ​适用场景:需要极低延迟的应用。
    ​JDK 支持:JDK 12+(需手动启用)

算法与回收器对比

​回收器 ​算法 ​停顿时间 ​吞吐量 ​适用场景
Serial 复制 客户端应用
Parallel Scavenge 复制 + 标记-整理 后台批处理
CMS 标记-清除 服务端低延迟应用
G1 标记-整理 + 复制 可预测 大内存、低延迟服务端应用
ZGC/Shenandoah 并发标记-整理 极短(ms) 超大规模、实时系统

如何选择垃圾回收器

​客户端应用:优先选择 Serial 或 Parallel Scavenge。
​服务端应用:
追求吞吐量:Parallel Scavenge + Parallel Old。
追求低延迟:G1、ZGC 或 Shenandoah。
​超大堆(TB 级)​:选择 ZGC 或 Shenandoah。

常用 JVM 参数

查看 GC 日志:-Xloggc: -XX:+PrintGCDetails -XX:+PrintGCDateStamps
调整新生代与老年代比例:-XX:NewRatio=2(新生代:老年代 = 1:2)
设置 G1 区域大小:-XX:G1HeapRegionSize=
通过理解不同 GC 算法和回收器的特性,可以针对应用场景优化内存管理,平衡吞吐量与延迟,提升系统性能


基础堆配置(示例)

-Xms512m # 初始堆大小(建议设为物理内存的1/4~1/2)
-Xmx1024m # 最大堆大小(避免动态扩容带来的性能抖动)
-XX:NewRatio=2 # 新生代与老年代比例(默认2:1,即新生代占1/3堆)
-XX:SurvivorRatio=8 # Eden:Survivor比例(默认8:1:1)

垃圾回收(GC)优化

  1. 减少GC停顿
    ​调整堆大小:避免堆过大或过小,使用-Xmx/-Xms固定堆。
    ​选择低延迟GC:如G1或ZGC(需JDK 11+)。
    ​缩短GC周期:降低-XX:MaxGCPauseMillis(如设为200ms)。
  2. 减少内存碎片
    ​启用压缩整理​(如Parallel Old使用-XX:+UseCompactOops)。
    ​选择CMS或G1:减少内存碎片产生。
  3. GC日志分析(示例)
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    分析工具:

​jvisualvm:可视化分析GC停顿时间和频率。
​GCViewer:生成GC日志图表,识别高频GC区域。
4. 线程与执行栈优化
线程池配置

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,       // 核心线程数(根据CPU密集度设置)
    maximumPoolSize,    // 最大线程数(避免过多线程竞争CPU)
    60L, TimeUnit.SECONDS, // 线程空闲时间
    new LinkedBlockingQueue<>(1000), // 任务队列
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

减少锁竞争
​缩小锁粒度:避免全局锁,改用局部锁或并发数据结构(如ConcurrentHashMap)。
​使用无锁编程:AtomicInteger、CAS操作替代synchronized。
5. 代码级优化
对象创建优化
​避免频繁创建临时对象:复用对象池(如ObjectPool)。
​使用基本数据类型:优先用int代替Integer,减少自动装箱开销。
内存泄漏排查
​工具:
​jmap:导出堆转储(jmap -dump:live,format=b,file=heapdump.hprof )。
​Eclipse MAT:分析堆转储,定位泄漏对象。
减少GC Roots引用
​避免静态集合存储对象:改用WeakReference或定期清理。
​解除内部类引用:将非静态内部类改为静态类。

  1. 典型优化案例
    ​案例1:Tomcat服务器性能优化
    ​问题:HTTP请求响应慢,GC频繁。
    ​优化步骤:
    调整堆大小:-Xms512m -Xmx1024m。
    切换GC器为G1:-XX:+UseG1GC -XX:MaxGCPauseMillis=200。
    限制线程池大小:server.xml中配置maxThreads=”200”。
    ​结果:GC停顿从2秒降至200ms,吞吐量提升40%。
    ​案例2:大数据计算内存优化
    ​问题:OutOfMemoryError(堆内存不足)。
    ​优化步骤:
    增加堆内存:-Xmx8g。
    使用对象池复用大对象(如ByteBuffer)。
    开启GC压缩:-XX:+UseCompressedOops。
    ​结果:内存占用减少30%,任务处理速度提升。

  2. 性能监控与调优工具
    ​工具 ​用途 ​示例命令
    ​jvisualvm 实时监控CPU、内存、线程、类加载。 查看堆内存使用趋势,分析GC停顿。
    ​jstack 导出线程堆栈信息,定位死锁或阻塞线程。 jstack -l > threaddump.txt
    ​jmap 导出堆转储,分析内存泄漏。 jmap -dump:live,format=b,file=heap.hprof
    ​Arthas 运行时诊断,监控方法调用、内存分配。 monitor命令跟踪对象创建和GC情况。

  3. 总结
    ​优化原则:

​权衡取舍:高吞吐量 vs 低延迟,内存占用 vs 性能。
​监控驱动:通过工具定位瓶颈,而非盲目调整参数。
​适配场景:根据应用类型(Web/桌面/大数据)选择合适的GC器和配置。
​未来趋势:

​ZGC/Shenandoah:JDK 11+的低延迟GC,适合超大堆。
​AOT编译:GraalVM提前编译字节码,提升启动速度和性能。
一句话:JVM优化需结合内存结构、GC机制、代码实践与监控工具,通过精准调参和场景适配实现性能飞跃。


反射

反射是Java的动态特性,允许程序在运行时获取类的元数据(如字段、方法、构造函数),并操作对象的行为,而无需编译时知道具体类名。
核心用途:框架开发(如Spring、Hibernate)、动态代理、注解处理、测试工具等

反射应用场景

​场景 ​示例
​框架自动装配 Spring通过反射将Bean的属性值自动注入。
​ORM框架(如Hibernate) ​ 将数据库表映射到Java对象,反射自动处理字段与SQL语句的转换。
​动态代理 创建代理对象,拦截方法调用(如AOP实现)。
​注解处理器 处理自定义注解(如@Deprecated、@Transactional)。
​测试工具 动态生成测试用例,模拟对象行为

其它

为什么放在hashMap集合key部分的元素需要重写equals方法?

因为equals方法默认比较的是两个对象的内存地址

为什么重写equals

  • Object类的equals,比较的是对象的地址值
  • 如果希望比较对象的内容是否相同,必须重写equals方法

为什么重写equals就必须重写hashCode?

  1. equals()和hashCode()的作用
  • equals() 用来比较两个对象是否相等;没有重写比较两个对象的地址值(地址值不会相同),若是想让两个内容相同的对象在equals后得到true,则需重写equals方法
  • hashCode() 用来返回对象的hash码值,通常情况下,我们都不会使用到这个方法;本质是为了帮助HashMap和HashSet集合加快插入的效率
  1. 为什么只要重写了equals方法,就必须重写hashCode
  • 主要是针对一些使用到了hashCode方法的集合,比如HashMap、HashSet等
  • 当equals方法被重写时,两个对象的内容相同即代表相同;但是如果没有重写hashCode方法,向hash结构的集合中添加这两个对象时,两个都会添加成功,因为没有重写hascode,生成的是两个hash值,在hash集合看来就是两个对象;说明这里必须重写hashCode(),两个内容相同的对象hash值相同,hash结构才会认为是一个对象;

自定义类型的实例对象使用set存储或作为Map的键需要做什么?

必须要重写hashcode和equals方法
原因:
不重写hashcode和equals方法的实例对象:每个对象的hash值都不相同;这时候所有的实例对象都会存入set
重写hashcode和equals方法的实例对象:比较对象某些内容是否相同,若这些内容相同即为同一实例对象,即不需要存入set

  • 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法
  • 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals

值传递和址传递的区别?

  • 值传递:传递的是真实的数据
  • 址传递:传递的是对象的引用

jdk,jre,jvm的含义以及三者之间的关系?

  • JVM: Java虚拟机,是Java程序的运行环境,
  • JRE: Java程序的运行时环境,包含JVM和运行时需要的核心类库
  • JDK: Java程序的开发工具包,包含JRE和开发使用的工具

i++和++i的区别?

  • i++ 先执行,后自增
  • ++i 先自增,后执行

super与this代表的含义

  • super:代表父类的存储空间标识(通过这个标识可以访问父类的成员和构造,但是其实是不同于父类的引用的)
  • this:代表当前对象的引用(谁调用就代表谁)

final和finally的区别

  • final是一个安全修饰符,用final修饰的类不能被继承,用final声明的方法不能被重写,使用final声明的变量是常量,不能被修改
  • finally是在异常里经常用到的, 是try和cach里的代码执行完以后,必须要执行的代码块,主要执行一些释放资源的操作,比如说关闭数据库连接,或者关闭IO流

Object常用方法

equals,hashcode,toString,wait,notitfy

类的初始化过程

​类初始化:加载 → 链接(验证→准备→解析) → 初始化(静态块→静态变量)。
​对象创建:分配内存 → 实例变量初始化 → 构造函数执行。
​继承规则:父类初始化完毕才执行子类初始化。

int和Integer的区别?

  • int是基本数据类型;Integer是java为int提供的包装类,是引用数据类型;
  • int的默认值为0,而integer的默认值为null

Java jdk1.7和1.8的区别

1.8特性新增

  • ​Lambda表达式 支持函数式编程,简化回调逻辑
  • ​Stream API 声明式集合操作,替代for循环
  • ​Optional类 解决空值问题,减少NullPointerException
  • ​默认接口方法 允许接口定义默认实现,兼容旧版本代码
  • ​类型注解(Type Annotations)​ 支持在类型声明上添加注解(如@NonNull),增强编译时检查。

API改进

  1. 新时间API(java.time,JSR 310)​
    ​改进点:替换Date/Calendar,线程安全且更易用。
    ​示例:
    // JDK 1.8+
    LocalDate now = LocalDate.now();
    LocalDateTime ldt = LocalDateTime.of(2023, 10, 1, 12, 30);
  2. Nashorn JavaScript引擎

​新增功能:在JVM内直接执行JavaScript代码。
​示例:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
engine.eval("print('Hello World!');");

​3. HTTP/2 Client
​新增类:HttpClient支持HTTP/2协议,提升性能。
​示例:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com"))
    .build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .join();

其他重要变化

​JavaFX分离

总结

JDK 1.8是Java长期演进的重要版本,新增的Lambda、Stream、Optional等特性显著提升了代码简洁性和可维护性,同时优化了性能和安全性。

Java EE面试题

DataSource 和DriverManager的区别

  1. 获取的对象不同。DataSource主要是获取数据库连接池,而DriverManager主要是获取数据库连接,通过管理JDBC驱动程序来建立连接。
  2. DataSource中封装了DriverManager的使用
  3. DataSource创建的connection可以被复用,而DriverManager的则不行

servlet的生命周期?

  • init:初始化方法,默认在第一次访问时被执行,只执行1次
  • service:提供服务的方法,在每次访问时都会执行,执行多次
  • destory:销毁的方法。会在服务器正常关闭的之后执行1次

SpringMVC和SpringBoot的区别

SpringBoot包含了Spring和SpringMVC;是它们两个的升级版

SpringBoot的核心注解

@EnableAutoConfiguration
开启自动化配置
@SpringBootConfiguration
用来声明当前类是SpringBoot应用的配置类,项目中只能有一个
@SpringBootApplication
相当于SpringBoot中的@EnableAutoConfiguration和Spring中的@ComponentScan


文章作者: zrh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zrh !
  目录