一、JVM虚拟机

1.1 架构

image.png

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)类加载器的双亲委派机制

    好处:避免多份同样字节码的加载(内存是宝贵的)
  1. 自底向上检查类是否已经加载
    1. Custom检查是否已经加载,如无加载则委派App检查
    2. App检查是否已经加载,如无加载则委派Extension检查
    3. Extension检查是否已经加载,如无加载则委派BootStrap检查
    4. 如果检查都未完成加载,则进行第2步
  2. 自顶向下尝试加载类
    1. BootStrap:Load JRE\lib\rt.jar或者Xbootclasspath选项指定的Jar包
    2. Extension:Load JRE\lib\ext*.jar或-Djava.ext.dirs指定目录下的Jar包
    3. App:Load CLASSPATH或-Djava.class.path所指定的目录下的类和Jar包
    4. Custom:通过java.lang.ClassLoader的子类自定义加载class

1.2.2 类加载的过程

1)类的加载方式

  • 隐式加载:new
  • 显式加载:loadClass,forName等

    2)类的装载过程

  • 加载:通过ClassLoader加载class文件字节码,生成Class对象

  • 链接
    • 校验:检查加载的class的正确性和安全性
    • 准备:为类变量分配存储空间并设置类变量初始值
    • 解析:JVM将常量池内的符号引用转换为直接引用
  • 初始化:执行类变量赋值和静态代码块

Class.forName得到的class是已经初始化完成的,会执行static方法
ClassLoader.loadClass得到的class是还没有链接的

1.2.3 反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象的方法称为java语言的反射机制。

  1. Class c =Class.forName("类路径")
  2. Instance i = (Instance)c.newInstance();
  3. //方法
  4. Method m = rc.getDeclaredMethod("方法名", "参数");
  5. // 只能够获取该类的所有方法,包括私有方法
  6. m.setAccessibe(true)
  7. Method m1 = rc.getMethod("方法名", "参数");
  8. //只能获取public方法,可以获取继承或接口的方法
  9. m.invoke(i, "参数");
  10. //属性
  11. Field f = rc.getDeclaredField("属性名");
  12. f.setAccessibe(true)
  13. f.set(i, "属性值");

1.3 JVM内存模型与垃圾回收

内存模型主要指的是Runtime Data Area(运行时数据区),垃圾回收主要是Java堆空间的管理。

1.3.1 内存简介

1)计算机内存寻址


03.JAVA JVM篇 - 图2

  • 32位处理器:2^32的可寻址范围
  • 64位处理器:2^64的可寻址范围

    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)

  • 与虚拟机栈相似,主要作用于标注了native的方法

    4) 元空间(MetaSpace)

    是方法区的一种实现

    与永久代(PermGen)的区别
  • 元空间使用本地内存,而永久代使用的是jvm的内存

    • java.lang.OutOfMemoryError:PermGen space 这个异常不再存在
  • MetaSpace相比PermGen的优势

    • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
    • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
    • 永久代会为GC带来不必要的复杂性
    • 方便HotSpot与其他JVM如Jrockit的集成

      5)Java堆(Heap)

  • 对象实例的分配区域

  • GC管理的主要区域

1.3.3 JVM堆和栈

1)三大性能调优参数

  1. java -Xms128m -XMx128m -Xss256k -jar xxxx.jar

-Xms(-XX:InitialHeapSize):堆的初始值

-Xmx(-XX:MaxHeapSize):堆能达到的最大值

-Xss(-XX:ThreadStackSize):规定了每个线程虚拟机栈(堆栈)的大小

一般会将Xms和Xmx设置成一样的,因为堆初始大小的变化会引起程序的抖动

2)堆和栈的区别

  • 内存分配策略
    • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
    • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
    • 堆式存储:编译时或运行时模块入口都无法确定,动态分配
  • 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址
  • 管理方式:栈自动释放,堆需要GC
  • 空间大小:栈比堆小
  • 碎片相关:栈产生的碎片远小于堆
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高

    3)示例:元空间、堆、线程独占部分间的联系——内存角度

    03.JAVA JVM篇 - 图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

