深入JVM2

参考

《深入理解JVM虚拟机》第三版,在此感谢宋红康老师的JVM教程。承接上一篇博客
《深入JVM》

性能监控、故障处理工具

恰当使用虚拟机故障处理、分析的工具可以提升我们分析数据、定位并解决
问题的效率

jps

JVM Process Status Tool 虚拟机进程状况工具。可以列出正在运行的虚拟机
进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID

  • jps -q 只输出LVMID,省略主类名称
  • jps -m 输出虚拟机进程启动时传递给主类main()函数的参数
  • jps -l 输出主类的全名,如果进程执行的是JAR包,则输出JAR路径
  • jps -v 输出虚拟机进程启动时的JVM参数

jstat

JVM Statistics Monitoring Tool 用于监视虚拟机各种运行状态信息的命
令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、
即时编译等运行时数据。命令格式如下
jstat [ option vmid [interval[s|ms] \[count] ]]
参数interval和count分别代表查询间隔和次数,如果省略这两个参数说明只
查询一次。假设需要每250毫秒查询一次进程2764垃圾收集情况,一共查询20
次,那么命令如下
jstat -gc 2764 250 20

主要选项

选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载、垃圾收
集、运行期编译状况

执行样例

查询结果如下

  1. S0和S1分别代表Survivor0和Survivor1,都未占用空间
  2. E代表Eden,占用了20%的空间
  3. O代表老年代,未占用空间
  4. YGC和YGCT分别代表Young GC和耗时,执行0次耗时0s
  5. FGC和FGCT分别代表Full GC和耗时,执行0次耗时0s
  6. GCT代表所有GC总耗时,耗时0s

jinfo

Configuration Info for Java 的作用是实时查看和调整虚拟机各项参数,
使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表。当如果想
知道未被显示指定的参数的系统默认值,可以使用jinfo的-flag选项进行查
询,如果是JDK6或以上的版本,也可以使用 java -XX:PrintFlagsFinal
查看参数默认值。命令格式如下
jinfo [option] pid

jmp

Memroy Mao for Java 用于生成堆转储快照(dump文件),还可以查询
finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用
的是哪种收集器等。命令格式如下
jmap [option] vmid

主要选项

jhat

JVM Heap Analysis Tool 与jmap搭配使用,来分析jmap生成的堆转储
快照。但一般不会使用

jstack

Stack Trace for Java 用于生成虚拟机当前时刻的线程快照。线程快照
就是当前虚拟机每一条线程正在执行的方法堆栈集合,生成线程快照的目
的通常是定位线程出现长时间停顿的原因,比如线程间死锁、死循环、请
求外部资源导致的长时间挂起,命令格式如下
jstack [option] vmid

主要选项

  • -F 当正常输出的请求不被响应时,强制输出线程堆栈
  • -l 除堆栈外,显示关于锁的附加信息
  • -m 如果调用到本地方法的话,可以显示C/C++的堆栈

基础工具总结

  1. 基础工具 用于支持基本的程序创建和运行
  2. 安全 用于程序签名、设置安全测试等
  3. 国际化 用于创建本地语言文件
  4. 远程方法调用 用于跨Web或网络的服务交互
  5. 部署工具 用于程序打包、发布和部署
  6. 性能监控和故障处理 用于监控分析Java虚拟机运行信息,排查问题
  7. REPL和脚本工具

可视化故障处理工具

JDK中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化
工具。接下来着重介绍一下VisualVm

VisualVM

All-in-one Java Troubleshooting Tool 是功能最强大的运行监视和故
障处理程序之一,并且可以通过插件扩展功能

样例展示

  1. 选择一个需要监视的程序就可以进入程序的主界面
  2. 生成、浏览堆转储快照 在VisualVM中生成堆转储快照文件
  • 在应用程序窗口中右键单击应用程序节点,然后选择堆Dump

生成堆转储快照文件后,应用程序页签会在该堆的应用程序下增加一个以 [he
ap-dump]开头的子节点。可以点击节点右键另存为磁盘文件,否则当进程结束
生成的快照文件会被自动处理

调优案例分析与实战

这部分除了知识和工具外,还需要经验

大内存硬件上的程序部署策略

集群间同步导致的内存溢出

堆外内存导致的溢出错误

外部命令导致系统缓慢

服务器虚拟机进程崩溃

不恰当数据结构导致内存占用过大

由Windows虚拟内存导致的长时间停顿

由安全点导致的长时间停顿

虚拟机字节码执行引擎

