Java


volatile,hashmap, hashtable, ConcurrentHashMap, thread-pool,
lock, invoke, reflect, Stream, optional, lambada,netty,log,nio,jmx, Object Monitor和AQS,synchronized实现原理,Callable, FutureTask, 内部类,InputStream/OutputStream,class loader, ThreadLocal内存泄露,Spliterator,parallelStream,Method,动态代理,异常体系,

tools: JStack, JConsole,VisualVM, fastthread.io,


ServiceProviderInterface,SPI
常见的 SPI 有 JDBC、JNDI、JAXP 等。

 


异常:
异常链:Exception(String message, Throwable cause)
try-with-resources:try后面引入了一对小括号(),用于在其中完成资源的初始化

常见的RuntimeException异常:
- NullPointerException 空指针异常
- ArithmeticException 出现异常的运算条件时,抛出此异常
- IndexOutOfBoundsException 数组索引越界异常
- ClassNotFoundException 找不到类异常
- IllegalArgumentException(非法参数异常)
常见的 Checked Exception 异常:
- IOException (操作输入流和输出流时可能出现的异常)
- ClassCastException(类型转换异常类)
Checked Exception就是编译器要求你必须处置的异常。
与之相反的是,Unchecked Exceptions,它指编译器不要求强制处置的异常,它包括Error和RuntimeException 以及他们的子类。

不管有没有发生异常,finally都会执行到。
即使try和catch中有return时,finally仍然会执行
finally是在return后面的表达式运算完后再执行的。(此时并没有返回运算后的值,而是先把要返回的值保存起来,若finally中无return,则不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),该情况下函数返回值是在finally执行前确定的)
finally部分就不要return了,要不然,就回不去try或者catch的return了。

 

 

 

volatile:
使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)

 

 


ThreadPool:
ThreadPoolExecutor自己已经提供了四个拒绝策略,分别是CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy

核心线程和非核心线程轮询执行队列里的任务。
线程池不区分核心线程和非核心线程,只记录核心线程数量和最大数量。

 

 

wait,notify,notifyAll
wait()通过ObjectMonitor::exit方法释放当前的ObjectMonitor对象,这样其它竞争线程就可以获取该ObjectMonitor对象,最终通过底层的park()方法挂起线程。
在jdk的notify()方法注释中说明的是随机唤醒一个线程,这里其实是第一个ObjectWaiter节点。
notify()和notifyAll()并不会释放所占有的ObjectMonitor对象,它们的主要工作是将相应的线程从_WaitSet转移到_EntryList中,然后等待竞争获取锁。其实真正释放ObjectMonitor对象的时间点是在执行monitorexit指令,一旦释放ObjectMonitor对象后,_EntryList中ObjectWaiter节点所保存的线程就可以竞争ObjectMonitor对象进行加锁操作了。

https://blog.csdn.net/qq_38293564/article/details/80432875

Java中 wait,notify方法需要在 synchronized 的方法中调用

 

join,sleep,yield
sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程此时获取CPU占有权。
yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会。

 

不能覆盖静态方法:
虽然可以在子类中声明一个具有相同名称和方法签名的方法,看起来可以在Java中覆盖静态方法,但实际上这是方法隐藏。Java不会在运行时解析方法调用,并且根据用于调用静态方法的 Object 类型,将调用相应的方法。这意味着如果你使用父类的类型来调用静态方法,那么原始静态将从父类中调用,另一方面如果你使用子类的类型来调用静态方法,则会调用来自子类的方法。

 

序列化:
如果不显式声明 SerialVersionUID, 则 JVM 会根据类结构生成其结构, 该结构依赖于类实现接口和可能更改的其他几个因素。 假设你新版本的类文件实现的另一个接口, JVM 将生成一个不同的 SerialVersionUID 的, 当你尝试加载旧版本的程序序列化的旧对象时, 你将获得无效类异常 InvalidClassException。

瞬态transient和静态static变量不会序列化。

自定义序列化过程:编写readObject和writeObject完成部分属性的序列化。反射实现:
Java调用ObjectOutputStream类检查其是否有私有的,无返回值的writeObject方法,如果有,其会委托该方法进行对象序列化。
writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class },Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject", new Class<?>[] { ObjectInputStream.class }, Void.TYPE);

