当前位置: 首页 > article >正文

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

 类

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

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。

http://www.kler.cn/a/547462.html

相关文章:

  • 先验期望准则
  • ORB-SLAM3的源码学习: Settings.cc:settings构造函数
  • 【吾爱出品】针对红警之类老游戏适用WIN10和11的补丁cnc-ddraw7.1汉化版
  • Beszel监控Docker安装
  • SQL Server STUFF 函数的用法及应用场景
  • Python学习心得字符串拼接的几种方法
  • R语言中的函数32:seq_along()
  • Centos安装php-8.0.24.tar
  • P9993 [Ynoi Easy Round 2024] TEST_133 Solution
  • Rust包管理
  • Vue学习笔记4
  • 循环队列知识点及习题
  • C++从入门到实战(四)C++引用与inline,nullptr
  • 青少年编程与数学 02-009 Django 5 Web 编程 13课题、URL分发
  • 【Java 面试 八股文】Spring Cloud 篇
  • 【认证授权FAQ】SSL/TLS证书过期导致的CLS认证失败
  • 联想笔记本电脑摄像头灯亮,但没有画面怎么解决,
  • Python的那些事第二十一篇:Python Web开发的“秘密武器”Flask
  • MATLAB图像处理:图像特征概念及提取方法HOG、SIFT
  • 将Sqlite3数据库挂在内存上处理