java05(类、泛型、JVM、线程)---java八股

Java中有哪些类加载器

JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

●BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。

●ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。

●AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。

泛型

泛型中extends和super的区别

  • extends 用于指定上界(上限),表示类型参数必须是某个类或接口的子类(或实现类)。
  • super 用于指定下界(下限),表示类型参数必须是某个类或接口的父类(或接口的父类)。

T extends Number 表示 T 必须是 Number 或其子类。

<? super T> 使得我们可以将 T 或其子类的实例添加到该集合中。例如,如果 T 是 Integer,那么我们可以将 Integer 和其父类 Number 的实例添加到 List<? super Integer> 中。

super 确保了我们只能向集合中插入 T 类型的元素,不能读取具体的元素类型,因为你只能确认元素的类型为 Object 或 T 的父类。

JVM

java的运行时的数据区

Java的运行时数据区是JVM(Java虚拟机)在程序运行时所划分的内存区域。它主要包括以下几个部分:

  1. 方法区 (Method Area)

    存储类信息、常量池、静态变量等数据。它是所有线程共享的区域,用来存放已经被加载到JVM中的类信息、方法信息、常量池等。

    在JVM规范中,方法区也可以称作永久代 (Permanent Generation) ,但是在JDK 8以后,永久代被Metaspace所取代。

  2. 堆 (Heap)

    Java堆是JVM中最大的内存区域,所有对象和数组都在这里分配。它是所有线程共享的区域。堆的内存可以由GC(垃圾回收)进行管理,负责清理不再使用的对象。

    堆主要分为两个部分:

    • 年轻代 (Young Generation):存放新创建的对象。
    • 老年代 (Old Generation):存放经过多次GC仍然存活的对象。
  3. Java栈 (Java Stack)

    每个线程都有自己的栈,栈中存储的是局部变量、方法调用和控制信息等。栈的大小由JVM参数进行设置。

    栈中的数据分为:

    • 局部变量区:用于存储方法的局部变量和部分参数。
    • 方法调用信息:包括栈帧(Stack Frame)和执行过程中的相关信息。
  4. 程序计数器 (Program Counter Register)

    每个线程都有一个程序计数器,它用来记录当前线程执行的字节码指令的地址。

    当线程被切换时,程序计数器保存当前执行的指令地址,保证线程切换后能够继续执行。

    它是唯一一个在jvm规范中没有规定任何OutOfMemoryError情况的区域

  5. 本地方法栈 (Native Method Stack)

    与Java栈类似,区别在于它用于处理Native方法(即非Java代码编写的方法)。Native方法栈用于存储本地方法调用的相关信息。

这些区域协同工作,保证了Java程序的顺利执行,并通过垃圾回收机制等手段管理内存。

JVM中哪些是线程共享区

堆区和方法区是所有线程共享的

栈、本地方法栈、程序计数器是每个线程独有的

什么是 OutOfMemoryError(OOM)?

OutOfMemoryError 是 Java 中的一种运行时错误,表示 Java 虚拟机(JVM)无法再分配足够的内存来满足程序的内存请求。通常会出现在以下几种情况:

  • 堆内存溢出(Heap OOM):JVM 的堆内存不足,无法分配更多对象。
  • 方法区溢出(Metaspace OOM):JVM 的元空间(或类的加载区)不足,无法加载更多类或方法信息。
  • 直接内存溢出 :JVM 分配给直接内存(通过 ByteBuffer.allocateDirect)的内存不足。