执行引擎的概述

  1. 虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力,其区
    别是物理机的执行引擎是建立在处理器、缓存、指令集合操作系统层面上,
    而虚拟机的执行引擎是由软件自行实现,因此不受物理条件制约地定制指
    令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
  2. JVM的主要任务是负责装载字节码到其内部,但字节码并不能直接运行在
    操作系统上,因为字节码指令并非等价于本地机器指令,其内部包含的仅仅
    只是一些能够被JVM所识别的字节码指令、符号表以及其它辅助信息
  3. 执行引擎的任务就是将字节码指令编译为对应平台的本地机器指令,也就
    是将高级语言翻译成机器语言
  4. 执行引擎在执行字节码的时候,通过会有解释执行(通过解释器执行)和
    编译执行(通过即时编译器产生本地代码)两种选择

运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元,栈帧则是用于支持虚拟机进行方法
调用和方法执行背后的数据结构,栈帧中存储了方法的局部变量表、操作数栈
、动态连接和方法返回地址

局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局
部变量。局部变量表以变量槽为最小单位,实例方法局部变量表中第0位索引
的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键
字this来访问

操作数栈

操作数栈也被称为操作栈,这是一个后入先出的栈

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引
用是为了支持方法调用过程中的动态连接。常量池中有大量的符号引用,字节
码中的方法调用指令就以常量池中的符号引用为参数,这些符号引用一部分
在类加载阶段或者第一次使用就会转化为直接引用,这种转化成为静态解析
另一部分在运行期间转化为直接引用,称为动态连接

方法返回地址

方法退出的过程实际上就等同于把当前栈帧出栈,恢复上层方法的局部变量表
和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整PC计数
器的值以指向方法调用指令后面的一条指令

方法调用

调用不同类型的方法,字节码指令集设计了不同的指令

  • invokestatic 调用静态方法
  • invokespecial 调用实例构造器、私有方法和父类中的方法
  • invokevirtual 调用虚方法
  • invokeinterface 调用接口方法,运行时确定一个实现该接口的对象
  • invokedynamic 现在运行时动态解析调用点限定符所引用的方法

静态分派

乍一看我会认为分别输出man和woman,然而结果都是guy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TT {
static abstract class Human{

}
static class Man extends Human{

}
static class Woman extends Human{

}
public void say(Human guy)
{
System.out.println("guy");
}
public void say(Man guy)
{
System.out.println("man");
}
public void say(Woman guy)
{
System.out.println("woman");
}

public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
TT a=new TT();
a.say(man);
a.say(woman);
}
}

为什么虚拟机会执行Human的重载版本?对于以下代码

1
Human man=new Man();

Human称为变量的静态类型,Man称为变量的实际类型。虚拟机重载的时候是
根据参数的静态类型而不是实际类型作为判断依据,所有依赖静态类型来决
定方法执行版本的分派动作,都称为静态分派

动态分派

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TT {
static abstract class Human{
protected abstract void say();
}
static class Man extends Human{
protected void say()
{
System.out.println("man");
}
}
static class Woman extends Human{
protected void say()
{
System.out.println("woman");
}
}
public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
man.say();
woman.say();
man=new Woman();
man.say();
}
}

显然选择调用的方法版本是不可能再根据静态类型决定,因为静态类型同样是
Human的两个变量man和woman在调用say()时产生不同行为,因为这两个变量
实际类型不同

动态类型语言

动态类型语言的关键特性是它的类型检查的主体过程是在运行器而不是编译器

基于栈的字节码解释执行引擎

Java虚拟机的执行引擎在执行Java代码的时候都有解释执行和编译执行两种
选择

类加载及执行子系统的案例与实战

主流的Java Web服务器,如Tomcat Jetty WebLogic等都实现自己的类加载
器,而且一般都不止一个,Web服务器需要解决如下问题

  • 部署在同一服务器上的两个Web应用程序所使用的Java类库可以实现相互隔
    离。服务器应当保证两个独立应用程序的类库可以相互独立使用
  • 部署在同一服务器上的两个Web应用程序所使用的Java类库可以相互共享
  • 服务器需要尽可能保证自身的安全不受部署的Web应用影响

前端编译与优化

编译期是一个很模糊的概念,可能指前端编译器将.java文件转变为.class文
件,也可能指Java虚拟机的即时编译器(常称为JIT编译器,Just In Time
Compiler)运行期将字节码转变为本地机器码,还可能指使用静态的提前编
译器(常称AOT Ahead Of Time Compiler)直接把程序编译成与目标机器
指令集相关的二进制代码的过程

  • 前端编译器 JDK的Javac、Eclipse JDT中的增量式编译器
  • 即时编译器 HotSpot虚拟机的C1、C2编译器,Graal编译器
  • 提前编译器 JDK的Jaotc,GCJ,JET