03.JAVA JVM篇 - 图4

  • JDK6:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。负责,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
    • 返回 false、false
  • JDK6+:当调用intern()方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在常量池中创建该字符串并返回其引用。
    • 返回false/true

      1.4 JAVA内存回收机制

      1.4.1 垃圾回收简介

      1)对象被判定为垃圾的标准

      没有被其他对象引用

      2)方法

      引用计数算法
      判断对象的引用数量。具体步骤如下:
  1. 通过判断对象的引用数量来决定对象是否可以被回收
  2. 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
  3. 任何引用计数为0的对象实例可以被当作垃圾收集

优点:执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致内存泄露

可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收。可以作为GC Root的对象如下:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈JN(Native方法)的引用对象
  • 活跃线程的引用对象

1.4.2 垃圾回收算法

1)标记-清除算法(Mark and Sweep)

  1. 标记:从根集合进行扫描,对存活的对象进行标记
  2. 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存

缺点:容易造成碎片化

2)复制算法(Copying)
  • 分为对象面和空闲面
  • 对象在对象面上创建
  • 存活的对象被从对象面复制至空闲面
  • 将对象面所有对象内存清除

优点:适合年轻代

  1. 解决碎片化问题
  2. 顺序分配内存,简单高效
  3. 适用于对象存活率低的场景

缺点:需设置两块内存互换对于存活率较高的对象效率不高

3)标记-整理算法(Compacting)

标记:从根集合进行扫描,对存活的对象进行标记
清楚:移除所有存活的对象,且按照内存地址次序依次排序,然后将末端内存地址以后的内存全部回收。

优点:适合老年代

  • 避免内存的不连续性
  • 不用设置两块内存互换
  • 适用于存活率高的场景

4)分代收集算法(Generational Collector)


03.JAVA JVM篇 - 图5

  • 垃圾回收算法的组合拳
  • 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
  • 目的:提高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的运行模式

  • Server:重量级虚拟机,启动慢,稳定后更快
  • Client:轻量级虚拟机,启动快,稳定后一般

    2)垃圾回收期之间的联系

    有连线的代表可以配合使用
    03.JAVA JVM篇 - 图6

3)年轻代收集器

  • Serial收集器(-XX:+UseSerialGC,复制算法)
    • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效,Client模式下默认的年轻代收集器03.JAVA JVM篇 - 图7
  • ParNew收集器(-XX:+UseParnewGC,复制算法)03.JAVA JVM篇 - 图8
    • 多线程收集,其余的行为、特点和Serial收集器一样
    • 单核执行效率不如Serial,在多核下执行才有优势
  • Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)03.JAVA JVM篇 - 图9

    • 比起关注样式线程停顿时间,更关注系统的吞吐量
      吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)
    • 在多核下执行才有优势,Server模式下默认的年轻代收集器

      4)老年代收集器

  • Serial Old收集器(-XX:+UseSerialOldGC,标记-清理算法)03.JAVA JVM篇 - 图10

    • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
    • 简单高效,Client模式下默认的老年代收集器
  • Parallel Old收集器(-XX:+UseParallelOldGC,标记-清理算法)03.JAVA JVM篇 - 图11
    • 多线程,吞吐量优先
  • CMS收集器(-XX:+UseConcMarkSweepGC,标记-清理算法)03.JAVA JVM篇 - 图12
    • 初始标记:stop-the-world
    • 并发标记:并发追溯标记,程序不会停顿
    • 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
    • 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
    • 并发清理:清理垃圾对象,程序不会停顿
    • 并发重置:重置CMS收集器的数据结构
  • G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
  • Garbage First特点
    • 并行和并发
    • 分代收集
    • 空间整合
    • 可预测的停顿
  • 将整个Java堆内存划分成多个大小相等的Region
  • 年轻代和老年代不再物理隔离03.JAVA JVM篇 - 图13

5)常见问题

