JAVA八股文

发布于:2025-05-09 ⋅ 阅读:(13) ⋅ 点赞:(0)

2025年 Java 面试八股文(20w字)_java面试必备八股文-CSDN博客

二、JAVA进阶

1.HashMap底层源码

1.判断当前hash表是否为空,如果为空进行初始化

2.判断相应的hash槽是否有元素(判断是否有hash冲突)

3.如果hash槽当前为空直接插入;

4.如果有hash冲突,先判断与第一个节点是否相等,相同用新的替换原来的

5.如果不相同,判断当前槽是否为红黑树,如果是红黑树调用红黑树的插入方法

6.如果不是红黑树,遍历链表,判断是否有相同元素,如果到了链表尾部,说明没有重复元素,直接插到链表尾部后判断元素个数,看是否需要将链表转化为红黑树,元素重复跳出循环,进行value替换

7.若 size 超过阈值(threshold = capacity * loadFactor),触发扩容。

get (Object key) 方法

1.计算哈希值并定位数组索引。

2.若桶中第一个节点匹配,直接返回。

3.若为树节点,调用红黑树的查找方法。

4.否则遍历链表查找。

resize () 方法(扩容机制)

触发条件:元素数量超过阈值或链表转换为红黑树时数组长度不足 64。

步骤:

1.新容量 = 旧容量 × 2(例如从 16 扩容到 32)。

2.重新计算阈值。

3.迁移元素:

        链表节点通过 e.hash & oldCap 是否为 0 分为两组,分别放入新数组的原位置或原位置 + 旧容量。

        红黑树节点若拆分后数量 ≤ 6,转换回链表。

红黑树优化

转换条件:链表长度 ≥ 8 且数组长度 ≥ 64。

优势:树化后查找、插入、删除的时间复杂度从 O (n) 降为 O (log n)。

退化条件:树节点数 ≤ 6 时,红黑树转换回链表。

(为什么是6?避免频繁链表和红黑树之间的转换,红黑树查找性能高但是比较难维护)

 线程安全性

非线程安全:多线程下可能出现数据不一致(如扩容时的循环链表问题)。

替代方案ConcurrentHashMap:JDK1.8 采用 CAS + synchronized 实现高效并发。Collections.synchronizedMap():使用全局锁,性能较差。

与JDK1.7相比改进:

尾插法:避免 JDK1.7 头插法在多线程扩容时的循环链表问题。

哈希扰动优化:减少哈希冲突,使元素分布更均匀。

懒加载:首次调用 put() 时才初始化数组,减少内存浪费。

2.JVM

1.方法区:

jdk1.7永生代,jdk1.8元空间

主要用来存储已被虚拟机加载的类信息、常量、静态变量,静态方法

很少发生垃圾回收(GC),在这里进行的GC主要是对方法区里的常量池和对类型的卸载

线程共享

方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

2.堆:

几乎所有的对象实例都在这里创建,线程共享,回收GC,虚拟机启动时创建

方法区和堆,溢出错误类型:OutOfMemoryError

3.虚拟机栈(java栈):

栈内存,为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。

虚拟机栈是线程私有的,它的生命周期与线程相同。

局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定

操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

存储方法调用的栈帧(每个方法对应一个栈帧),包含:

局部变量表(基本类型 + 对象引用)、操作数栈、动态链接、方法出口

关键点:

栈深度过大 → StackOverflowError(递归调用常见)

无法扩展 → OutOfMemoryError

通过 -Xss 参数调整栈大小(默认 1MB)

4.本地方法栈:

作用
为 JVM 调用 Native 方法(如 C/C++ 代码)服务

特点

HotSpot 将虚拟机栈与本地方法栈合并同样可能抛出 StackOverflowError 和 OutOfMemoryError

5.程序计数器

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

作用
记录当前线程执行的字节码指令地址(相当于代码执行的「行号指示器」)。

特点

唯一不会发生 OutOfMemoryError 的区域

线程切换时依赖它恢复执行位置

执行 Native 方法时值为 undefined

3.Java中垃圾收集的方法有哪些 

复制算法  Minor GC,这种GC算法采用的是复制算法(Copying)
a) 效率高,缺点:需要内存容量大,比较耗内存

b) 使用在占空间比较小、刷新次数多的新生区

标记-清除  标记清除或者是标记清除与标记整理的混合实现

效率比较低,会差生碎片。

标记-整理  标记清除或者是标记清除与标记整理的混合实现

效率低速度慢,需要移动对象,但不会产生碎片。

4.如何判断一个对象是否存活(或者GC对象的判定方法)

1.引用计数法:

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

优点

实时性:内存可立即回收,无需等待垃圾回收周期。

低延迟:回收过程分散在程序运行中,避免Stop-The-World暂停。

内存高效:适合内存受限的环境(如嵌入式系统)。

缺点