Externalizable继承自Serializable,使用Externalizable接口需要实现writeExternal以及readExternal方法
Externalizable接口的实现方式一定要有默认的无参构造函数,反序列化不用反射,而是创建对象,再调用readExternal方法。Serializable接口实现,其采用反射机制完成内容恢复,没有一定要有无参构造函数的限制。
采用Externalizable无需产生序列化ID(serialVersionUID)~而Serializable接口则需要。
相比较Serializable, Externalizable序列化、反序列更加快速,占用相比较小的内存。

兼容性:
问题一:假设有A端和B端,如果2处的serialVersionUID不一致,会产生什么错误呢?
1)先执行测试类SerialTest,然后修改serialVersion值(或注释掉serialVersion并编译),再执行测试类DeserialTest,报错:
java.io.InvalidClassException: com.test.serializable.Serial; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 11
2)A端和B端都没显示的写serialVersionUID,实体类没有改动(如果class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释,等等),).序列化,反序列化正常.
问题二:假设2处serialVersionUID一致,如果A端增加一个字段,B端不变,会是什么情况呢?
答案二: 序列化,反序列化正常,A端增加的字段丢失(被B端忽略).
问题五:假设2处serialVersionUID一致,如果B端减少一个字段,A端不变,会是什么情况呢?
答案:与问题二类似,序列化,反序列化正常,B端字段少于A端,A端多的字段值丢失(被B端忽略).
问题三:假设2处serialVersionUID一致,如果B段增加一个字段,A端不变,会是什么情况呢?
答案三: 序列化,反序列化正常,B端新增加的int字段被赋予了默认值0.
问题四:假设2处serialVersionUID一致,如果A端减少一个字段,B端不变,会是什么情况呢?(与问题三类似,四答案:序列化,反序列化正常,B端字段多余A端,B端多出的字段被赋予对应类型的默认值)
原文链接:https://blog.csdn.net/y874961524/article/details/51412585

 

 


单例:
双重检查锁定:须使用volatile,否则多线程懒加载可能创建多个实例。
静态工厂方法:线程安全
枚举的单例(最佳实现):创建枚举实例是线程安全的,但 Enum 上的任何其他方法是否线程安全都是程序员的责任。序列化由 JVM 进行。
Singleton.INSTANCE.doSomething();

传统单例可能有反射攻击或者反序列化攻击
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
Singleton newSingleton = constructor.newInstance();

byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
解决:实现private Object readResolve(),ObjectOutputStream会调用

 

 

 


HashMap:无序,LinkedHashMap:插入顺序或访问顺序,TreeMap:自然顺序或自定义顺序
LinkedHashMap与LRU
TreeMap
SortedMap,NavigatableMap

 

 

ConcurrentHashMap:
1.7:
16个segment
put利用 scanAndLockForPut() 自旋获取锁。重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。当出现有key,但是没有value的情况时,将加lock锁,等待value值写入,再读取,防止读不到最新的值。
1.8
将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
并发控制使⽤synchronized 和 CAS 来操作。
JDK1.8的Nod节点中value和next都用volatile修饰,保证并发的可见性。
synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
数组用volatile修饰主要是保证在数组扩容的时候保证可见性。

 

 


mybatis

Apollo配置中心,分布式id生成器,

 

JDK 5时加入的自动装箱、泛型、动态注解、枚举、变长参数、遍历循环(foreach循环);
JDK 8时加入的Lambda表达式、Stream API、接口默认方法,try-resources语法

1.7之后,字符串常量池迁移到堆区


command:
javap


注解处理器


锁:
乐观锁,悲观锁
阻塞,不阻塞(自旋,适应性自旋)
多线程竞争:无锁,偏向锁,轻量级锁(自旋),重量级锁(阻塞)
公平锁,非公平锁
可重入,不可重入
共享锁,排他锁


占有锁的线程释放锁一般会是以下三种情况之一:
1:占有锁的线程执行完了该代码块,然后释放对锁的占有;
2:占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
3:占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。


Lock和Synchronized对比:
Synchronized不能被中断。
synchronized就是非公平锁。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行。Lock的实现基于juc的基石AQS。
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。


synchronized:
可重入、非公平。块结构(Block Structured)的同步语法,在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。

ReentrantLock:
与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁及锁可以绑定多个条件。
默认非公平。一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降。
ReentrantLock在功能上是synchronized的超集,在性能上又至少不会弱于synchronized。
synchronized是在Java语法层面的同步。Lock应该确保在finally块中释放锁,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息。


java.util.concurrent.locks.Lock
临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)
根据《Java虚拟机规范》的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。

