本项目是本人参加BAT等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。
JVM虚拟机内存模型实现规范:
按线程是否共享分为以下区域:
所有线程共享的数据区:
每个线程都会有一块私有的数据区:
以HotSpot虚拟机实现为例,Java8中内存区域如下:
与规范中的区别:
逃逸分析:
逃逸是指在某个方法之内创建的对象除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收。由于其被其它变量引用,由于无法回收,即称为逃逸。
逃逸分析技术可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象可以直接在栈上分配提高对象分配回收效率,对象占用的空间会随栈帧的出栈而销毁。
加载过程:
类加载器:
启动类加载器:用C++语言实现,是虚拟机自身的一部分,它负责将
扩展类加载器:用Java语言实现,它负责加载
系统类加载器:用Java语言实现,它负责加载系统类路径ClassPath指定路径下的类库,开发者可以直接使用
双亲委派:
定义:如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优点:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次防止恶意覆盖Java核心API。
三次大型破坏双亲委派模式的事件:
新生代
进入条件
优先选择在新生代的Eden区被分配。
老年代
进入条件
回收对象
不可达对象:通过一系列的GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时则此对象是不可用的。
GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。
彻底死亡条件:
条件1:通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
条件2:在finalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。
如何回收
新生代因为每次GC都有大批对象死去,只需要付出少量存活对象的复制成本且无碎片所以使用“复制算法”
老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法
复制算法:将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉。这样没有内存碎片。
标记-清除:首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。会产生大量碎片,导致无法分配大对象从而导致频繁GC。
标记-整理:首先标记出所有需要回收的对象,让所有存活的对象向一端移动。
Minor GC条件
当Eden区空间不足以继续分配对象,发起Minor GC。
Full GC条件
串行收集器Serial是最古老的收集器,只使用一个线程去回收,可能会产生较长的停顿
新生代使用Serial收集器复制
算法、老年代使用Serial Old标记-整理
算法
参数:-XX:+UseSerialGC
,默认开启-XX:+UseSerialOldGC
并行收集器Parallel关注可控的吞吐量,能精确地控制吞吐量与最大停顿时间是该收集器最大的特点,也是1.8的Server模式的默认收集器,使用多线程收集。ParNew垃圾收集器是Serial收集器的多线程版本。
新生代复制
算法、老年代标记-整理
算法
参数:-XX:+UseParallelGC
,默认开启-XX:+UseParallelOldGC
并发收集器CMS是以最短停顿时间为目标的收集器。G1关注能在大内存的前提下精确控制停顿时间且垃圾回收效率高。
CMS针对老年代,有初始标记、并发标记、重新标记、并发清除四个过程,标记阶段会Stop The World,使用标记-清除
算法,所以会产生内存碎片。
参数:-XX:+UseConcMarkSweepGC
,默认开启-XX:+UseParNewGC
G1将堆划分为多个大小固定的独立区域,根据每次允许的收集时间优先回收垃圾最多的区域,使用标记-整理
算法,是1.9的Server模式的默认收集器
参数:-XX:+UseG1GC
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互
STW总会发生,不管是新生代还是老年代,比如CMS在初始标记和重复标记阶段会停顿,G1在初始标记阶段也会停顿,所以并不是选择了一款停顿时间低的垃圾收集器就可以避免STW的,我们只能尽量去减少STW的时间。
那么为什么一定要STW?因为在定位堆中的对象时JVM会记录下对所有对象的引用,如果在定位对象过程中,有新的对象被分配或者刚记录下的对象突然变得无法访问,就会导致一些问题,比如部分对象无法被回收,更严重的是如果GC期间分配的一个GC Root对象引用了准备被回收的对象,那么该对象就会被错误地回收。
定义:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性
实现:volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字
主内存:所有变量都保存在主内存中
工作内存:每个线程的独立内存,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行
每个线程都有自己的本地内存共享副本,如果A线程要更新主内存还要让B线程获取更新后的变量,那么需要:
我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是happens-before。特别关注在多线程之间的内存可见性。
它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。
前提:在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间
目的:优化JVM垃圾收集性能从而增大吞吐量或减少停顿时间,让应用在某个业务场景上发挥最大的价值。吞吐量是指应用程序线程用时占程序总用时的比例。暂停时间是应用程序线程让与GC线程执行而完全暂停的时间段
对于交互性web应用来说,一般都是减少停顿时间,所以有以下方法:
ArrayList | LinkedList |
---|---|
数组 | 双向链表 |
增删的时候在扩容的时候慢,通过索引查询快,通过对象查索引慢 | 增删快,通过索引查询慢,通过对象查索引慢 |
当数组无法容纳下此次添加的元素时进行扩容 | 无 |
扩容之后容量为原来的1.5倍 | 无 |
ConcurrentHashMap演进从Java7到Java8 | 技术世界 | java,concurrenthashmap,java 8,CAS,多线程,并发,技术世界,郭俊 Jason,大数据架构
HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 将 hash 表分为 16 个桶(默认值)
最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment
Java7
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。
Java8
新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡
锁是什么
锁是在不同线程竞争资源的情况下来分配不同线程执行方式的同步控制工具,只有线程获取到锁之后才能访问同步代码,否则等待其他线程使用结束后释放锁
synchronized
通常和wait,notify,notifyAll一块使用。
wait:释放占有的对象锁,释放CPU,进入等待队列只能通过notify/all继续该线程。
sleep:则是释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
notify:唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。
lock
拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁
两者的区别
我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
锁的种类
公平锁是指多个线程按照申请锁的顺序来获取锁
ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁
指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁
Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有
ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的
Synchronized是独享锁
悲观锁在Java中的使用,就是各种锁
乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
针对Synchronized的锁状态:
偏向锁是为了减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。指一段同步代码一直被一个线程所访问,在无竞争情况下把整个同步都消除掉
轻量级锁是为了减少无实际竞争情况下,使用重量级锁产生的性能消耗。指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过CAS自旋的形式尝试获取锁,不会阻塞
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,默认自旋次数为10
自适应自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定,是虚拟机对锁状况的一个预测
功能:
使用ThreadLocal
的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取到当前线程ID,然后得到该线程对象中的Map,然后与Map交互。
起源
new Thread弊端:
线程池时核心参数
创建线程的逻辑
以下任务提交逻辑来自ThreadPoolExecutor.execute方法:
阻塞队列的策略
CountDownLatch
计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。
图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。
使用场景:
并行计算:把任务分配给不同线程之后需要等待所有线程计算完成之后主线程才能汇总得到最终结果
模拟并发:可以作为并发次数的统计变量,当任意多个线程执行完成并发任务之后统计一次即可
Semaphore
信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(3); //配置只能发布3个运行许可证
for (int i = 0; i < 100; i++) {
int finalI = i;
executorService.execute(() -> {
try {
semaphore.acquire(3); //获取3个运行许可,如果获取不到会一直等待,使用tryAcquire则不会等待
Thread.sleep(1000);
System.out.println(finalI);
semaphore.release(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
由于同时获取3个许可,所以即使开启了100个线程,但是每秒只能执行一个任务
使用场景:
数据库连接并发数,如果超过并发数,等待(acqiure)或者抛出异常(tryAcquire)
CyclicBarrier
可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类
与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。
与CountDownLatch的区别:
使用场景: 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。
交替打印奇偶数
public class PrintOddAndEvenShu {
private int value = 0;
private synchronized void printOdd() {
while (value <= 100) {
if (value % 2 == 1) {
System.out.println(Thread.currentThread() + ": -" + value++);
this.notify();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private synchronized void printEven() {
while (value <= 100) {
if (value % 2 == 0) {
System.out.println(Thread.currentThread() + ": --" + value++);
this.notify();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
PrintOddAndEvenShu print = new PrintOddAndEvenShu();
Thread t1 = new Thread(print::printOdd);
Thread t2 = new Thread(print::printEven);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
多线程交替打印alibaba: 记一道阿里多线程笔试题_从入门到脱发的博客-CSDN博客 我自行实现的简单解法:
public class Test {
private int currentI = 0;
private synchronized void printSpace(int inputLength) {
while (true) {
if (currentI == inputLength) {
currentI = 0;
System.out.print(" ");
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private synchronized void printLetter(int i, char c) {
while (true) {
if (i == currentI) {
currentI++;
System.out.print(c);
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private synchronized void print(String input) throws Exception {
Test test = new Test();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
int finalI = i;
Thread tt = new Thread(() -> test.printLetter(finalI, c));
tt.start();
}
Thread space = new Thread(() -> test.printSpace(input.length()));
space.start();
space.join();
}
public static void main(String[] args) throws Exception {
Test test = new Test();
test.print("alibaba");
}
}
Block-IO:InputStream和OutputStream,Reader和Writer。属于同步阻塞模型
同步阻塞:一个请求占用一个进程处理,先等待数据准备好,然后从内核向进程复制数据,最后处理完数据后返回
NonBlock-IO:Channel、Buffer、Selector。属于IO多路复用的同步非阻塞模型
同步非阻塞:进程先将一个套接字在内核中设置成非阻塞再等待数据准备好,在这个过程中反复轮询内核数据是否准备好,准备好之后最后处理数据返回
IO多路复用:同步非阻塞的优化版本,区别在于IO多路复用阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用上。换句话说,轮询机制被优化成通知机制,多个连接公用一个阻塞对象,进程只需要在一个阻塞对象上等待,无需再轮询所有连接
在Java的NIO中,是基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(比如:连接打开,数据到达)
因此,单个线程可以监听多个数据通道,Selector的底层实现是epoll/poll/select的IO多路复用模型,select方法会一直阻塞,直到channel中有事件就绪:
与BIO区别如下:
Asynchronous IO:属于事件和回调机制的异步非阻塞模型
AIO得到结果的方式:
但要实现真正的异步非阻塞IO,需要操作系统支持,Windows支持而Linux不完善
在生产环境中,我们无法通过断点调试、新增log、可视化工具去立马查看当前的运行状态和拿到错误信息,此时,借助Java自带的命令行工具以及相关dump分析工具以及一些小技巧,可以大大提升我们排查问题的效率。
下面会列出一些常用且非常有效的命令以及参数来查看运行时Java程序的信息,从而辅助你了解程序运行状态。还有大量可用的功能由其他参数提供,自行参阅oracle文档
jps -l
查看所有正在运行的Java程序,同时显示启动类类名,获取到PID
4706 org.apache.catalina.startup.Bootstrap
5023 sun.tools.jps.Jps
jinfo -flags PID
查看运行时进程参数与JVM参数
Attaching to process ID 28987, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.171-b11
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2092957696 -XX:MaxNewSize=697303040 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
Command line: -Dspring.config.location=application.properties -Dspring.profiles.active=staging
java -XX:+PrintFlagsFinal -version
查看当前虚拟机默认JVM参数
jstat -gc PID 1000 10
每秒查看一次gc信息,共10次
输出比较多的参数,每个字段的解释参看 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
512.0 512.0 15.3 0.0 4416.0 1055.2 11372.0 7572.5 14720.0 14322.5 1664.0 1522.8 40 0.137 8 0.039 0.176
期间可能碰到提示sun.jvm.hotspot.runtime.VMVersionMismatchException: Supported versions are 24.181-b01. Target VM is 25.171-b11
的问题,原因在于安装了多个版本,使用which
、ls -l
可简介定位到与当前执行Java程序相同的Java版本
内存泄露导致OOM?内存占用异常的高?这是生产环境常常出现的问题,Java提供dump文件供我们对内存里发生过的事情进行了记录,我们需要借助一些工具从中获取有价值的信息。
导出Dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
jmap -dump:format=b,file=heap.hprof PID
分析Dump文件
如果Dump文件不太大的话,可以传到 World-class heap Dump analysis - Java, Android memory dump analyzer 来分析
文件比较大,且想进行更加系统的分析,推荐使用MAT分析,有如下几种常用查看方式
任务长时间不退出?CPU 负载过高?很可能因为死循环或者死锁,导致某些线程一直执行不被中断,但是不报错是最烦人的,所以日志里看不到错误信息,并且又不能用dump文件分析,因为跟内存无关。这个时候就需要用线程分析工具来帮我们了。
导出jstack文件
使用jstack PID > 文件
,如果失败请加-F
参数,如果还失败请使用Java程序启动时使用的用户执行jstack,下面是jstack的部分输出格式
线程名 PID的16进制
"http-nio-8080-Acceptor-0" #17 daemon prio=5 os_prio=0 tid=0x00007fac2c4bd000 nid=0x29f4 runnable [0x00007fac192f6000]
java.lang.Thread.State: RUNNABLE(tomcat的工作线程正在运行,有NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WATING/TERMINATED状态)
at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
- locked <0x00000000faf845a8> (a java.lang.Object)
at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:682)
at java.lang.Thread.run(Thread.java:748)
jstack的输出可以看到所有的线程以及他们的状态,我们就可以看有哪些我们自己创建的正在运行的线程,那很可能就是那个一直在执行的线程了,此时线程名就格外重要了,所以建议创建新线程时指定有意义的线程名。当然,通过PID查找也非常方便。
排查步骤
top
查看到哪个java程序负载高top -p PID -H
查看该进程所有进程的运行状态printf "&x" PID
转换成16进制jstack PID > 文件