Java基础
1. String 和StringBuffer 和 StringBuilder的区别?
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
2. sleep() 区间wait()区间有什么区别?
sleep
是Thread
中的方法,线程暂停,让出CPU,但是不释放锁🔐
wait()
是Object
中的方法, 调用次方法必须让当前线程必须拥有此对象的monitor(即锁),执行之后 线程阻塞,让出CPU, 同时也释放锁🔐; 等待期间不配拥有CPU执行权, 必须调用notify/notifyAll
方法唤醒,(notify是随机唤醒) 唤醒并不意味着里面就会执行,而是还是需要等待分配到CPU才会执行;
3. Object 中有哪些方法?其中clone(),怎么实现一个对象的克隆,Java如何实现深度克隆?
clone
是浅拷贝;只克隆了自身对象和对象内实例变量的地址引用,使用它需要实现接口Cloneable
;
使用ObjectStream
进行深度克隆; 先将对象序列化;然后再反序列化;
public static <T extends Serializable> T deepClone(T t) throws CloneNotSupportedException {// 保存对象为字节数组try {ByteArrayOutputStream bout = new ByteArrayOutputStream();try(ObjectOutputStream out = new ObjectOutputStream(bout)) {out.writeObject(t);}// 从字节数组中读取克隆对象try(InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {ObjectInputStream in = new ObjectInputStream(bin);return (T)(in.readObject());}}catch (IOException | ClassNotFoundException e){CloneNotSupportedException cloneNotSupportedException = new CloneNotSupportedException();e.initCause(cloneNotSupportedException);throw cloneNotSupportedException;}}
AI生成项目java运行
ThreadLocal相关
4. ThreadLocal作用和实现方式 ?
TL用于保存本地线程的值, 每个
Thread
都有一个threadLocals
属性,它是一个ThreadLocalMap
对象,本质上是一个Entry
数组;Entry
是k-v结构; 并且是WeakReference
弱引用, K存的是Thread
对象,Value是设置的值; 那么每个线程就可以读自己设置的值了;
ThreadLocal会不会发生内存泄漏?
会发生内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key
,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
ThreadLocal为什么使用弱引用?
key是弱引用好歹还可以 GC掉key的对象;强引用则不行
使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
5. InheritableThreadLocal作用和实现方式 ?
InheritableThreadLocal
基础ThreadLocal
; 他跟ThreadLocal
区别是 可以传递值给子线程; 每个Thread
都有一个inheritableThreadLocals
属性, 创建子线程的时候,把把父线程的Entry
数组 塞到子线程的Entry数组
中; 所以就实现了父子线程的值传递; 注意如果Value是一个非基本类型的对象, 父子线程指向的是相同的引用; 子线程如果修改了值,父线程也是会修改的;
6. InheritableThreadLocal所带来的问题?
线程不安全:
如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量
线程池中可能失效:
在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过Thread的init
方法的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了
7. 如何解决线程池异步值传递问题 (transmittable-thread-local)?
阿里开源的
transmittable-thread-local
可以很好的解决 在线程池情况下,父子线程值传递问题;TransmittableThreadLocal
继承了InheritableThreadLocal
, 简单的原理就是TTL 中的holder持有的是当前线程内的所有本地变量,被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量
HashMap ConcurrentHashMap相关
9. HashMap为什么线程不安全
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。(链表的头插法 造成环形链)
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。(元素插入时使用的是尾插法)
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
10. HashMap在jdk7和8中的区别
- JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
- 扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
11. HashMap 为啥将链表改成红黑树?
提高检索时间,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。复杂度变成O(logn)
12. ConcurrentHashMap在jdk7和8中的区别?
可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中
synchronized+CAS+HashEntry+红黑树
,相对而言
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
- JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock; 因为粒度降低了
提到synchronized时候,顺便说一下javaSE1.6对锁的优化?
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能大的影响是阻塞的是实现,挂起 线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力
javaSE1.6引入了偏向锁,轻量级锁(自旋锁)后,synchronized和ReentrantLock两者的性能就差不多了
锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.
偏向锁
偏向锁
: HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得; 偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
偏向锁的获取
: 当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入
偏性锁的撤销:
偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)
轻量级锁
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。
加锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁:
轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.
重量级锁
Java线程的阻塞以及唤醒,都是依靠操作系统来完成的,这些操作将涉及系统调用,需要从操作系统 的用户态切换至内核态,其开销非常之大。
其他优化
锁粗化:
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
锁消除:
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁
ReentrantLock和synchronized的区别?
在HotSpot虚拟机中, 对象在内存中的布局分为三块区域: 对象头, 示例数据和对其填充.
对象头中包含两部分: MarkWord 和 类型指针.
多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作
Synchronized
对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现,Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放
- 代码块同步: 通过使用
monitorenter
和monitorexit
指令实现的.- 同步方法:
ACC_SYNCHRONIZED
修饰
ReentrantLock
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
- 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
13. 为什么重写equals时候被要求重写hashCode()?
如果两个对象相同(即:用 equals 比较返回true),那么它们的 hashCode 值一定要相同
如果两个对象的 hashCode 相同,它们并不一定相同(即:用 equals 比较返回 false
为了提供程序效率 通常会先进性hashcode
的比较,如果不同,则就么有必要equals
比较了;
14. 什么时候回发生内存泄露?让你写一段内存泄露的代码你会怎么写?
我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露
下面给出一个 Java 内存泄漏的典型例子,
Vector v = new Vector(10);for (int i = 0; i < 100; i++) {Object o = new Object();v.add(o);o = null;}
AI生成项目java运行
在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。
v = null
ThreadLocal使用不当也可能泄漏
Java内存模型
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。Java 的并发采用的是共享内存模型
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory)
Java 内存模型中的 happen-before 是什么?
从 JDK5 开始,java 使用新的 JSR -133 内存模型,提出了 happens-before 的概念
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系
这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。
注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
简单聊聊volatile 的特性?以及内存语义
可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
原子性:对任意单个 volatile 变量的读 / 写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
volatile 写的内存语义
:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
volatile 读的内存语义:
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM 采取保守策略
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
通过反编译可以看到,有volatile变量修饰的遍历,会有一个lock前缀的指令,lock前缀的指令在多核处理器下会引发了两件事情
将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
GC垃圾回收
垃圾回收主要是针对 内存区的哪些区域?
主要追对的是 Java堆 和 方法区 ;
java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
垃圾检查有哪些算法?
- 引用计数法 :给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。
好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:- 可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象
垃圾回收方法有哪些?
- 标记-清除(Mark-sweep):标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
优缺点:实现简单,容易产生内存碎片
- 复制(Copying)将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
- 标记-整理(Mark-Compact)
先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存
优缺点
:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下- 分代收集算法(目前大部分JVM的垃圾收集器所采用的算法)
年轻代(Young Generation)
的回收算法 (回收主要以Copying为主)
年老代(Old Generation
)的回收算法(回收主要以Mark-Compact为主)
什么时候会触发Full GC
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区(1.8之后改为元空间)空间不足
(4)创建大对象,比如数组,通过Minor GC后,进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
GC机制简要说明一下,不同区使用的算法。
年轻代:
是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。
年老代:
在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响
两个对象循环引用会不会被被GC?
GC里边在JVM当中是使用的ROOT算法,ROOT算法 也就是根; 只要看这个两个对象有没有挂在 根 上, 挂在根上了 就不会被回收; 没有挂在根上就会回收;
哪些可以算作根节点?
- 方法区中的静态属性
- 方法区的中的常量
- 虚拟机中的局部变量
- 本地方法栈中JNI
垃圾收集器 G1有什么样的特性了解吗? CMS呢?
Cms与G1的优缺点
CMS垃圾回收器:
- 初始标记(CMS-initial-mark) ,会导致STW(stop-the-world);
- 并发标记(CMS-concurrent-mark),与用户线程同时运行
- 预清理(CMS-concurrent-preclean),与用户线程同时运行
- 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
- 重新标记(CMS-remark) ,会导致STW; 这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了
- 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
CMS垃圾回收器的优化
1.减少重新标记remark阶段停顿
一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:-XX:+CMSScavengeBeforeRemark
在执行remark操作之前先做一次ygc,目的在于减少ygen对oldgen的无效引用,降低remark时的开销。
G1垃圾回收器
CMS收集器和G1收集器的区别
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
Jvm相关
Jvm内存结构简要说一些,栈里面一般存储了什么?
Java内存模型简要描述一下?
类加载机制简要描述一下?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。类加载和连接的过程都是在运行期间完成的。
类的加载方式
1):本地编译好的class中直接加载
2):网络加载:java.net.URLClassLoader可以加载url指定的类
3):从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类
4):从java源代码文件动态编译成为class文件
类加载的过程
- 类加载的生命周期:加载(Loading)–>验证(Verification)–>准备(Preparation)–>解析(Resolution)–>初始化(Initialization)–>使用(Using)–>卸载(Unloading)
加载
a)加载阶段的工作
i.通过一个类的全限定名来获取定义此类的二进制字节流。
ii.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
iii.在java堆中生成一个代表这个类的java.lang.Class对象,做为方法区这些数据的访问入口。
b)加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中
。
验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
准备
准备阶段是正式为变量
分配内存并设置初始值
,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量
。
解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
初始化
初始化阶段是执行类构造器()方法的过程
JVM三种预定义类型类加载器
a. Bootstrap ClassLoader/启动类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.
b. Extension ClassLoader/扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作
c. System ClassLoader/系统类加载器
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.
d. User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.
双亲委派加载
JVM在加载类时默认采用的是双亲委派机制, 先往上 让上层加载器去加载
由不同的类加载器加载的指定类型还是相同的类型吗(不同)
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但
在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.
所以是不相同的
在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用
调用类的类加载器来进行类加载
在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是?
在不指定父类加载器的情况下,默认采用系统类加载器(AppClassLoader);
编写自定义类加载器时,一般有哪些注意点?
- 一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑; 这样做极有可能引起系统默认的类加载器不能正常工作
如何在运行时判断系统类加载器能加载哪些路径下的类?
一 是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用
URLClassLoader中的getURLs()方法可以获取到
;
二 是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 ,System.getProperty("java.class.path")
在Java的反射中,Class.forName和ClassLoader的区别
ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器
Class.forName()方法实际上也是调用的CLassLoader来实现的;在这个forName0方法中的第二个参数被默认设置为了true,这个参数代表是否对加载的类进行初始化,设置为true时会类进行初始化,代表会执行类中的静态代码块,以及对静态变量的赋值等操作。
Class.forName 默认会进行初始化,执行静态代码块;有参数可以设置
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
Java 类加载机制及常见异常
ClassNotFoundException 发生在加载阶段
无法找到目标类
通常加载方式 Class.forName / ClassLoader.loadClass ;
导致原因:1、类名拼写错误或者没有拼写完整类名
2,没有导入相应的jar包
ClassNotFoundError 发生在 链接 阶段
类加载过程有几个阶段
读取:找到.class文件,读取
链接:校验读取的class文件是否符合规范
初始化:载入静态资源 静态块 产生一个Class对象
NoClassDefFoundError 通常在链接阶段
Exception和Error的区别
首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。
平时有没有遇到一些栈溢出或者内存溢出,内存泄露的问题吗?如何去分析这个问题?
内存泄漏
是指对象实例在新建和使用完毕后,仍然被引用,没能被垃圾回收释放,一直积累,直到没有剩余内存可用。如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。分析内存泄漏的工具有:Jprofiler,visualvm等。
内存溢出
是指当我们新建一个实力对象时,实例对象所需占用的内存空间大于堆的可用空间。
栈(JVM Stack)存放主要是栈帧( 局部变量表, 操作数栈 , 动态链接 , 方法出口信息 )的地方。注意区分栈和栈帧:栈里包含栈帧。与线程栈相关的内存异常有两个:
a)、StackOverflowError(方法调用层次太深,内存不够新建栈帧)
b)、OutOfMemoryError(线程太多,内存不够新建线程)
如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题
如果内存猛增,怎么去排查?
通过jstack分析问题
1、利用top名称查看哪个java进程占用了较多的cpu资源;
2、通过top -Hp pid可以查看该进程下各个线程的cpu使用情况;
3.通过top -Hp命令定位到cpu占用率较高的线程tid之后,继续使用jstack pid命令查看当前java进程的堆栈状态
4.然后将刚刚找到的tid转换成16进制,在 jstack -pid里面的堆栈信息里面找到对应的线程信息
多线程
为什么《阿里巴巴Java开发手册》强制不允许使用Executor创建线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
主要是Executor的一些方法创建的线程池的对了长度都非常大,容易堆积大量的请求,从而导致OOM
ThreadPoolExecutor机制
下面是ThreadPoolExecutor最核心的构造方法参数:
1)corePoolSize
核心线程池的大小
2)maximumPoolSize
最大线程池大小,当队列满了 就会创建新线程直至最大
3)keepAliveTime
线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程超出有效时间也关闭
4)TimeUnit keepAliveTime
的时间单位
5)workQueue
阻塞任务队列
6)threadFactory
新建线程工厂,可以自定义工厂
7)RejectedExecutionHandler
当提交任务数超过maximumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理
重点讲解
corePoolSize,maximumPoolSize,workQueue三者之间的关系
1)当线程池小于corePoolSize时,新提交的任务会创建一个新线程执行任务,即使线程池中仍有空闲线程。
2)当线程池达到corePoolSize时,新提交的任务将被放在workQueue中,等待线程池中的任务执行完毕
3)当workQueue满了,并且maximumPoolSize > corePoolSize时,新提交任务会创建新的线程执行任务
4)当提交任务数超过maximumPoolSize,新任务就交给RejectedExecutionHandler来处理
5)当线程池中超过 corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6)当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
RejectedExecutionHandler拒绝策略
1、
AbortPolicy策略
:该策略会直接抛出异常,阻止系统正常工作;
2、CallerRunsPolicy策略
:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
3、DiscardOledestPolicy策略
:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
4、DiscardPolicy策略
:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;
也可以自己扩展RejectedExecutionHandler接口
workQueue任务队列
- 直接提交队列:设置为
SynchronousQueue
队列,提交的任务不会被保存,总是会马上提交执行- 有界的任务队列:有界的任务队列可以使用
ArrayBlockingQueue
实现- 无界的任务队列:有界任务队列可以使用
LinkedBlockingQueue
实现- 优先任务队列:优先任务队列通过
PriorityBlockingQueue
实现,它其实是一个特殊的无界队列,PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行
线程设置越多越好吗?设置到什么值比较合理?
锁
CAS实现机制?
内存中value的偏移量
long valueOffset = Unsafe.getUnsafe().objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
AI生成项目java运行
- 1
- 2
- 3
- 4
- 5
- 6
通过本地方法
Unsafe.getUnsafe().objectFieldOffset
获取 值 在内存中的偏移量;然后又通过本地方法unsafe.compareAndSwapInt
去更新数据; 如果内存中的值跟期望中的值一样则 修改成update;
CAS的ABA问题
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化
所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference
来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更
算法
有哪些常用的排序算法?
冒泡算法、选择排序、插入排序、希尔排序、归并排序、快速排序
RPC框架 DUBBO
Dubbo缺省协议采用单一长连接和NIO异步通讯
适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
dubbo请求流程
- client一个线程调用远程接口,生成一个唯一的ID(比如一段随机字符串,UUID等),Dubbo是使用AtomicLong从0开始累计数字的
- 将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象callback,全部封装在一起,组成一个对象object
- 向专门存放调用信息的全局ConcurrentHashMap里面put(ID, object)
- 将ID和打包的方法调用信息封装成一对象connRequest,使用IoSession.write(connRequest)异步发送出去
- 当前线程再使用callback的get()方法试图获取远程返回的结果,在get()内部,则使用synchronized获取回调对象callback的锁, 再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态。
- 服务端接收到请求并处理后,将结果(此结果中包含了前面的ID,即回传)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到ID,再从前面的ConcurrentHashMap里面get(ID),从而找到callback,将方法调用结果设置到callback对象里。
- 监听线程接着使用synchronized获取回调对象callback的锁(因为前面调用过wait(),那个线程已释放callback的锁了),再notifyAll(),唤醒前面处于等待状态的线程继续执行(callback的get()方法继续执行就能拿到调用结果了),至此,整个过程结束。
dubbo 各个模块?
Service 业务层:业务代码的接口与实现
config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 Spring 解析配置生成配置类。
proxy 服务代理层:服务接口透明代理
registry 注册中心层:封装服务地址的注册与发现
cluster 路由层:封装多个提供者的路由及负载均衡
monitor 监控层:RPC 调用次数和调用时间监控
如果zookeeper挂掉了,dubbo还能正常运行吗?
能,本地有保存一份数据;
Dubbo 使用什么通信框架?
在 Dubbo 的最新版本,默认使用 Netty4 的版本
当然你也可以通过SPI 选择Netty3 Mina Grizzly
Dubbo 支持哪些序列化方式?
【重要】Hessian2 :基于 Hessian 实现的序列化拓展。dubbo:// 协议的默认序列化方案
Dubbo :Dubbo 自己实现的序列化拓展
还有Kryo 、FST、JSON、NativeJava、CompactedJava
Dubbo 有哪些集群容错策略?
Failover Cluster[默认]:
失败自动重试其他服务的策略。
Failover Cluster :
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。
Failfast Cluster:
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster:
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster:
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster:
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。
Broadcast Cluster:
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
Dubbo 服务如何做降级?
- Dubbo 原生自带的服务降级功能
- 引入支持服务降级的组件 比如 Alibaba Sentinel
Dubbo 如何做限流?
- Dubbo 原生自带的限流功能,通过 TpsLimitFilter 实现,仅适用于服务提供者
- 引入支持限流的组件 例如
Sentine
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
如何自己设计一个类似 Dubbo 的 RPC 框架?
举个栗子,我给大家说个最简单的回答思路:
- 上来你的服务就得
去注册中心注册
吧,你是不是得有个注册中心,保留各个服务的信心,可以用 zookeeper 来做,对吧。- 然后你的消费者需要去
注册中心
拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上。- 接着你就该发起一次请求了,咋发起?当然是基于
动态代理
了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。- 然后找哪个机器发送请求?那肯定得有个
负载均衡
算法了,比如最简单的可以随机轮询是不是。- 接着找到一台机器,就可以跟它发送请求了,第一个问题咋发送?你可以说用
netty
了,nio 方式;第二个问题发送啥格式数据?你可以说用 hessian序列化协议
了,或者是别的,对吧。然后请求过去了。- 服务器那边一样的,需要针对你自己的服务生成一个动态代理,
监听某个网络端口了
,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。
这就是一个最最基本的 rpc 框架的思路,先不说你有多牛逼的技术功底,哪怕这个最简单的思路你先给出来行不行?
dubbo SPI 机制 与 JAVA SPI?
Zookeeper
zookeeper快速选举描述一下?
- 每个事务,会分配全局唯一的递增id(
zxid,64位:epoch + 自增 id
),每次一个leader被选出来,它都会有一 个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。leader选举是保证分布式数据一致性的关键。
当zk集群中的一台服务器出现以下两种情况之一时,就会开始leader选举。
(1)服务器初始化启动。
(2)服务器运行期间无法和leader保持连接。
而当一台机器进入leader选举流程时,当前集群也可能处于以下两种状态。
(1)集群中本来就已经存在一个leader。
(2)集群中确实不存在leader。
首先第一种情况,通常是集群中某一台机器启动比较晚,在它启动之前,集群已经正常工作,即已经存在一台leader服务器。当该机器试图去选举leader时,会被告知当前服务器的leader信息,它仅仅需要和leader机器建立连接,并进行状态同步即可。
开始选举
sid:
即server id,用来标识该机器在集群中的机器序号。
zxid:
即zookeeper事务id号。
ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id,,该id称为zxid.,由于zxid的递增性质, 如果zxid1小于zxid2,,那么zxid1肯定先于zxid2发生。创建任意节点,或者更新任意节点的数据, 或者删除任意节点,都会导致Zookeeper状态发生改变,从而导致zxid的值增加。
以(sid,zxid)的形式来标识一次投票信息。
(1)初始阶段,都会给自己投票。
(2)当接收到来自其他服务器的投票时,都需要将别人的投票和自己的投票进行pk,规则如下:
优先检查zxid。zxid比较大的服务器优先作为leader。如果zxid相同的话,就比较sid,sid比较大的服务器作为leader。
有了解过zk的watch机制吗?
客户端watcher 可以监控节点的数据变化以及它子节点的变化,一旦这些状态发生变化,zooKeeper服务端就会通知所有在这个节点上设置过watcher的客户端 ,从而每个客户端都很快感知,它所监听的节点状态发生变化,而做出对应的逻辑处理。
watch对节点的监听事件是一次性的
那你说说Zookeeper有哪些应用场景?
- 数据发布与订阅
- 命名服务:作为分布式命名服务,命名服务是指通过指定的名字来获取资源或者服务的地址,利用ZooKeeper创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。
- 配置管理
- 集群管理: 所谓集群管理就是:是否有机器退出和加入、选举master。
- 分布式锁
- 分布式队列:
生产者通过在queue节点下创建顺序节点来存放数据,消费者通过读取顺序节点来消费数据。
zookeeper实现分布式锁怎么实现?
- 创建临时顺序节点
- 判断自己是不是最小值,是则获取了锁
- 用watch自己前面的一个节点;如果前面的节点删除了,则节点收到通知之后,立马判断自己是不是最小的节点,如果是则获取锁;如果不是,则watch它前面的一个节点
每个watch只会通知一次,锁具有顺序性,并且watch自己前面的一个节点是为了避免羊群效应
zookeeper集群可以部署2台吗?
Redis
redis是单线程还是双线程?
家所熟知的 Redis 确实是单线程模型,
指的是执行 Redis 命令的核心模块是单线程的
,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的。
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的
`,所以Redis才叫单线程模型。
Redis 不仅仅是单线程
一般来说 Redis 的瓶颈并不在 CPU,而在内存和网络。如果要使用 CPU 多核,可以搭建多个 Redis 实例来解决。
其实,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象
、以及通过 Redis 模块实现的阻塞命令等。
Redis6.0为什么网络处理要引入多线程?
内存不够的话,可以加内存或者做数据结构优化和其他优化等
但网络的性能优化才是大头
,网络 IO 的读写在 Redis 整个执行期间占用了大部分的 CPU 时间,如果把网络处理这部分做成多线程处理方式,那对整个 Redis 的性能会有很大的提升。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程
为什么redis的性能高?什么决定的?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;(但是redis6.0已经开始使用多线程了,不过是在网络层面)
- 使用多路 I/O 复用模型,非阻塞 IO;