循环引用问题
若两个或多个对象互相引用,它们的计数永不为0,导致内存泄漏。

性能开销
频繁更新计数器(尤其是多线程环境)会增加运行时开销。

无法处理全局引用
静态变量、缓存等长期引用可能导致对象无法释放。

2.可达性算法(引用链法)

核心思想是通过一系列称为“GC Roots”的根对象出发,遍历所有能被这些根对象直接或间接引用的对象,标记这些对象为存活,未被标记的则视为可回收的垃圾。

步骤:

1.确定GC Roots

虚拟机栈中的本地变量:当前执行方法中的局部变量和参数。

方法区中的静态变量:类的静态属性引用的对象。

方法区中的常量:如字符串常量池中的引用。

本地方法栈中的JNI引用:Java Native Interface引用的对象。

Java虚拟机内部的引用:如基本数据类型对应的Class对象,系统类加载器等。

2.标记阶段:从所有GC Roots出发,使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历对象图。访问每个可达对象,并将其标记为存活(例如,在对象头中设置标记位)。

3.清除阶段:遍历堆中的所有对象,将未被标记的对象回收,释放其占用的内存空间。可选择是否进行内存整理,以消除内存碎片(如标记-整理算法)。

优点:有效处理循环引用问题。相对引用计数法,实现简单且高效。

缺点:需要暂停应用程序线程(Stop-The-World),导致延迟。遍历整个对象图可能耗时,尤其在堆内存较大时。

5.什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查

引发 StackOverFlowError 的常见原因有以下几种:
1.无限递归循环调用(最常见)
2.执行了大量方法,导致线程栈空间耗尽
3.方法内声明了海量的局部变量
4.native 代码有栈上分配的逻辑,并且要求的内存还不小,比如java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。

排查:

步骤1:检查代码逻辑

定位报错堆栈:日志中明确提示的类和方法(如at com.example.MyClass.infiniteRecursion(MyClass.java:10))。

检查递归方法是否有终止条件,或循环调用是否存在死循环。

步骤2:调整栈大小(临时解决)

增大线程栈空间(如设置-Xss2M),但需谨慎,可能掩盖代码问题。

步骤3:分析线程栈

使用调试工具(如IDEA的Debug模式)查看方法调用链。

生成线程转储(Thread Dump)

分析线程栈中的重复调用模式,定位循环或递归点。


引发 OutOfMemoryError的常见原因有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
3.代码中存在死循环或循环产生过多重复的对象实体
4.启动参数内存值设定的过小

排查:

步骤1:确认错误类型

java.lang.OutOfMemoryError: Java heap space:堆内存不足。java.lang.OutOfMemoryError: Metaspace:元空间(类元数据)不足。

步骤2:生成堆转储(Heap Dump)

启动参数添加自动转储

手动生成堆转储

步骤3:分析堆转储

使用工具(如Eclipse MAT、VisualVM)分析内存占用:

查找Dominator Tree(支配树)中的大对象。

检查Leak Suspects报告,定位疑似内存泄漏点。

查看对象引用链,确认为何无法被GC回收。

步骤4:监控内存使用

实时监控工具(如JConsole、Arthas)

观察老年代(Old Gen)是否持续增长,Full GC后是否无法释放内存。

步骤5:修复代码

内存泄漏:移除无效的静态引用、关闭未释放的资源(如数据库连接)。

优化数据结构:避免缓存过大对象,使用弱引用(WeakHashMap)。

调整JVM参数

6.什么是线程池,线程池有哪些(创建)

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率

在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

然后调用他们的 execute 方法即可。

这4种线程池底层 全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。

newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:

工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。

7、为什么要使用线程池

线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

主要特点:线程复用;控制最大并发数:管理线程。

第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控

8、线程池底层工作原理

第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常

9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些

参数与作用:共7个参数

corePoolSize:核心线程数
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。

maximumPoolSize:最大线程数
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。

keepAliveTime:存活时间
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。

unit:keepAliveTime的单位。
workQueue:任务队列
常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界列),ArrayBlockingQueue(有界队列)。

threadFactory:线程工厂
ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。

RejectedExecutionHandler:拒绝策略
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。

线程池大小设置:

1.需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
2.每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系


如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。

一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目

这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)

拒绝策略:

AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
 

10、常见线程安全的并发容器有哪些

1.CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
2.CopyOnWriteArrayList、CopyOnWriteArraySet采用写时复制实现线程安全
3.ConcurrentHashMap采用分段锁的方式实现线程安全

11、Atomic原子类了解多少 原理是什么

Java 的原子类都存放在并发包

基本类型

使用原子的方式更新基本类型
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
数组类型

使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型

AtomicReference:引用类型原子类
AtomicStampedReference:原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
AtomicInteger 类利用 CAS (Compare and Swap) + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿“原值”的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。

 12、synchronized底层实现是什么 lock底层是什么 有什么区别