OOM 异常发生的情况

  • 堆内存不足(java.lang.OutOfMemoryError: Java heap space

    • 当 JVM 的堆内存不够用时,系统无法为新对象分配空间,进而导致 OOM 错误。此时,JVM 会尝试 GC(垃圾回收),但是如果回收后内存仍然不足,就会抛出该异常。
  • 元空间不足(java.lang.OutOfMemoryError: Metaspace

    • 在 Java 8 之后,类的元数据(如类定义)存储在元空间(Metaspace)中,而不是堆中。如果类的加载超过了 Metaspace 的限制,也会抛出 OOM 错误。
  • 直接内存不足(java.lang.OutOfMemoryError: Direct buffer memory

    • 使用 ByteBuffer.allocateDirect() 分配直接内存时,如果没有足够的物理内存,JVM 会抛出 OutOfMemoryError

OOM 发生时,进程会不会挂掉?

通常情况下,OOM 会导致进程终止:

  • OutOfMemoryError 是一个严重的错误,通常会导致 JVM 无法继续运行,因此会使进程退出。
  • 如果 JVM 无法分配足够的内存,它通常会抛出 OutOfMemoryError 异常,这个异常是 Error 类型的,而不是 Exception,意味着它通常不能被应用程序捕获并处理。
  • 应用程序无法处理 OOM 异常,这意味着一旦发生 OOM,JVM 会尝试终止进程,释放资源并输出错误信息。

JVM 如何响应 OOM 异常?

OutOfMemoryError 被抛出时,JVM 通常会触发以下操作:

  • 停止当前线程:当 OOM 发生时,当前线程会被终止。
  • 打印错误日志:JVM 会打印出错误日志,指示哪种内存(堆、元空间、直接内存等)导致了 OOM。
  • 终止进程 :由于 OutOfMemoryError 是一种 Error,它通常会导致应用程序退出,进程会被挂掉。

如何处理 OOM?

虽然 OOM 通常不能被捕获或处理,但可以采取以下措施以减少发生 OOM 的概率:

  • 优化内存使用:避免内存泄漏,减少不必要的大对象创建,合理使用内存。
  • 配置 JVM 内存参数 :通过设置 -Xmx(最大堆内存)、-Xms(初始堆内存)、-XX:MaxMetaspaceSize 等参数来控制 JVM 的内存分配。
  • GC 调优:使用合适的垃圾回收策略来减少内存的碎片化。
  • 使用外部监控工具:例如,使用 JConsole、VisualVM 等工具来监控内存使用情况,及时发现潜在的内存问题。

一个对象从加载到JVM,再到被GC清除,都经历了什么过程?

  • 首先把字节码文件内容加载到方法区
  • 然后再根据类信息在堆区创建对象
  • 对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄加1
  • 当年龄超过15后,对象依然存活,对象就会进入老年代
  • 如果经过Full GC,被标记为垃圾对象,那么就会被GC线程清理掉

怎么确定一个对象到底是不是垃圾?

  • 引用计数算法: 这种方式是给堆内存当中的每个对象记录一个引用个数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
  • 可达性算法: 这种方式是在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM有哪些垃圾回收算法?

  • 1标记清除算法:
    • a标记阶段:把垃圾内存标记出来
    • b清除阶段:直接将垃圾内存回收。
    • c这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。
  • 2复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。
  • 3标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

什么是STW?

STW: Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

JVM参数有哪些?

JVM参数大致可以分为三类:

1标注指令: -开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。

2非标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X 打印出来。

3不稳定参数: -XX 开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。

线程

说说对线程安全的理解

线程安全(Thread Safety)是指多个线程同时访问某个对象时,该对象的状态不会受到线程的干扰,并且不会导致错误的行为或数据不一致。

换句话说,线程安全的程序在多线程环境下运行时,不会因为线程的切换和并发访问导致问题

对守护线程的理解

线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程会在其他普通线程都停止运行之后自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
  4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

并发、并行、串行之间的区别

串行 :一个任务执行完,才能执行下一个任务
并行(Parallelism) :两个任务同时执行
并发(Concurrency):两个任务整体看上去是同时执行,在底层,两个任务被拆成了很多份,然后一个一个执行,站在更高的角度看来两个任务是同时在执行的

造成死锁的几个原因:

死锁(Deadlock) 是指两个或多个线程在执行过程中,因争夺资源而互相等待,导致线程无法继续执行的情况。死锁会导致程序的执行陷入无限等待状态,影响系统的稳定性和性能。

死锁的四个必要条件

死锁发生的必要条件包括以下四个:

  1. 互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即每次只能有一个线程使用该资源。
  2. 占有且等待条件(Hold and Wait):一个线程持有一个资源,同时等待另一个线程持有的资源。
  3. 非抢占条件(No Preemption):资源不能被强制从一个线程中剥夺,必须由线程自行释放。
  4. 循环等待条件(Circular Wait) :存在一种线程循环等待的关系,线程 T1 等待 T2,线程 T2 等待 T3,直到线程 Tn 等待 T1,形成一个闭环。

为了避免死锁,通常我们需要打破上述的至少一个条件。

Java死锁如何避免?

  1. 破坏循环等待条件
  • 通过规定线程获取资源的顺序来避免循环等待条件。例如,在系统中规定所有线程必须按一定的顺序请求资源。如果每个线程按照相同的顺序请求资源,那么就避免了死锁的发生。

2、使用定时锁

  • 使用定时锁(ReentrantLocktryLock(long time, TimeUnit unit) 方法)来避免线程长时间等待锁。如果在规定时间内无法获得锁,则放弃,避免死锁。

3、采用资源分配图算法(如银行家算法)

  • 对资源进行动态管理,使用类似银行家算法的方式,检查是否会导致死锁。如果当前的资源分配会导致死锁,则拒绝该请求。该方法需要复杂的资源分配图和算法支持,适用于需要严格管理资源分配的场景。

4、减少锁的持有时间

  • 在执行临界区代码时,尽量减少持有锁的时间。只在必要的情况下持有锁,完成任务后立即释放锁。这不仅有助于减少死锁的发生,也能提高程序的并发性能。

5、使用工具检测死锁

  • Java 提供了一些工具和技术来帮助开发人员检测死锁,例如:
    • JVM监控工具 :可以使用 jstack 或者通过 JConsoleVisualVM 等工具监控线程的状态,发现是否有死锁。
    • 线程调度器 :一些线程调度框架(如 ThreadMXBean)可以获取 JVM 的死锁信息。

6、优化资源使用顺序

  • 可以对系统资源进行排序或分配,确保所有线程都按照统一的顺序获取资源,避免因资源获取顺序不一致而产生循环等待。这样可以消除循环等待条件。

线程池的工作模式

线程池的工作原理通常包括以下几个部分:

  • 任务队列:用于缓存等待执行的任务。队列的大小决定了能缓存多少个任务,而不会直接创建线程来执行任务。
  • 核心线程数(corePoolSize):线程池在没有任务时,仍会保持这么多个线程处于活动状态,随时准备执行任务。
  • 最大线程数(maximumPoolSize):线程池可以创建的最大线程数,超过这个值的任务会进入等待队列,或者根据拒绝策略处理。

如果线程池在任务到来时首先创建最大线程数,而不使用队列缓存任务,那么无论任务数量如何,都会立刻创建新线程,这不仅没有利用队列的缓冲作用,还可能在瞬间创建大量的线程,造成系统资源浪费。

线程池的底层工作原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

线程池为什么是先添加列队而不是先创建最大线程?

线程池采用先添加到队列 而不是先创建最大线程的设计,主要是为了提高资源利用率,减少线程创建的开销,避免系统过度消耗资源,并通过合理的线程调度提高系统性能。这样可以保证线程池在高并发情况下依然高效运作,避免线程争用、内存溢出等问题的发生。

提高资源利用率,减少线程创建的开销

  • 线程的创建、销毁和上下文切换是有开销的。每次创建一个线程时,JVM 都需要分配内存并进行初始化,而销毁线程时又需要释放资源,这个过程本身就会消耗时间和系统资源。
  • 如果线程池在任务到来时直接创建最大线程,意味着即使任务数量很少,线程池也会创建大量的线程,这不仅增加了系统开销,还可能导致内存不足等问题。

解决方案: 线程池采用队列 来缓存任务,线程池中的线程可以复用,避免频繁创建和销毁线程 。通过先将任务放入队列,线程池可以按需创建线程,只有在队列满且线程数小于最大线程数时才会创建新线程。

控制线程数目,避免资源过度消耗

  • 如果线程池直接创建最大线程,系统的资源(如 CPU、内存)可能会因为线程数过多而受到压垮,尤其是在大量任务涌入时,系统可能会过度创建线程,导致线程争用,使得系统性能急剧下降。
  • 通过先将任务放入队列,可以限制同时活跃的线程数目,并确保不会因为任务过多而瞬间消耗掉系统的所有资源。

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

ReentrantLock中tryLock()和lock()方法的区别

  • tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
  • lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

Sychronized的偏向锁、轻量级锁、重量级锁

  • 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  • 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  • 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  • 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Sychronized和ReentrantLock的区别

  • 1sychronized是一个关键字,ReentrantLock是一个类
  • 2sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  • 3sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
  • 4sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  • 5sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  • 6sychronized底层有一个锁升级的过程

谈谈你对AQS的理解,AQS如何实现可重入锁?

  • AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
  • 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。 在不同的场景下,有不用的意义。
  • 在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。
相关推荐
赔罪9 分钟前
Python 高级特性-切片
开发语言·python
星星点点洲19 分钟前
【操作幂等和数据一致性】保障业务在MySQL和COS对象存储的一致
java·mysql
xiaolingting36 分钟前
JVM层面的JAVA类和实例(Klass-OOP)
java·jvm·oop·klass·instanceklass·class对象
风口上的猪20151 小时前
thingboard告警信息格式美化
java·服务器·前端
子豪-中国机器人1 小时前
2月17日c语言框架
c语言·开发语言
夏天的阳光吖1 小时前
C++蓝桥杯基础篇(四)
开发语言·c++·蓝桥杯
追光少年33221 小时前
迭代器模式
java·迭代器模式
oioihoii2 小时前
C++17 中的 std::to_chars 和 std::from_chars:高效且安全的字符串转换工具
开发语言·c++
秋窗72 小时前
Mac下Python版本管理,适用于pyenv不起作用的情况
开发语言·python·macos
柯腾啊2 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段