Condition newCondition()
tryLock(long time, TimeUnit unit)


ReadWriteLock:
ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得,读写锁之间为互斥。
ReentrantReadWriteLock支持锁降级,当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的,会导致死锁。


等待唤醒机制发展:
Object的wait/notify
Condition的await和signal
LockSupport的park和unpark

 

非阻塞:
指令:
·测试并设置(Test-and-Set);
·获取并增加(Fetch-and-Increment);
·交换(Swap);
·比较并交换(Compare-and-Swap,下文称CAS);
·加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)。

 

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

 

 

 

容器:
vector

 


类加载:
加载,验证,准备,解析,初始化

验证:1.文件格式验证;2.元数据验证;3.字节码验证;4.符号引用验证;
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

Bootstrap Class Loader <- Extension Class Loader <- Application Class Loader <- User Class Loader

破坏双亲委派模型:
1、线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
SPI的加载:这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。
2、在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
3、双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

 

 

 


垃圾收集器不可能三角:低内存占用,高吞吐量,低延迟(一般收集器目标)。


新生代(标记清除+标记复制):Serial,parNew,parallel Scavenge
年老代:CMS(标记清除),Serial Old(标记整理),Parallel Old(标记整理)
Mixed GC:G1
FullGC:不移动大对象

和用户线程并发:CMS, G1
低延迟:CMS,G1
高吞吐:


新生代参数:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure


Serial:客户端模式使用,内存消耗最小

ParNew:-XX:+/-UseParNewGC选项来强制指定或者禁用它

Parallel Scavenge:目标是达到一个可控制的吞吐量。-XX:MaxGCPauseMillis,-XX:GCTimeRatio,-XX:+UseAdaptiveSizePolicy自适应调节策略

CMS:-XX:+UseConcMarkSweepGC
与CMS配合的新生代:Serial,parNew
1)初始标记(CMS initial mark):和GCRoots关联的,很快
2)并发标记(CMS concurrent mark):增量更新
3)重新标记(CMS remark):处理增量更新记录。
4)并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。
并发标记和并发清理阶段产生“浮动垃圾”(Floating Garbage),下一次清理。
参数-XX:CMSInitiatingOccupancyFraction:触发CMS比率,设置得太高将会很容易导致大量的并发失败产生。
“并发失败”(Concurrent Mode Failure):这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
空间碎片多,触发FullGC:
-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程。
-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。


G1(Garbage First):可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集
停顿时间模型(-XX:MaxGCPauseMillis指定,默认值是200毫秒):region是回收最小单元,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。价值即回收所获得的空间大小以及回收所需时间的经验值。
追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。
G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的。
它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。

每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间

1)初始标记:只是标记一下GC Roots能直接关联到的对象
2)并发标记:只有这个阶段可以和用户线程并发。
3)最终标记:处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4)筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以*选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。


跨region引用:记忆集
同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。

并发标记阶段:
引用关系改变:原始快照(SATB)
新建对象:每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,新对象在两个指针之上。

 

三色标记和用户线程并行导致错误,以下两点同时发生:
1、黑色增加对白色引用
2、灰色减少对白色引用


full gc:
g1 停顿时间太短

 

JVM:
safe point, Mark Word, memory barrier,
统一垃圾收集器接口

 

 

final:
修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
修改类时,该类成为最终类,无法被继承。

 

 

反射:
getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。
getMethod:获取当前类和父类的所有public的方法。这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。

 

静态代理:每个方法都要重写
动态代理:可以把代理行为应用到多个方法
JDK动态代理:根据代理目标实现的接口,动态生成实现这些接口的代理对象。代理对象调用每个接口方法,把这个方法(Method类型)传给InvocationHandler。InvocationHandler可以判断方法名称不同反应。

 

 

 

内存泄漏排查:

内存泄露解决的原则:
1.尽量减少使用静态变量,类的静态变量的生命周期和类同步的。
2.声明对象引用之前,明确内存对象的有效作用域,尽量减小对象的作用域,将类的成员变量改写为方法内的局部变量;
3.减少长生命周期的对象持有短生命周期的引用;
4.使用StringBuilder和StringBuffer进行字符串连接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可变的字符串,后两者表示可变的字符串。如果使用多个String对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
5.对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;
6.各种连接(数据库连接,网络连接,IO连接)操作,务必显示调用close关闭。


频繁FullGC:
内存泄漏

上一篇:我们一起来回顾一下Synchronized关键字吧


下一篇:线程相关面试题