Javac编译器

这个编译器本身是由Java实现

后端编译与优化

把Class文件转换为与本地基础设施相关的二进制机器码,都可以视为整个编
译过程的后端,《Java虚拟机规范》并没有规定后端编译器的实现细节,但是
后端编译器性能的好坏、代码优化质量的高低确是衡量一款商用虚拟机优秀
与否的关键指标之一

Java内存模型与线程

多线程处理在现代计算机操作系统中几乎是一项必备的功能。一个很重要的原
因就是计算机的运算速度与它的存储和通信子系统的速度差距太大,如果不
希望处理器在大部分时间都处于等待其他资源的空闲状态,就可以让计算机
同时处理多个任务。一个服务端要同时对多个客户端提供服务,衡量一个服
务性能的好坏,每秒事务处理数(Transactions Per Second)是重要的
指标之一,TPS代表一秒内服务端平均能响应的请求总数

硬件的效率与一致性

绝大多数的运算任务都不可能只靠处理器计算就能完成,处理器至少要与内
存交互,如读取运算数据、存储运算结果。由于计算机的存储设备与处理器
的运算速度有几个数量级的差距,所以使用高速缓存作为缓冲:将运算需
要的数据复制到缓存中,让运算快速进行,运算结束后再从缓存同步到内
存之中。当高速缓存引入了一个问题:缓存一致性。在多路处理器系统中
每个处理器都有自己的高速缓存,而它们又共享同一主内存,这种系统
称为共享内存多核系统,当多个处理器的运算任务都涉及同一块主内存
区域时,将可能导致各自的内存数据不一致
如果发生这种情况,那么同步到主内存时该以谁的缓存数据为准?为了
解决这个问题,需要各个处理器访问缓存时都遵循一些协议,在读写时
要根据协议来进行操作,比如MISI MESI MOSI Synapse Firefly

Java内存模型

C和C++直接使用物理硬件和操作系统的内存模型,《Java虚拟机规范》
曾试图定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问
差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
,必须保证让Java的并发内存访问操作不会产生歧义

主内存和工作内存

Java内存模型的主要目的就是定义程序中各种变量的访问规则,即关
注在虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细
节,这里的变量包括实例字段、静态字段和构成数组对象的元素,不
包括局部变量和方法参数,因为后者是线程私有的,不会被共享也
就不存在同步问题。
Java内存还规定了所有变量都存储在主内存中,每条线程还有自己的
工作内存(可以与处理器高速缓存类比),线程的工作内存还保存了
该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工
作内存中进行,而不能直接读写子内存的数据,不同的线程也无法访
问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来
完成

内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷
贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内
存模型定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及
的每一种操作都是原子的、不可再分的

  • lock 作用于主内存的变量,把一个变量标识为一条线程独占的状态
  • unlock 作用于主内存的变量,把一个处于锁定状态的变量释放出来释
    放后的变量才可以被其他线程锁定
  • read 作用于主内存的变量,把一个变量的值从主内存传输到线程的工
    作内存,以便随后的load动作使用
  • load 作用于工作内存的变量,把read操作从主内存中得到的变量值放
    入工作内存的变量副本中
  • use 作用于工作内存的变量,把工作内存中一个变量的值传递给执行引
    擎,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • assign 作用于工作内存的变量,把一个从执行引擎接收到的值赋给工
    作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个
    操作
  • store 作用于工作内存的变量,把工作内存中一个变量的值传递到主内
    存中,以便随后的write操作
  • write 作用于主内存的变量,把store操作从工作内存中得到的变量的
    值放入主内存的变量中

如果要把一个变量从主内存拷贝到工作内存,就要按照read和load的执行
操作,如果要把变量从工作内存同步回主内存,则要按照store和write
的执行顺序。Java内存模型还规定了执行上述8种基本操作时必须满足如
下规则

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃最近的assign操作,即变量在工作内存中改变了以
    后必须把该变化同步回主内存
  • 不允许一个线程无原因地(没有发生assign操作)把数据从线程的工作
    内存同步回主内存中
  • 一个变量同一时刻只允许一条线程对其进行lock操作,当lock操作可以
    被同一条线程重复执行多次,多次执行lock后只有执行相同次数的unlock
    操作才会被解锁
  • 如果对一个变量执行lock操作,那么会清空工作内存中此变量的值,在
    执行引擎使用这个变量之前,需要重新load或assign操作以初始化变量
    的值
  • 如果对一个变量事先没有被lock操作锁定,那么不允许执行unlock操作
    也不允许unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行
    store write)