Synchronized原理:

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

Lock原理:

Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
Lock释放锁的过程:修改状态值,调整等待链表。
Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。

Lock与synchronized的区别:

1.Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
2.当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的     条件下提供一种退出的机制。
3.一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以    及设置等待时限等方式退出条件队列。
4.synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式

13、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理

ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。

hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化

 14、ConcurrentHashMap底层原理

Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

15、了解volatile关键字不

volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。
volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱

16、synchronized和volatile有什么区别

1.volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
5.volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

17、Java类加载过程

Java类加载是JVM将类的.class文件加载到内存,并进行验证、准备、解析和初始化,最终形成可被JVM直接使用的Java类型的过程。整个过程分为 加载、连接(验证、准备、解析)、初始化 三个阶段,并遵循 双亲委派模型

类加载的三大阶段
1. 加载(Loading)
  • 核心任务:
    将类的二进制字节流(.class文件、网络数据、动态生成等)加载到JVM内存,并在堆中生成一个java.lang.Class对象,作为方法区数据的访问入口。

  • 步骤:

    1. 通过类的全限定名(如com.example.MyClass)获取二进制字节流。

    2. 将字节流转换为方法区的运行时数据结构。

    3. 在堆中创建Class对象,作为程序访问方法区数据的接口。

  • 类加载器(ClassLoader):

    • 启动类加载器(Bootstrap ClassLoader):
      加载JRE核心类库(如java.lang.*),由C++实现,是JVM的一部分。

    • 扩展类加载器(Extension ClassLoader):
      加载JAVA_HOME/lib/ext目录下的扩展类。

    • 应用程序类加载器(Application ClassLoader):
      加载用户类路径(ClassPath)下的类(默认的类加载器)。

    • 自定义类加载器:
      用户可继承ClassLoader实现自定义加载逻辑(如热部署、加密类加载)。

2. 连接(Linking)
  • 子阶段1:验证(Verification)

    • 确保字节码符合JVM规范,防止恶意代码破坏虚拟机安全。

    • 检查项:文件格式、元数据、字节码、符号引用等。

  • 子阶段2:准备(Preparation)

    • 为 类变量(static变量) 分配内存并设置初始值(零值)。

      public static int value = 123;  // 准备阶段value=0,初始化阶段value=123
  • 子阶段3:解析(Resolution)

    • 将常量池中的 符号引用(如类名、方法名)替换为 直接引用(内存地址、偏移量)。

    • 例如:将java.lang.Object替换为实际内存中Object类的地址。

3. 初始化(Initialization)
  • 执行类的初始化代码(<clinit>()方法):

    • 合并类中所有static变量的赋值操作和static{}代码块。

    • JVM保证<clinit>()在多线程环境下被正确加锁同步。

  • 触发条件(有且仅有以下情况):

    1. 使用new实例化对象、访问或设置类的静态字段(非final)、调用静态方法。

    2. 反射调用(如Class.forName("com.example.MyClass"))。

    3. 初始化子类时,若父类未初始化,先触发父类初始化。

    4. JVM启动时指定的主类(包含main()方法的类)。

    5. 动态语言支持(如MethodHandle实例解析结果为REF_getStatic等)。

加载 加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
通过一个类的全限定名获取该类的二进制流。

将该二进制流中的静态存储结构转化为方法去运行时数据结构。 

在内存中生成该类的Class对象,作为该类的数据访问入口。

验证 验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:
文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

18、什么是类加载器,类加载器有哪些

类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。

主要有以下四种类加载器
1.启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用

2.扩展类加载器(extension class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类

3.系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它

4.用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

什么时候会使用到加载器?java中的加载器是按需加载,什么时候用到,什么时候加载:
new对象的时候
访问某个类或者接口的静态变量,或者对该静态变量赋值时
调用类的静态方法时
反射
初始化一个类的子类时,其父类首先会被加载
JVM启动时标明的启动类,也就是文件名和类名相同的那个类

19、简述java内存分配与回收策略以及Minor GC和Major GC(full GC)

1.内存分配
栈区:栈分为java虚拟机栈和本地方法栈

堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)

程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。

2.回收策略以及Minor GC和Major GC
对象优先在堆的Eden区分配
大对象直接进入老年代
长期存活的对象将直接进入老年代

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

20.死锁产生的必要条件

1.互斥:资源x的任意一个时刻只能被一个线程持有

2.占有且等待:线程1占有资源x的同时等待资源y,并不释放x

3.不可抢占:资源x一旦被线程1占有,其他线程不能抢占x

4.循环等待:线程1持有x,等待y,线程2持有y,等待x

当全部满足时才会死锁

21.Java死锁如何避免

1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁

2.要注意加锁时限,可以针对锁设置一个超时时间

3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决


网站公告

今日签到

点亮在社区的每一天
去签到