1、HashMap的put方法处理逻辑以及线程不安全体现的场景,基于HashMap实现线程安全该怎么改代码,hashMap在jdk7和jdk8的扩容过程,ConcurrentHashMap的jdk7和jdk8的实现原理
HashMap的底层数据结构、hash冲突如何处理是基础的一定要了解
#1.在jdk1.7中,在多线程环境下,扩容时因为采用头插法,会造成环形链或数据丢失。
#2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
HashMap resize时JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位
ConcurrentHashMap 和JDK6不同,JDK7中除了第一个Segment之外,剩余的Segments采用的是延迟初始化的机制:每次put之前都需要检查key对应的Segment是否为null,如果是则调用ensureSegment()以确保对应的Segment被创建
ConcurrentHashMap在JDK7和JDK8中的不同实现原理
2、ReentrantLock
ReentrantLock 源码分析 :http://www.blogjava.net/zhanglongsr/articles/356782.html
1)等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
2)公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
总结
ReentrantLock在采用非公平锁构造时,首先检查锁状态,如果锁可用,直接通过CAS设置成持有状态,且把当前线程设置为锁的拥有者。
如果当前锁已经被持有,那么接下来进行可重入检查,如果可重入,需要为锁状态加上请求数。如果不属于上面两种情况,那么说明锁是被其他线程持有,当前线程应该放入等待队列。 在放入等待队列的过程中,首先要检查队列是否为空队列,如果为空队列,需要创建虚拟的头节点,然后把对当前线程封装的节点加入到队列尾部。由于设置尾部节点采用了CAS,为了保证尾节点能够设置成功,这里采用了无限循环的方式,直到设置成功为止(?这里是不是由于有可能有其他节点同时在尝试加入队列,所以称之为非公平,这里没有保证入队的先后顺序?)。
在完成放入等待队列任务后,则需要维护节点的状态,以及及时清除处于Cancel状态的节点,以帮助垃圾收集器及时回收。如果当前节点之前的节点的等待状态小于1,说明当前节点之前的线程处于等待状态(挂起),那么当前节点的线程也应处于等待状态(挂起)。挂起的工作是由LockSupport类支持的,LockSupport通过JNI调用本地操作系统来完成挂起的任务(java中除了废弃的suspend等方法,没有其他的挂起操作)。在当前等待的线程,被唤起后,检查中断状态,如果处于中断状态,那么需要中断当前线程。
3、类加载机制,能否自定义一个java.lang.Thread
类的初始化过程:加载 校验 准备 解析 初始化
双亲委派:向上委托,向下查找
为对象分配空间,分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题,2种方案
1)CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
2)TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
JAVA能否自定义一个加载器? https://blog.csdn.net/sufu1065/article/details/106760850
4、写代码实现OOM和StackOverflow
https://blog.csdn.net/universe_ant/article/details/52996398
1)OOM 对象数量过多
2)SOF 递归不退出 死循环
5、JVM的内存结构,GC的种类及使用场景、GCRoots有哪些、minorGC和FullGC
堆:新生代(eden from to 8:1:1) 老年代
程序计数器
堆:存放对象实例,可能OOM,GC区域
栈:存放局部变量,方法出口等,栈帧出入栈,OOM SOF
方法区:常量,已加载的类信息,静态变量,OOM
JDK8移除永久代,新增元空间,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
原因:
1)字符串存在永久代中,容易出现性能问题和内存溢出。
2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4)Oracle 可能会将HotSpot 与 JRockit 合二为一。
GC调优:https://blog.csdn.net/JAVA_I_want/article/details/103071760
GC优化的核心思路在于:尽可能让对象在新生代中分配和回收,尽量避免过多对象进入老年代,导致对老年代频繁进行垃圾回收,同时给系统足够的内存减少新生代垃圾回收次数。
JVM相关命令:jstat -gc <统计间隔时间> <统计次数>
GC算法:分代处理机制、标记-复制(新生代)、标记-整理/压缩(老年代)、引用计数,各自优缺点
GC收集器种类:
1)serial:单线程,效率高,要停工作线程,新生代复制,老年代标记-整理
2)parallel
3)concurrent(CMS):标记-清除,老年代, 尽可能让GC线程和用户线程并发执行,以达到降低STW时间的目的
4)G1收集器:最新的,标记-整理/复制,没有内存碎片,可预测停顿,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是: 实现可预期及可配置的STW停顿时间
JVM: GC过程总结(minor GC 和 Full GC)
GC种类:
1)minorGC:新生代gc(复制),eden区满,每发生一次对象年龄+1,超过阈值对象被promote到老年代
2)majorGC:老年代GC(标记-清除/标记-整理),system.gc()/老年代空间不足/方法区空间不足/minorGC后进入老年代的平均大小大于老年代的可用内存
full GC为什么慢:老年代的回收采用标记-压缩算法,是空间换时间的做法。full gc在整个heap上(甚至包括永生代,或者说新的metaspace)
这几个都是 GCRoots:
1)两个栈: Java栈 和 Native 栈中所有引用的对象;
2)两个方法区:方法区中的常量和静态变量;
3)所有线程对象;
4)所有跨代引用对象;
5)和已知 GCRoots 对象同属一个CardTable 的其他对象。
6、反射的原理、应用场景
https://zhuanlan.zhihu.com/p/162971344
调用反射的总体流程如下:
1)准备阶段:编译期装载所有的类,将每个类的元信息保存至Class类对象中,每一个类对应一个Class对象
2)获取Class对象:调用x.class/x.getClass()/Class.forName() 获取x的Class对象clz(这些方法的底层都是native方法,是在JVM底层编写好的,涉及到了JVM底层,就先不进行探究了)
3)进行实际反射操作:通过clz对象获取Field/Method/Constructor对象进行进一步操作
4)Class类中包含的ReflectionData,用于保存进行反射操作的基础信息
应用场景:
Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
反射会有性能问题,最终调用的都是native方法,解决方案:
1)对通过反射调用获得的Class、Method、Field实例进行缓存,避免多次Dynamic Resolve。
2)Java 7开始提供了java.lang.invoke.MethodHandle类,MethodHandle类的安全性验证在获取实例时进行而不是每次调用时都要进行验证,减小开销。
3)使用Runtime创建的类
7、Lsit、Set、Map数据结构
https://blog.csdn.net/qq_28033239/article/details/98204664
1)List
Arraylist: Object数组(动态数组)
Vector: Object数组
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
2)Set
HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素,查找O(1)
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树,查找O(logn))
3)Map
红黑树特点:根节点黑色、红色节点的子节点为黑色、任意节点到叶子节点经过的黑色节点相同、叶子节点为黑色
8、String、StringBuilder、StringBuffer区别
1)String实现了equals()方法和hashCode()方法,new String("java").equals(new String("java"))的结果为true;而StringBuffer没有实现equals()方法和hashCode()方法,因此,new StringBuffer("java").equals(new StringBuffer("java"))的结果为false,将StringBuffer对象存储进Java集合类中会出现问题。
2)StringBuilder是线程不安全的,StringBuffer是线程安全的
9、Java IO/NIO/AIO
https://www.cnblogs.com/sxkgeek/p/9488703.html
区别:
1)标准IO
基于阻塞的 I/O 操作:在等待数据写入或写入都会引起阻塞,即当线程调用 write(), read() 时,线程会被阻塞,直到数据被完全读取或写入;
面向流:使用流来传输数据,这种方式的 I/O 操作速度比较慢;
2)NIO
NIO详解:https://www.cnblogs.com/pony1223/p/8138233.html
基于非阻塞的 I/O 操作(同步非阻塞):不等待数据的读取或写入,允许调用线程请求向通道写入数据,而不等待数据被完全写入;NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程
面向缓存:使用通道(channel)和缓冲区(buffer)来传输数据,数据被读取入缓冲区,使用通道进一步进行处理;
3)AIO
基于回调和事件机制,在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。
Java NIO 系统的核心组件
通道和缓冲区(Channel and Buffer):NIO 使用通道和缓冲区传输数据,数据总是从缓冲区写入通道,并从通道读取到缓冲区;
选择器(Selectors):NIO 的选择器用于监视多个通道的状态(如数据到达,连接打开等),通过选择器,单线程可以监视多个通道中的数据;
10、工厂+策略模式
工厂+策略:https://blog.csdn.net/weixin_36167744/article/details/110522413
各种设计模式:https://www.runoob.com/design-pattern/abstract-factory-pattern.html
11、序列化与反序列化
https://blog.csdn.net/Cavewang/article/details/119257739
writeObject和readObject——重写这2个方法来自定义序列化和反序列化
考察一个序列化框架一般会关注以下几点:
1)Encoding format。是 human readable 还是 binary。
2)Schema declaration。也叫作契约声明,基于 IDL,比如 Protocol Buffers/Thrift,还是自描述的,比如 JSON、XML。另外还需要看是否是强类型的。
3)语言平台的中立性。比如 Java 的 Native Serialization 就只能自己玩,而 Protocol Buffers 可以跨各种语言和平台。
4)新老契约的兼容性。比如 IDL 加了一个字段,老数据是否还可以反序列化成功。
5)和压缩算法的契合度。跑 benchmark 和实际应用都会结合各种压缩算法,例如 gzip、snappy。
6)性能。这是最重要的,序列化、反序列化的时间,序列化后数据的字节大小是考察重点。
12、JVM内存调优常用命令
https://blog.csdn.net/lin_litao/article/details/80788919
1)jps :JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 (用这个查进程IP)
2)jstat(JVM statistics Monitoring):是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。(-gc -gcutil -gccause)
3)jmap(JVM Memory Map):用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
4)jhat(JVM Heap Analysis Tool):是与jmap搭配使用,用来分析jmap生成的dump
5)jstack:用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
6)jinfo(JVM Configuration info):这个命令作用是实时查看和调整虚拟机运行参数。 之前的jps -v口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用jinfo口令
13、OOM了怎么排查
https://mp.weixin.qq.com/s/bClGUG32QQmdgtnpyo431Q
可能原因:
(1)内存确实分配过小,内存确实不够用;(2)某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽;(3)某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接;
检查方法:
1)确认是不是内存本身就分配过小方法:jmap -heap + PID
2)找到最耗内存的对象方法:jmap -histo:live PID | more
会打印(1)实例数;(2)所占内存大小;(3)类名;
3)确认是否是资源耗尽工具:(1)pstree(2)netstat
https://blog.csdn.net/ywlmsm1224811/article/details/91866707
OOM一般有以下两种情况:
1、年老代堆空间被占满
异常:java.lang.OutOfMemoryError:java heap space
说明:这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机再也无法分配新空间
解决方案:这种方式解决起来比较简单,一般就是根据垃圾回收前后的情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
2、持久代被占满
异常:java.lang.OutOfMemoryError:PermGen space
说明:Perm 空间被占满,无法为新的 class 分配存储空间而引发的异常。这个异常以前是没有的,但是在 java 大量使用反射的今天这个异常就比较常见了。主要原因是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。更可怕的是,不同的 classLoader 即便使用相同的类,但是都会对其进行加载,相当于同一个东西,如果有 N 个classLoader 那么它将会被加载 N 次。因此,在某些情况下,这个问题基本视为无解,当然,存在大量 classLoader 和大量反射类的情况并不多
解决方案:增加持久代内存 ,例如:-XX:MaxPermSize=16M
14、设计模式六(七?)大原则
面向对象的设计模式有七大基本原则:(SOLID)
开闭原则(首要原则):要对扩展开放,对修改关闭
单一职责原则:实现类要职责单一
里氏代换原则:不要破坏继承体系
依赖倒转原则:面向接口编程
接口隔离原则:设计接口要精简单一
合成/聚合复用原则:尽量使用聚合,组合,而不是继承
最少知识原则或者迪米特法则:降低耦合
15、Java中的引用类型
https://www.jianshu.com/p/fcc09b2eb006
Java中存在四种引用,它们由强到弱依次是:强引用、软引用、弱引用、虚引用。
16、JDK动态代理和CGLib的区别
www.cnblogs.com/sandaman2019/p/12636727.html
1)JDK动态代理:利用拦截器(必须实现InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
2) Cglib动态代理:利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理
Cglib比JDK快?
1)cglib底层是ASM字节码生成框架,但是字节码技术生成代理类,在JDL1.6之前比使用java反射的效率要高
2)在jdk6之后逐步对JDK动态代理进行了优化,在调用次数比较少时效率高于cglib代理效率
3)只有在大量调用的时候cglib的效率高,但是在1.8的时候JDK的效率已高于cglib
4)Cglib不能对声明final的方法进行代理,因为cglib是动态生成代理对象,final关键字修饰的类不可变只能被引用不能被修改
Spring如何选择是用JDK还是cglib?
1)当bean实现接口时,会用JDK代理模式
2)当bean没有实现接口,用cglib实现
3)可以强制使用cglib(在spring配置中加入<aop:aspectj-autoproxy proxyt-target-class=”true”/>)
17、深拷贝、浅拷贝
www.cnblogs.com/xinruyi/p/11537963.html
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
Object 类提供的 clone 是只能实现 浅拷贝的。