一、JVM虚拟机
1.1 架构
1.1.1 关键架构信息
- Class Loader(类加载器):依据特定格式,加载class文件到内存
- Runtime Data Area(运行时数据区):JAVA内存模型的主要区域
- 线程私有
- 程序计数器(Program Counter Register)
- 虚拟机栈(JAVA Stack)
- 本地方法栈(Native Method Area)
- 线程公有
- 堆(Heap)
- 方法区(Method Area,MetaSpace是方法区的一种实现)
- 线程私有
- Execution Engine(执行引擎):对命令进行解析
- JIT编译器
- 垃圾回收期
Native Interface(本地方法接口):融合不同开发语言的原生库为Java所用
1.2 类加载机制:ClassLoader
1.2.1 JVM加载.class文件
Class Loader:依据特定格式,加载class文件到内存
- Execution Engine:对命令进行解析并提交给操作系统去执行
- Native Interface:融合不同开发语言的原生库为Java所用
1)ClassLoader的作用
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
种类
- BootStrap ClassLoader:C++编写,加载核心库java.*
- Extension ClassLoader:Java编写,加载扩展库javax.*
- App ClassLoader:java编写,加载程序所在目录
- Custom ClassLoader:Java编写,定制化加载
2)类加载器的双亲委派机制
好处:避免多份同样字节码的加载(内存是宝贵的)
- 自底向上检查类是否已经加载
- Custom检查是否已经加载,如无加载则委派App检查
- App检查是否已经加载,如无加载则委派Extension检查
- Extension检查是否已经加载,如无加载则委派BootStrap检查
- 如果检查都未完成加载,则进行第2步
- 自顶向下尝试加载类
- BootStrap:Load JRE\lib\rt.jar或者Xbootclasspath选项指定的Jar包
- Extension:Load JRE\lib\ext*.jar或-Djava.ext.dirs指定目录下的Jar包
- App:Load CLASSPATH或-Djava.class.path所指定的目录下的类和Jar包
- Custom:通过java.lang.ClassLoader的子类自定义加载class
1.2.2 类加载的过程
1)类的加载方式
- 隐式加载:new
-
2)类的装载过程
加载:通过ClassLoader加载class文件字节码,生成Class对象
- 链接
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配存储空间并设置类变量初始值
- 解析:JVM将常量池内的符号引用转换为直接引用
- 初始化:执行类变量赋值和静态代码块
Class.forName得到的class是已经初始化完成的,会执行static方法
ClassLoader.loadClass得到的class是还没有链接的
1.2.3 反射
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象的方法称为java语言的反射机制。
Class c =Class.forName("类路径")
Instance i = (Instance)c.newInstance();
//方法
Method m = rc.getDeclaredMethod("方法名", "参数");
// 只能够获取该类的所有方法,包括私有方法
m.setAccessibe(true)
Method m1 = rc.getMethod("方法名", "参数");
//只能获取public方法,可以获取继承或接口的方法
m.invoke(i, "参数");
//属性
Field f = rc.getDeclaredField("属性名");
f.setAccessibe(true)
f.set(i, "属性值");
1.3 JVM内存模型与垃圾回收
内存模型主要指的是Runtime Data Area(运行时数据区),垃圾回收主要是Java堆空间的管理。
1.3.1 内存简介
1)计算机内存寻址
- 32位处理器:2^32的可寻址范围
-
2)地址空间的划分
内核空间
-
1.3.2 JAVA内存模型
1)程序计数器(Program Counter Register)
当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即“线程私有”
- 对Java方法计数,如果是Native方法则计数器为Undefined
- 不会发生内存泄露
2)Java虚拟机栈(Stack)
- Java方法执行的内存模型
- 包含多个栈帧
- 局部变量表:包括方法执行过程中的所有变量
- 操作栈:入栈、出栈、复制、交换、产生消费标量
- 动态连接
- 返回地址
- 递归会引发java.lang.StackOverflowError异常
- 递归过多,栈帧数超过虚拟栈深度
虚拟机栈过多会引发java.lang.OutOfMemoryError异常
3)本地方法栈(Native Method Stack)
-
4) 元空间(MetaSpace)
与永久代(PermGen)的区别
元空间使用本地内存,而永久代使用的是jvm的内存
- java.lang.OutOfMemoryError:PermGen space 这个异常不再存在
MetaSpace相比PermGen的优势
对象实例的分配区域
- GC管理的主要区域
1.3.3 JVM堆和栈
1)三大性能调优参数
java -Xms128m -XMx128m -Xss256k -jar xxxx.jar
-Xms(-XX:InitialHeapSize):堆的初始值
-Xmx(-XX:MaxHeapSize):堆能达到的最大值
-Xss(-XX:ThreadStackSize):规定了每个线程虚拟机栈(堆栈)的大小
一般会将Xms和Xmx设置成一样的,因为堆初始大小的变化会引起程序的抖动
2)堆和栈的区别
- 内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
-
3)示例:元空间、堆、线程独占部分间的联系——内存角度
元空间(MetaSpace)
- Class: Hello World-Method:sayHello\setName\main-Field:name
- Class: System
- 堆(Heap)
- Object: String(“test)
- Object: HelloWorld
- 线程独占(虚拟机栈+程序计数器)
- Parameter reference: “test” to String object
- Variable reference: “hw” to HelloWorld object
- Local Variables: a with 1, lineNo
不同JDK版本之间的intern()方法的区别——JDK6
- JDK6:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。负责,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
- 返回 false、false
- JDK6+:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在常量池中创建该字符串并返回其引用。
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当作垃圾收集
优点:执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致内存泄露
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收。可以作为GC Root的对象如下:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈JN(Native方法)的引用对象
- 活跃线程的引用对象
1.4.2 垃圾回收算法
1)标记-清除算法(Mark and Sweep)
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
缺点:容易造成碎片化
2)复制算法(Copying)
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制至空闲面
- 将对象面所有对象内存清除
优点:适合年轻代
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
缺点:需设置两块内存互换对于存活率较高的对象效率不高
3)标记-整理算法(Compacting)
标记:从根集合进行扫描,对存活的对象进行标记
清楚:移除所有存活的对象,且按照内存地址次序依次排序,然后将末端内存地址以后的内存全部回收。
优点:适合老年代
- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景
4)分代收集算法(Generational Collector)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
- 目的:提高JVM的回收效率
- 不同版本的分代
- jdk6,jdk7时,一般分为年轻代、老年代和永久代
- jdk8以后,永久代被去掉,只有年轻代和老年代
- GC的分类
- Minor GC:年轻代
- Full GC:老年代
- 年轻代:尽可能快速地收集掉那些生命周期短的对象
- Eden区:对象刚被创建时,如果对象放不下则可能放在Survivor或者老年区
- 两个Survivor区
- 对象如何晋升到老年代
- 经历一定Minor次数依然存活的对象
- Survivor区中存放不下的对象
- 新生成的大对象(参数-XX:+PretenuerSizeThreshold)
- 常用的调优参数
- -XX:SurvivorRatio: Eden和Survivor的比例,默认8:1
- -XX:NewRatio: 老年代和年轻代内存大小的比例,默认2:1
- -XX:MaxTenuringThreshold: 对象从年轻代晋升到老年代经过GC次数的最大阈值
- 老年代:存放生命周期较长的对象
- 标记-清理算法
- 标记-整理算法
- Full GC和Major GC
- Full GC比MinorGC慢,但执行频率低
触发Full GC的条件
- 老年代空间不足
- 永久代空间不足(jdk7及以前的版本,jdk8及以上不存在)
- CMS GC时出现promotion failed,concurrent mode failure
- promotion failed:新对象,eden、Survivor都存放不下而要存入老年代中,而此时老年代空间不足
- concurrent mode failure:执行GMS GC时同时有对象要放入老年代中,而此时老年代空间不足
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 调用System.gc(): 只是提醒JVM进行垃圾回收,但是否执行仍由JVM控制
- 使用RMI来进行RPC或管理的JDK应用,每小时执行1次FullGC
5)关键字
Stop-the-World
JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
多数GC优化通过减少Stop-the-world发生的时间来提高程序性能
Safepoint
分析过程中对象引用关系不会发生变化的点
- 产生Safepoint的地方::方法调用;循环跳转;异常跳转等
- 安全点数量得适中
1.4.3 常见的垃圾回收期
1)JVM的运行模式
3)年轻代收集器
- Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
- ParNew收集器(-XX:+UseParnewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
Serial Old收集器(-XX:+UseSerialOldGC,标记-清理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
- Parallel Old收集器(-XX:+UseParallelOldGC,标记-清理算法)
- 多线程,吞吐量优先
- CMS收集器(-XX:+UseConcMarkSweepGC,标记-清理算法)
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
- G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
- Garbage First特点
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
- 将整个Java堆内存划分成多个大小相等的Region
- 年轻代和老年代不再物理隔离
5)常见问题
001 Object的finalize()方法的作用是否与C++的析构函数作用相同
- 与C++的析构函数不同,析构函数调用确定,而它的是不确定的
- 将未被引用的对象放置于F-Queue队列
- 方法执行随时可能会被终止
- 给与对象最后一次重生的机会
002 Java中的强引用、软引用、弱引用、虚引用有什么用处?
强引用>软引用>弱引用>虚引用
强引用(Strong Reference)
- 最普遍的引用:Object obj = new Object()
- 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
类层次结构
- 引用队列(ReferenceQueue)
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用以及虚引用
软引用(Soft Reference)
- 对象处在由于但非必须的状态
- 只有当内存空间不足时,GC会回收该引用的对象的内存
- 可以用来实现高速缓存
- 弱引用(Weak Reference)
- 弱引用(Weak Reference)
- 非必须的对象,比软引用更弱一些
- GC时会被回收
- 被回收的概率也不大,因为GC线程优先级比较低
- 适用于引用偶尔被使用且不影响垃圾收集的对象
- 虚引用(Phantom Reference)
- 虚引用(Phantom Reference)
- 不会决定的生命周期
- 任何时候都可能被垃圾回收期回收
- 跟踪对象被垃圾收集器回收的活动,起哨兵作用
- 必须和引用队列ReferenceQueue联合使用
1.5 常见问题
001 谈谈你对Java的理解
平台无关性
Complie Once,Run Anywhere如何实现
java代码编译成.class文件,.class文件是跨平台的基础。不同平台的JVM完成.class文件的解析,转换成特定平台的执行指令。
为什么JVM不直接将源码解析成机器码去执行?
- 准备工作:每次执行都需要各种检查
- 兼容性:也可以将别的语言解析成字节码
GC
语言特性
面向对象
类库
异常处理
二、多线程与并发
2.1 概述
2.1.1 进程与线程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。1)二者区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
- 所有与进程相关的资源,都被记录在PCB中,见下图
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
- 线程只有堆栈寄存器、程序计数器和TCB组成,见程序与数据图
总结
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程的切换开销大
Java进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程包括至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
进程和线程的由来
- 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户输入
- 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行
- 进程:进程独占内存空间,保存各自运行状态,相互间不互相干扰且可以互相切换,为并发处理任务提供了可能
线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,是进程内的子任务得以并发执行
2.2 JAVA线程实用篇
2.2.1 线程的6个状态
1)新建(NEW)
2)就绪(RUNNABLE)
3)无限期等待(Waiting)
不会被分配CPU执行时间,需要显式被唤醒。以下情况会进入:
没有设置Timeout的参数的Object.wait()方法
- 没有设置Timeout的参数的Thread.join()方法
-
4)限期等待(Timed Waiting)
在一定时间后会由系统自动唤醒。以下情况会进入:
Thread.sleep()方法
- 设置了Timeout的参数的Object.wait()方法
- 设置了Timeout的参数的Thread.join()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
5)阻塞(Blocked)
等待获取排它锁6)结束(Terminated)
已终止线程的状态,线程已经结束执行2.2.2 线程中重要的概念
1)锁池(EntryList)
假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正在被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。2)等待池WaitSet
假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。2.2.3 与线程相关的类与接口
1)Thread类
start方法
调用start方法会创建一个新的子线程并启动run方法run方法
只是Thread的一个普通方法的调用2)Runnbale接口
run方法
Thread是实现了Runnable接口的类,使得run支持多线程3)Callable接口
4)ReentrantLock类:再入锁
常见问题
001 Thread的start和run方法的区别?
002 Thread和Runnable是什么关系?
Thread是实现了Runnable接口的类,使得run支持多线程
因类的单一继承原则,推荐多使用Runnable接口
003 如何实现处理线程的返回值
实现的方式主要有三种
- 主线程等待法
- 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
- 通过Callable接口实现:通过FutureTask Or线程池获取
前两种方法需要给run()方法传参
- 实现的方式主要有三种
- 构造函数传参
- 成员变量传参
- 回调函数传参
2.2.4 线程方法
1)sleep方法
sleep是Thread类的方法,Thread.sleep()只会让出CPU,不会导致锁行为的改变。可以在任何地方使用
2)wait方法
wait是Object类的方法,Object.wait()不仅让出CPU,还会释放已经占有的同步锁资源。只能在synchronized方法或synchronized块中使用
3)notify方法
notify是Object类的方法,notify只会随机选取一个处于等待池的线程进入锁池去竞争获取锁的机会
4)notifyall方法
notifyall是Object类的方法,notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
5)yield方法
yield是Thread类的方法,当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
6)interrupt方法
interrupt是Thread类的方法,通知线程应该中断了。实际会出现以下两种情况:
①如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
②如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
7)stop方法(已废弃)
8)suspend方法(已废弃)
9)resume方法(已废弃)
2.2.5 线程关键字
1)synchronized
synchronized代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized方法和synchronized块。
2)volatile
volatile作为java中的关键词之一,用以声明变量的值可能随时会被别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。
volatile具有可见性、有序性,不具备原子性。
2.3 Java锁机制
https://segmentfault.com/a/1190000023735772