volatile

当一个变量被定义为volatile之后,它将具有两项特性:第一项是保证此
变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量
的值,新值对于其他线程来说是立即可知的,而普通变量不能做到这一点
普通变量的值在线程间传递均需要主内存来完成,比如线程A修改一个普
通变量的值,然后向主内存回写,另一条线程B在线程A回写完成再对主
内存进行读取操作,新变量值才会对线程B可见
volatile变量在各个线程的工作内存中是不存在一致性问题的(每次使用
前都要先刷新,执行引擎看不到不一致的情况,因此认为不存在一致性问
题),但是Java里面的运算操作符并非原子操作,这导致volatile变量
的运算在并发下一样是不安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TT {
public static volatile int race=0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads=new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++)
{
threads[i]=new Thread(new Runnable(){
public void run()
{
for(int i=0;i<10000;i++)
increase();
}
});
threads[i].start();
}
while(Thread.activeCount()>2)
Thread.yield();
System.out.println(race);
}
}

预计的结果应该是200000,而在IDEA实际显示的结果是197403,问题就出在
自增运算race++中,查看字节码指令
getstatic 指令把 race 的值取到操作数栈顶,volatile 关键字保证了race的
值在此时是正确的,但是在 iconst_1 、 iadd 这些指令的时候,其他线程可能
已经把race的值改变了,而操作栈顶的值就变成了过期的数据,所以putstatic
指令执行后就把较小的race值同步回主内存中。由于volatile变量只能保证可见
性,在不符合以下两条规则的场景中,仍然要通过加锁(syschronized java.
util.concurrent中的锁或原子类)来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束
  • 当用synchronized修饰的方法执行时, 普通的方法时仍然可以访问
    synchronized 方法中的变量的

以下场景很适合使用volatile变量来控制并发,当shutdown()方法被调用时,能
保证所有线程中执行doWork()方法都立即停下来

1
2
3
4
5
6
7
8
9
10
11
12
volatile boolean shutdownRequested;
public void shutdown()
{
shutdownRequested=true;
}
public void doWork()
{
while(!shutdownRequested)
{
//代码的业务逻辑
}
}

使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证
在该方法执行过程中所有依赖赋值结果的地方都能获得正确结果,但不能保证
变量赋值操作的顺序与程序代码的执行顺序一致,因为在同一个线程的方法执
行过程中无法感知这点,这就是Java内存模型中描述的所谓线程内表现为串
行的语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Map configOptions;
char[] configText;
volatile boolean initialized=false;

//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后
//将initialized设置为true,通知其他线程配置使用
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true;

//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized)
sleep();

//使用线程A中初始化好的配置信息
dosomethingWithConfig();

如果定义initialized变量时没有使用volatile修饰,就可能由于指令重排序
的优化导致线程A最后一条代码 initialized=true被提前执行,这样在线程
B使用配置信息的代码就可能出现错误,volatile关键字能避免此类情况的发
生。volatile屏蔽指令重排序的语义在JDK5才被修复,以下是一段标准的双
锁检测单例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton
{
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance==null)
{
synchronized(Singleton.class)
{
if(instance==null)
instance=new Singleton();
}
}
return instance;
}
}

对应的汇编代码如下

1
2
3
4
5
6
mov $0x3375cdb0,%esi
mov %eax,0x150(%esi)
shr $0x9,%esi
movb $0x0,0x1104800(%esi)
//注意如果使用volatile就会多一行
lock addl $0x0,(%esp)

这个操作的作用相当于一个内存屏障(指重排序时不能把后面的指令重排序到内存
屏障之前的位置),只有一个处理器访问内存时,并不需要内存屏障,但如果有多
个处理器访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一
致性。lock的作用是将本处理器的缓存写入内存,该写入动作会引起别的处理器或
别的内核无效化其缓存,这种操作相当于对缓存中的变量做一次前面介绍的Java
内存模式中的store和write操作,通过这样一个空操作让前面volatile变量的
修改对其他处理器立即可见。之前有个问题就是一个处理器访问内存时并不需要
内存屏障这句话,一个处理器也会存在指令重排序,但是为什么不会出现问题,
我查了一下,有一种解释是单个处理器的情况下无需考虑线程安全,所谓的多
线程实际都是串行执行,并不存在真正的并发执行

