目录
21.JVM中哪些是线程共享区
22.你们项⽬如何排查JVM问题
23.⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
24.怎么确定⼀个对象到底是不是垃圾?
25.GC Root 是什么?
26.JVM有哪些垃圾回收算法?
27.什么是STW?
28.JVM参数有哪些?
29.说说对线程安全的理解
30.对守护线程的理解
31.ThreadLocal的底层原理
32.并发、并⾏、串⾏之间的区别
33.Java死锁如何避免?
34.线程池的底层⼯作原理
35.线程池为什么是先添加列队⽽不是先创建最⼤线程?
36.ReentrantLock中的公平锁和⾮公平锁的底层实现
37.ReentrantLock中tryLock()和lock()⽅法的区别
38.CountDownLatch和Semaphore的区别和底层原理
39.Sychronized的偏向锁、轻量级锁、重量级锁
40.Sychronized和ReentrantLock的区别
21.JVM中哪些是线程共享区
JVM中线程共享区有: 堆和方法区
JVM中线程独有的是: 栈,本地方法栈,程序计数器
22.你们项⽬如何排查JVM问题
对于还在正常运⾏的系统:
1.
可以使⽤jmap来查看JVM中各个区域的使⽤情况
2.
可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
3.
可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏ 调优了
4.
通过各个命令的结果,或者
jvisualvm等⼯具来进⾏分析
5.
⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表 示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这 些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻 代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效
6.
同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免 某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
1.
⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(
-
XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base
)
2.
我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3.
根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
4.
然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
23.⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- 首先把字节码文件内容加载到方法区
- 再根据类信息在堆创建对象
- 对象首先分配在堆中的年轻代的伊甸园区,经过一次轻GC之后,如果对象存在就进入幸存一区,
- 在之后经过多次轻GC过后,如果对象一直存活,这个对象就会在幸存一区和幸存二区来回from to,
- 每一次from to 移动 年龄就会增长1岁
- 当年龄大于15岁,如果这个对象还存活,就会进入老年区
- 在老年区时,经过full GC ,如果被标记为垃圾对象,就会被GC线程清除
24.怎么确定⼀个对象到底是不是垃圾?
引用计数器: 这种方式就是给堆内存中的对象进行计数,当计数为0时,就会被认为是垃圾对象。这是早起JDK的方式,无法解决循环引用问题。
可达性分析:从一个根对象 (GC root)向下寻找,找到的就不是垃圾对象,未找到的就是垃圾对象。
25.GC Root 是什么?
GC Root 就是根对象,栈中的局部变量表,方法区中的静态变量,正在运行的线程,本地方法栈中的变量都可以是GCRoot。
26.JVM有哪些垃圾回收算法?
1.
标记清除算法:
a. 标记阶段:把垃圾内存标记出来
b. 清除阶段:直接将垃圾内存回收。
c. 这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
2.
复制算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩ 相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然 后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽ 且,他的效率跟存活对象的个数有关。
3.
标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清 除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然 后将边界以外的所有内存直接清除。
27.什么是STW?
STW(Stop The World) :在执行垃圾回收时,将JVM冻结的一种状。发生STW时,停止一切Java线交互,本地方法native可以执行,但是不能与JVM进行交互。各种算法的优化就是减少STW。
28.JVM参数有哪些?
JVM参数⼤致可以分为三类:
1. 标准
指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
2.
⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
3.
不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤
29.说说对线程安全的理解
线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到 正常的结果,⽐如i++,i初始化值为0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那么最终的结果应该就是⼀个线程的结果为1,⼀个线程的结果为2,如果出现了两个线程的结果都为1,则 表示这段代码是线程不安全的。
所以线程安全,主要指的是⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果。
30.对守护线程的理解
线程分为⽤户线程和守护线程,⽤户线程就是普通线程,守护线程就是JVM的后台线程,⽐如垃圾回收 线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭。我们可以通过设置 thread.setDaemon(true)来把⼀个线程设置为守护线程
31.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经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)
32.并发、并⾏、串⾏之间的区别
1.
串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
2.
并⾏(
Parallelism
):两个任务同时执⾏
3.
并发(
Concurrency
):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的
33.Java死锁如何避免?
造成死锁的⼏个原因:
1.
⼀个资源每次只能被⼀个线程使⽤
2.
⼀个线程在阻塞等待某个资源时,不释放已占有资源
3.
⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
4.
若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
1.
要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
2.
要注意加锁时限,可以针对所设置⼀个超时时间
3.
要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
34.线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
1.
如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。
2.
如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。
3.
如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
4.
如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5.
当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数
35.线程池为什么是先添加列队⽽不是先创建最⼤线程?
当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之 后,才会新开线程。这就相当于,⼀个公司本来有10个程序员,本来这10个程序员能正常的处理各种需 求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中,然后这 10个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员⼯了。
36.ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使 ⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。 不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的
37.ReentrantLock中tryLock()和lock()⽅法的区别
1.
tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回
true,没有加到则返回false
2.
lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
38.CountDownLatch和Semaphore的区别和底层原理
- CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤
- CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
- 对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。
- Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()
- ReentrantLock中tryLock()和lock()⽅法的区别
- CountDownLatch和Semaphore的区别和底层原理⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。
39.Sychronized的偏向锁、轻量级锁、重量级锁
1.
偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
2.
轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
3.
如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4.
⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
40.Sychronized和ReentrantLock的区别
1.
sychronized是⼀个关键字,ReentrantLock是⼀个类
2.
sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
3.
sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
4.
sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
5.
sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
6.
sychronized底层有⼀个锁升级的过程