001 Object的finalize()方法的作用是否与C++的析构函数作用相同
  1. 与C++的析构函数不同,析构函数调用确定,而它的是不确定的
  2. 将未被引用的对象放置于F-Queue队列
  3. 方法执行随时可能会被终止
  4. 给与对象最后一次重生的机会
    002 Java中的强引用、软引用、弱引用、虚引用有什么用处?
    强引用>软引用>弱引用>虚引用
    03.JAVA JVM篇 - 图14
    强引用(Strong Reference)
  • 最普遍的引用:Object obj = new Object()
  • 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
  • 通过将对象设置为null来弱化引用,使其被回收

类层次结构
03.JAVA JVM篇 - 图15

  • 引用队列(ReferenceQueue)
    • 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
    • 存储关联的且被GC的软引用,弱引用以及虚引用

软引用(Soft Reference)03.JAVA JVM篇 - 图16

  • 对象处在由于但非必须的状态
  • 只有当内存空间不足时,GC会回收该引用的对象的内存
  • 可以用来实现高速缓存
    • 弱引用(Weak Reference)03.JAVA JVM篇 - 图17
  • 非必须的对象,比软引用更弱一些
  • GC时会被回收
  • 被回收的概率也不大,因为GC线程优先级比较低
  • 适用于引用偶尔被使用且不影响垃圾收集的对象
    • 虚引用(Phantom Reference)03.JAVA JVM篇 - 图18
  • 不会决定的生命周期
  • 任何时候都可能被垃圾回收期回收
  • 跟踪对象被垃圾收集器回收的活动,起哨兵作用
  • 必须和引用队列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调度的最小单位
  1. 所有与进程相关的资源,都被记录在PCB中,见下图
  2. 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
  3. 线程只有堆栈寄存器、程序计数器和TCB组成,见程序与数据图

03.JAVA JVM篇 - 图19
03.JAVA JVM篇 - 图20

总结
  1. 线程不能看做独立应用,而进程可看做独立应用
  2. 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
  3. 线程没有独立的地址空间,多进程的程序比多线程程序健壮
  4. 进程的切换比线程的切换开销大

Java进程和线程的关系
  1. Java对操作系统提供的功能进行封装,包括进程和线程
  2. 运行一个程序会产生一个进程,进程包括至少一个线程
  3. 每个进程对应一个JVM实例,多个线程共享JVM里的堆
  4. Java采用单线程编程模型,程序会自动创建主线程
  5. 主线程可以创建子线程,原则上要后于子线程完成执行

进程和线程的由来
  1. 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户输入
  2. 批处理:预先将用户的指令集中成清单,批量串行处理用户指令,仍然无法并发执行
  3. 进程:进程独占内存空间,保存各自运行状态,相互间不互相干扰且可以互相切换,为并发处理任务提供了可能
  4. 线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,是进程内的子任务得以并发执行

    2.2 JAVA线程实用篇

    2.2.1 线程的6个状态

    03.JAVA JVM篇 - 图21

    1)新建(NEW)

    创建后尚未启动的线程的状态

    2)就绪(RUNNABLE)

    包括Running和Ready

    3)无限期等待(Waiting)

    不会被分配CPU执行时间,需要显式被唤醒。以下情况会进入:

  5. 没有设置Timeout的参数的Object.wait()方法

  6. 没有设置Timeout的参数的Thread.join()方法
  7. LockSupport.park()方法

    4)限期等待(Timed Waiting)

    在一定时间后会由系统自动唤醒。以下情况会进入:

  8. Thread.sleep()方法

  9. 设置了Timeout的参数的Object.wait()方法
  10. 设置了Timeout的参数的Thread.join()方法
  11. LockSupport.parkNanos()方法
  12. 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方法的区别?

见1)中Thread类

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方法(已废弃)

stop是Thread类的方法

8)suspend方法(已废弃)

suspend是Thread类的方法

9)resume方法(已废弃)

resume是Thread类的方法

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

三 多线程并发原理