指令重排序

指令重排序是指处理器采用允许多条指令不按程序规定的顺序分开发送给各个相
应的电路单元进行处理,但并不是任意重排,处理器必须能够正确处理指令依
赖情况保障程序能得出正确的执行结果。比如指令1把地址A中的值加10,指令
2把地址A中的值乘以2,指令3把地址B中的值减去3,这是指令1和指令2是有
依赖的,它们之间的顺序不能重排,但是指令3可以任意位置。所以在同一个
处理器中,重排序过的代码看起来依然有序,lock指令把修改同步到内存中
意味着之前的操作都已经执行完成,这样便形成指令从排序无法越过内存屏
障的效果

针对long和double型变量的特殊情况

对于64位的数据类型,在模型中特别定义了一条宽松的规定,允许虚拟机将
没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行
,经过测试,目前主流平台下商用的64位Java虚拟机并不会出现非原子性
访问行为

原子性、可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个
特征来建立的

原子性

有Java 内存模型来直接保证的原型性变量操作包括 read load assign use
store write。基本数据类型的访问和读写都是具备原子性的,synchronized
块之间的操作也具备原子性

可见性

可见性就是指当一个线程修改了共享变量的值时 ,其他线程能够立即得知这个
修改。Java 内存模型是通过在变量修改后将新值同步回追内存,在变量读取前
从主内存刷新变量值这种依赖主内存作为媒介的方式来实现可见性,valatile
保证多线程操作时变量的可见性,普通变量不能保证这一点。除了 volatile
Java还有两个关键字能实现可见性:synchronized和 final。同步块的可见
性是由“对一个变量执行unlock 操作之前(变量可以放在同步块中),必须
保证把此变量同步回主内存中”这条规则获得的。而final的可见性是指:被
final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的
引用传递出去,那么其他线程能够看到final字段的值,无需同步就能被其
他线程访问,还有一点需要注意,对一个变量执行unlock操作之前已经刷
新的次数是不确定的,在没有解锁之前如果在代码块的某一地方修改变量
的值可能立即刷新回主内存也可能不刷新,但是unlock之前一定会刷新
一次

有序性

如果在本线程内观察,所有的操作都是有序,如果在一个线程观察另一个线程,
所有的操作都是无序的,前半句是指线程内似表现为串行的语义,后半句是指
指令重排序现象和工作内存与主内存同步延迟现象

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如操作A先行
发生于操作B,操作A的影响能被操作B观察到,影响包括修改内存中共享变量
的值、发送了消息、调用了方法等

1
2
3
4
5
6
//以下操作在线程A中执行
i=1;
//以下操作在线程B中执行
j=i;
//以下操作在线程C中执行
i=2;

如果线程A中的操作先行发生于线程B,那么可以确定线程B的操作执行后j=1,
得出这个结论的依据是线程A已发生,线程C未发生。如果线程C发生在A和B之
间则无法确定j的值,因为线程C对变量i的操作可能被B观察到也可能不会。
注意时间先后顺序与先行发生原则之间基本没有因果关系

Java与线程

并发不一定依赖多线程,但是在Java中谈论并发基本上与线程脱不开关系

线程的实现

目前线程是Java里进行处理器资源调度的基本单位,每个已经调用过start
方法且还未结束的java.lang.Thread类的实例就代表一个线程。Thread类
的所有关键方法都被声明为Native,Native方法意味着这个方法没有使用
或无法使用平台无关的手段来实现。实现线程主要有三种方式:使用内核
线程实现(1:1),使用用户线程实现(1:N),使用用户线程加轻量级
进程混合实现(N:M)

内核线程实现

内核线程是直接由操作系统内核支持的线程,这种线程由内核来完成线程
切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到
各个处理器上,支持多线程的内核称为多线程内核。程序一般不会直接使
用内核线程,而是使用内核线程的一种高级接口–轻量级进程。轻量级
进程就是通常意义上的线程,每个轻量级进程都由一个内核线程支持,
轻量级进程与内核线程之间1:1的关系成为一对一的线程模型

用户线程实现

使用用户线程实现的方式称为1:N实现,广义上讲一个线程只要不是内核
线程,都可以认为是用户线程。狭义上的用户线程指的是完全建立在用户
空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完
成,不需要内核帮助,这种进程与用户线程之间1:N的关系称为一对多
的线程模型

混合实现

将内核线程与用户线程混合使用的实现方式

线程的实现

以HotSpot虚拟机为例,它的每一个Java线程都是直接映射到一个操作系
统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot自己
是不会干涉线程调度,全权交由操作系统处理

状态转换

线程状态转换关系

线程安全与锁优化

《Java并发编程实战》为线程安全做了一个比较恰当的定义:当多线程同时
访问一个对象时,如果不用考虑这些对象在运行时环境下的调度和交替执
行,也不需要进行额外的同步,或者在调用方进行任何其他的协作操作,
调用这个对象的行为都可以获得正确的结果,就称这个对象是线程安全

Java语言的线程安全

可以将Java中的数据分为五类

  • 不可变
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程对立

不可变

不可变的对象一定是线程安全的,如果多线程共享的数据是一个基本数据类型
只要定义时使用final修饰就可以保证是不可变的,如果是一个对象,就需要
对象自行保证其行为不会对其状态产生任何影响,例如java.lang.String。
保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里
带有状态的变量都声明为final

绝对线程安全

Java API中标注是线程安全的类大多数都不是绝对的线程安全

相对线程安全

相对线程安全就是通常意义上的线程安全,保证对这个对象单次操作是线程安
全的

线程兼容

对象本身不是线程安全,但是通过在调用端正确使用同步手段来保证对象在并
发环境中可以安全使用

线程对立

不管调用端是否采取同步措施,都无法在多线程环境中使用并发代码

垃圾回收器

垃圾回收器的分类

JDK的版本处于高速迭代过程,Java发展至今已经衍生了许多GC版本,
Java不同版本的新特性可以从三个方面分析

  • 语法层面 Lambda表达式、switch、自动装箱、自动拆箱、enum、<>
  • API层面 Stream API、新的时间日期、Optional、String、集合框架
  • 底层优化 JVM优化、GC的变化、元空间、静态域、字符串常量池

评估GC的性能指标

吞吐量

暂停时间

吞吐量VS暂停时间

在最大吞吐量优先的情况下,降低停顿时间

垃圾收集器发展史

  • 串行回收器 Serial,Serial Old
  • 并行回收器 ParNew,Scavenge,Parallel Old
  • 并发回收器 CMS,G1

垃圾回收器的组合关系

查看默认垃圾收集器

  • -XX:+PrintCommandLineFlags 查看命令行相关参数
  • 使用命令行 jinfo -flag 相关垃圾回收器参数 进程ID

Serial回收器

  • 优点 简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有
    线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,
    运行在Client模式下的虚拟机是个不错的选择
  • -XX:+UseSerialGC 参数可以指定年轻代和老年代都是用串行收集器
  • 对于交互性较强的应用而言,这种垃圾收集器是不能接受的

ParNew回收器

ParNew收集器除了采用并行回收的方式执行内存回收外,与Serial几乎
没有区别,只是针对年轻代的回收

  • -XX:+UseParNewGC
  • -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的
    线程数

Parallel回收器

吞吐量优先,是Java8默认的垃圾收集器

CMS回收器


CMOS的特点


CMS收集器的垃圾收集算法是标记-清除算法,会产生垃圾碎片,如果使用
Compact整理内存的话,但是还要保证用户线程能够继续执行,用户的运
行资源不能受影响,所以标记-清除算法更合适

小结

G1:区域化分代式

官方给G1设定的目标是在延迟可控的条件下获得尽可能高的吞吐量,所以
才担起全功能收集器的重任与期望

  • G1是一个并行回收器,它把堆内存分割为很多不相关的region(物理上
    不连续),使用不同的region表示各个区
  • G1避免了在整个Java堆中进行全区域的垃圾收集,G1跟踪各个region里
    面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经
    验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价
    值最大的region
  • 由于这种方式的侧重点在于回收垃圾最大量的region,所以我们给G1取
    名垃圾优先

G1的特点

  • 优点
  • 缺点 在用户程序运行时,G1无论是为了垃圾收集产生的内存占用还是程序
    运行的额外执行负载都要比CMS高

G1的参数设置

G1的使用场景

Region

G1的垃圾回收过程

记忆集与写屏障

G1回收过程

  • 年轻代GC
  • 并发标记过程
  • 混合回收
  • Full GC

垃圾回收器总结

GC日志分析


  • 垃圾回收数据的分析
  • Minor GC
  • Full GC
Author: 高明
Link: https://skysea-gaoming.github.io/2020/10/11/%E6%B7%B1%E5%85%A5JVM2/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.