jvm基础知识学习,感兴趣的可以看下
本帖最后由 chengxuyuan01 于 2022-8-4 15:15 编辑本来准备只花三个星期对照教学视频整理好的,最后多花了两个星期,看成果啦:lol
# JVM入门
## jvm基础
> 什么是jvm
定义:Java Virtual Machine -java 程序的运行环境(java 二进制字节码的运行环境)
好处:
- 一次编写,到处运行的基石
- 自动内存管理机制,垃圾回收
- 数组下标越界检查
- 多态
比较:
jvm,jre,jdk
> 用处
- 用户
- 理解底层实现原理
- 中高级程序员的必备技能
> 常见的jvm
## 内存结构
### 程序计数器 PC Register
> 定义
Program Counter Register 程序计数器(寄存器)
- 在程序执行过程中,记录吓一跳jvm指令的执行地址
- 在代码运行过程中,java代码会被编译成有序的jvm指令,再由解释器解释成程序能够识别的二进制指令
- 在上一条指令执行完成后,程序会从程序计数器中获取下一条指令的地址
- 特点:
- 线程私有
- 不会存在内存溢出的区
### 虚拟机栈
- 先进后出结构
- 线程运行时需要的内存空间(线程私有)
- 虚拟机栈由多个栈帧组成(每个方法运行时需要的内存-》参数、局部变量、返回地址)
- 每个线程只能有一个活动栈帧,对应着正在执行的那个方法
```java
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(1,2);
}
private static int method2(int a ,int b) {
int c = a + b;
return c;
}
```
备注:debugger运行,可通过Frames变化查看当前线程对应虚拟机栈的内存变化,入栈操作为main-》method1-》method2,实际的执行顺序为反序
问题:
1. 垃圾回收是否涉及栈内存?
不会:栈内存在每次方法执行结束后都会被出栈,释放内存,所以不需要垃圾回收
2. 栈内存分配越大越好吗?
栈内存的默认大小都是1024kb(mac,linux..)
在windows环境下的默认大小是根据内存大小进行分配的
在相同内存大小下,如果栈分配的内存大小不一致,栈内存大的程序所能调用的线程数会比栈内存小的程序少
3. 方法内的局部变量是否线程安全?
线程安全,方法内的局部变量对每个线程都会分配一份单独的内存空间,每个栈内的内存空间互不影响,如果变量是共享的,会存在线程安全问题。
变量是否线程安全:
- 是否是方法内的局部变量
- 局部变量是否逃离方法的作用范围(参数或返回)
- 如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
> 栈内存溢出
- 栈内栈帧过多(方法的递归调用)
- 栈帧过大(一个栈帧大小就超过了栈内存大小)
```java
private static int count;
public static void main(String[] args) {
try {
method3();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(count);
}
}
private static void method3() throws Exception {
count++;
method3();
}
```
默认栈内存大小下,递归发生了23522次
修改VM option栈内存大小参数 **Xss256k** 后:-递归次数变为3558
**java.lang.StackOverflowError**
> 线程运行诊断
案例1:cpu占用过多
定位:
top命令查看当前系统中进程占用资源情况
ps H(线程中的进程数) -eo(显示的字段) pid,tid,%cpu -展示进程中线程的属性展示
jstack 进程id 查看进程中的线程信息
- 根据进程id找到有问题的线程,进一步定位到问题代码的源代码行数
案例2:程序运行很长时间没有结果
jstack 进程id 查看进程中的线程信息
- 找到线程死锁代码
### 本地方法栈-Native method stack
给本地方法的运用提供内存空间(java代码存在局限,无法直接和操作系统进行交互,需要通过C等代码编写的native方法和操作系统形成交互)
### 堆-Heap
通过new关键字,创建对象都会使用堆内存
特点:
- 线程共享,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
> 堆内存溢出
堆内的对象都存在引用,且一直存在对象的新增
```java
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
```
**Java.lang.OutOfMemoryError: Java heap space**
-Xmx8m:设置堆内存大小
> 堆内存诊断
1. jps工具
- 查看当前系统中有那些java进程
```log
60064 RemoteJdbcServer
7424 RemoteMavenServer
10728
1544 Test3
33400 ApiApplication
53148 Launcher
6940 Jps
```
2. jmap工具
- 查看堆内存占用情况:jmap -heap 进程id
```log
##堆配置
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4261412864 (4064.0MB)#最大内存配置
NewSize = 88604672 (84.5MB)#新生代内存
MaxNewSize = 1420296192 (1354.5MB)#最大新生代内存
OldSize = 177733632 (169.5MB)#老年代内存
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB #元空间大小
G1HeapRegionSize = 0 (0.0MB)
#伊甸区内存使用情况
Eden Space:
capacity = 66584576 (63.5MB)
used = 1331712 (1.27001953125MB)
free = 65252864 (62.22998046875MB)
2.0000307578740157% used
```
3. jconsole工具
- 图形界面的,多功能的监测工具,可以连续监测:jconsole
案例:垃圾回收后,内存依旧占用很高
jvisualvm 可视化工具工具
### 方法区
- 所有java虚拟机进程共享的区域
- 存储类结构相关信息:成员变量、方法数据、成员方法、构造方法、特殊方法
- 运行时常量池
- 在虚拟机启动时被创建
- 逻辑上是堆的存储部分
> 方法区内存溢出
hotspot 在jdk1.8之前在堆空间中划分永久代保存方法区内的数据
1.8以后,永久代被替代为元空间,且使用系统物理内存作为数据存储
一般不会出现内存溢出问题,需要修改相关jvm参数
-XX:MaxMetaspaceSize=8m:修改元空间最大内存为8m
```java
public static void main(String[] args) {
int j = 0;
try {
Test4 test4 = new Test4();
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter wa = new ClassWriter(0);
//版本号,public,类名,包名,父类,接口
wa.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回byte
byte[] code = wa.toByteArray();
//执行了类的加载
test4.defineClass("Class"+i,code,0,code.length);
}
}finally {
System.out.println(j);
}
}
```
结果:
```log
Error occurred during initialization of VM
MaxMetaspaceSize is too small.
```
场景:
- spring
- mybatis
> 运行时常量池
1. 静态常量池:.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:**字面量**(Literal)和**符号引用量**(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
2. 运行时常量池:则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在**方法区**中,我们常说的常量池,就是指方法区中的运行时常量池,除此外,运行期间也可以将新的常量放入池中,如String类的intern()方法,会在常量池查找是否存在一份equal相等的字符串,如果有则返回该字符串的引用,否则自己添加字符串进入常量池。
优点:
- 实现了对象的共享,避免了频繁的创建和销毁对象而影响系统性能
- 节省内存空间,如字符串常量池
- 合并相同字符串只占用一个空间
- 节省运行时间,进行比较时==比equals快,只判断引用是否相等就可以判断实际值是否相等
如
```java
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2);// true
System.out.println(s1 == s3);// true
System.out.println(s1 == s4);// false
System.out.println(s1 == s9);// false
System.out.println(s4 == s5);// false
System.out.println(s1 == s6);// true
```
s1 == s1 作为直接赋值,使用的字符串字面量,在编译时会直接加载到class文件的常量池中,内容被合并且指向一致
s1==s3 s3对象为拼接对象,但是作为拼接的两个字符串也是字面量,在编译时会被优化为s1 = "Hello",等同于s1和s2
s1==s4 s4中存在new的新对象,其地址为堆中地址,与常量池地址不一致,需要在等到运行时才会知道地址
s1==s9 s9为拼接对象,虽然s7,s8中使用的是字符串字面量,但是拼接s9时,s7,s8作为两个变量,地址不可预料,不能在编译时确定,所有不存在优化
s4==s5 两个都是new出来的堆对象,地址不可能一致
s1==s6 s6使用intern()方法在常量池中寻找,如果有直接调用常量池中的地址
特例:static修饰符
```java
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B;// 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
```
结果是一致对象。
原因:static修饰方法在编译时直接定死了AB的值和地址,所以s值也为定值
特例2:静态代码块
```java
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
```
并不是同一个对象
原因:AB为变量,但是没有直接赋值,程序不知道它们何时赋值和赋予什么样的值,那么s值就不能作为一个定时被初始化,只能在运行时创建。
注意:
- 运行时常量池中的常量基本来源于class文件中的常量池
- 程序运行时,除非手动向常量池中添加常量(intern方法),否则jvm不会自动添加常量到常量池中
基本数据类型和其包装类中除了Float和Double外都实现了常量池技术,但是数值类型的常量池不能手动添加常量,程序启动时常量池中的常量就已经确定了,
如整形常量池中的常量范围:-128-127,(Byte,Short,Integer,Long,Character,Boolean)这五种包装类默认创建了数值【-128,127】之间的响应类型缓存数据,超过此范围后都会创建新的对象到堆中。
> StringTable 串池
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池机制,避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(jdk1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
位置:
1.6时,是跟随常量池放在永久代空间中
1.7,1.8后在堆中单独开辟一个空间存放StringTable
原因:
- 永久代的回收效率不高
### 直接内存-Direct Memory
- 常见于NIO操作,用于数据缓冲区
- 分配回收成本高,但读写性能高
- 不受jvm内存回收管理
相较于原始的io操作,原始io在进行文件读写时需要先由操作系统划分内存空间,再由java程序划分内存空间,进行内容的复制,最后交由内核进行内容写入
直接内存同样也是系统开辟内存空间,但是这块内存空间java程序可以直接访问,减少了java自己开辟空间以及进行内容拷贝的时间
> 内存溢出
```java
static int defaultMember = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(defaultMember);
list.add(byteBuffer);
i ++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
```
运行结果:
```log
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
```
-XX:+DisableExplicitGC:禁用显示的垃圾回收
System.gc:显示的垃圾回收,FUll GC,导致程序出现较长时间的停滞
## 垃圾回收
-XX:+PrintGCDetails -verbose:gc:打印垃圾回收详细参数
### 如何判断对象可以回收
> 引用计数法
弊端:如果存在相互引用的两个对象,会造成这两个对象一直无法被垃圾回收,它们的引用计数最少为1
> 可达性分析算法
jvm虚拟机底层分析算法
- java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用连接找到该对象,找不到,表示可以回收
- 哪些对象可以作为GC Root?
- 系统类
- native 类
- 正在加锁的对象
- 活动线程运行过程中,局部变量引用的对象
> 四种引用
- 强引用
- new() 方法生成对象赋值给变量
- 在强引用未结束前,不会被垃圾回收
- 软引用
- 没有其它强引用对象引用它(只有软引用的对象引用了这个对象)
- 发生垃圾回收,且内存不足时,会被回收
- 可以配合引用队列来释放软引用本身
```java
/**
* @AuThor ysx
* @Class Test7
* @description
* @packagesName com.ysx.check
* @createTime 2022/7/8 13:53
**/
/**-Xmx8m**/
/**-XX:+PrintGCDetails -verbose:gc**/
public class Test7 {
private static int size = 1024 * 1024 * 2;
public static void main(String[] args) {
// method1();
method2();
}
//强引用导致内存溢出 Java.lang.OutOfMemoryError: Java heap space
private static void method1() {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte);
}
}
//弱引用不会导致内存溢出
private static void method2() {
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 4; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:"+list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
//使用引用队列,对软引用本身进行垃圾回收
private static void method3() {
//引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 4; i++) {
//关联了软引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去
SoftReference<byte[]> ref = new SoftReference<>(new byte,queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:"+list.size());
//从引用队列中获取软引用对象,最先放入的最先取出
Reference<? extends byte[]> poll = queue.poll();
//如果队列中不为空,则该软引用对象所关联的对象被回收了,从集合中对该引用对象进行删除,并进行下一次流程
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===============================");
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}
```
- 弱引用
- 没有其它强引用对象引用它(只有弱引用的对象引用了该对象)
- 只要发生垃圾回收,不管内存够不够,都会回收
- 可以配合引用队列来释放软引用本身
```java
/**-Xmx20m**/
/**-XX:+PrintGCDetails -verbose:gc**/
public class Test8 {
private static int size = 1024 * 1024 * 4;
public static void main(String[] args) {
method1();
}
//弱引用实例
private static void method1() {
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.println(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:"+list.size());
}
}
```
- 虚引用
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存
- 终结器引用
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
### 垃圾回收算法
> 标记清除 Mark Sweep
具体查看流程图:**标记-清除**
优点:速度快
缺点:清理出的内存不连续,出现大量内存碎片
> 标记整理 Mark Compact
具体查看流程图:**标记-整理**
在标记清除的基础上,将内存碎片向某个方向移动,进行整理,避免了内存碎片的出现
优点:不会产生内存碎片
缺点:存在对象的移动,对性能损耗较大
> 复制 Copy
具体查看流程图:**复制**
复制存活的对象到另一个区域,原区域内的对象全部清除,且进行复制区域和原区域的替换
优点:不会产生内存碎片
缺点:复制的空间也是从堆空间中划分的,相当于愿有的堆空间被划分为两块,程序只能同时对一块区域进行操作,且存在对象的移动,性能损耗大
### 分代垃圾回收
具体查看流程图:**分代垃圾回收**
堆内存
- 新生代(生命周期较短的对象):清理频率高
- 伊甸区
- 幸存区From
- 幸存区To
- 老年代(生命周期较长对象):清理频率低,速度慢
当伊甸区内存达到上限后,会触发新生代垃圾回收(Minor GC - 可达性分析标记可回收对象),执行复制算法,复制幸存对象到幸存区To,且复制的对象的存活寿命 **+1**,并进行幸存区To和幸存区From的交换,继续生成对象,达到上限,进行第二次垃圾回收,便利幸存区From中是否有存活的对象,有就移动到幸存区To中,且寿命 **+1**,继续复制伊甸区中的存活对象到幸存区To,寿命 **+1**,再进行空间的替换,当幸存区中的对象寿命达到某个阈值(最大寿命是15,4bit),会将该对象晋升到老年代空间中,当新生代和老年代的空间都不足时,会触发Full GC进行整个空间的清理
当Minor GC触发时,会引发一次 stop the world,暂停其它用户的线程,等垃圾回收线程结束后,其它用户线程会恢复运行
当老年代空间不足时会先尝试触发Minor GC,如果空间还是不足,紧接着会触发Full GC,暂停其它用户的线程(算法不同,停顿时间较长),等垃圾回收完成后,其它用户线程恢复运行
> 大对象直接晋升到老年代
如果新生代中创建的对象大小超过新生代内存大小,不会触发Minor GC,该对象会直接晋升到老年代,如果大对象过多,会导致内存溢出
一个线程内的内存溢出不会导致整个Java程序的服务停止
> jvm 参数
| 含有 | 参数 |
| ------------------ | --------------------------------------------------------- |
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx或-XX:MaxHeapSize=size |
| 新生代大小 | -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和-XX:UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
默认新生代内存分配比例:伊甸区:From:To=8:1:1
### 垃圾回收器
-XX:+UseSerialGC:设置垃圾回收器
- 串行的垃圾回收
- 单线程
- 堆内存较小,单核cpu,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW(stop the world)的时间最短
- 响应时间优先
- 多线程,多核cpu
- 堆内存较大,多核cpu
- 尽可能让单次STW的时间最短
> 串行
-XX:+UseSerialGC=Serial + SerialOld:开启串行垃圾回收器(新生代复制算法,老年代标记整理算法)
> 吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC:开启吞吐量优先的垃圾回收器
-XX:+UseAdaptiveSizePolicy:采用自适应大小调整策略
-XX:GCTimeRatio=radio:调整垃圾回收时间与总时间占比-计算公式:1/(1+radio)
-xx:MaxGCPauseMillis=ms:最大暂停毫秒数(默认200),此项与上一项目标对立
-XX:ParallelGCThreads=n:控制垃圾回收线程数
> 响应时间优先
-XX:UseConCMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld :老年代基于标记-清除算法的垃圾回收,且在垃圾回收阶段,其余用户线程可以可继续执行,存在初始标记和重新标记两次STW操作
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads:垃圾回收线程数可用于计算的垃圾回收线程数
-XX:CMSInitiatingOccupancyFraction=percent:执行CMS垃圾回收的内存占比,当老年带的内存占比达到percent后进行垃圾回收,剩余内存需要保存其余线程在垃圾回收线程清理过程中生成的新垃圾
-XX:+CMSScavengeBeforeRemark:在重新标记之前对新生代进行一次垃圾回收UseParNewGC
响应时间优先的垃圾回收会造成大量的内存碎片,最终可能会导致垃圾回收的并发失败,此时垃圾回收器会退化为串行的单线程的垃圾回收器,进行一次内存的标记整理,此时消耗的时间较长
> G1
定义:Garbage First
jdk1.7官方正式支持
jdk1.9官方默认垃圾回收器(取代cms垃圾回收)
适用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 超大堆内地,会将堆划分未多个大小相等的Region(每个Region都可以作为单独的新生代,老年代)
- 整体上是标记-整理算法,两个区域之间是复制算法
相关参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size:设置区域大小
-XX:MaxGcPauseMillis=time:设置默认暂停目标
> G1优化
> > 字符串去重
- 优点:节省大量内存
- 略微多占用了cpu时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
```java
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}
```
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果她们值一样,会让它们引用同一个char[]
- 注意,与String.intern不一样
- String.intern关注的是字符串对象
- 而字符串去重关注的是char[]
- 在jvm内部,使用了不同德字符串表
> > 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
> > 回收巨型对象
- 一个对象大于Region的一半时,称之为巨型对象
- G1不会堆巨型对象进行拷贝
- 回收时呗优先考虑
- G1会跟踪老年代所有incomming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
> > 并发标记时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为FullGC
- jdk1.9之前需要使用 -XX:InitiatingHeapOccupancyPercent
- jdk1.9可以动态调整
- -XX:InitiatingHeapOccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
### 垃圾回收调优
预备知识
- 掌握GC相关的参数,会基本的空间调整
- 掌握相关工具
- 调优跟应用、环境有关,没有放之四海而皆准的法则
- 内存
- 锁竞争
- cpu占用
- io
> 确定目标
低延迟还是高吞吐量,选择合适的回收器
CMS,G1,ZGC
ParallelGC
> 最快的GC是不发送GC
查看Full GC前后的内存占用,考虑下面几个问题
- 数据是不是太多
- 数据表示是否太臃肿
- 对象图
- 对象大小
- 是否存在内存泄漏
> 新生代调优
新生代特点
- 所有的new操作的内存分配非常廉价
- TABLE thread-local allocation buffer:线程私有内存分配
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
新生代越大越好吗?
-XMn:
堆内存大小一定,新生代内存过大,老年代内存会变小,最终导致老年代内存空间不足,导致Full GC,而Full GC的STW过长
官方建议新生代大小在堆内存25%-50之间
算法:【并发量*(请求-响应数据)】
幸存区:
- 需要大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution:打印幸存区对象详细信息
> 老年代调优
以CMS为例
- CMS老年代的内存越大越好
- 先尝试不做调优,如果没有Full GC 那么已经......,否则先尝试调优新生代
- 观察Full GC 时老年代内存占用,将老年代内存预设调大1/4~1/3
- -XX:CMSInitiatingOccupancyFraction=percent:控制老年代垃圾占用在老年代空间中的占比为多少时进行CMS垃圾回收
> 案例
1. Full GC 和 Minor GC频繁
- 新生代内存紧张
- 幸存区内存紧张,倒是老年代的晋升阈值降低,导致大量生命周期较短的对象直接晋升到老年代
- 解决:调整新生代内存大小--幸存区内存增大,调整晋升阈值大小
- 老年代内存紧张
2. 请求高峰期发生Full GC,单次暂停时间特别长(CMS)
- 分析哪部分STW时间较长
- 分析GC日志,定位各阶段时间消耗情况(CMS重新标记时间较长--重新标记阶段会扫描新生代和老年代,此时新生代可能会产生大量浮动垃圾,导致分析性标记阶段时间较长)
- 解决:在重新定位之前进行一次Minor GC,先清理一下新生代内存(-XX:+CMSScavengeBeforeRemark)
3. 老年代充裕情况下,发生Full GC(CMS jdk1.7)
- jdk1.8之前使用的是永久代作为方法区的实现,永久代的空间不足也会导致Full GC的出现,导致整个堆的垃圾清理
- 1.8之后方法区使用元空间实现,元空间使用的是操作系统的内存,基本不会存在内存不足情况
- 解决:调整永久代内存大小
## 类加载与字节码技术
### 类文件结构
javac -parameters -d . xxx.java:编译java文件
16进制查看
.class 文件在linux中使用vim打开,输入**:%!xxd**看到的就是16进制文件编码
```text
00000000: cafe babe 0000 0034 001f 0a00 0600 1109.......4........
00000010: 0012 0013 0800 140a 0015 0016 0700 1707................
00000020: 0018 0100 063c 696e 6974 3e01 0003 2829.....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654eV...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 104d 6574/String;)V...Met
00000070: 686f 6450 6172 616d 6574 6572 7301 0004hodParameters...
00000080: 6172 6773 0100 0a53 6f75 7263 6546 696cargs...SourceFil
00000090: 6501 000f 4865 6c6c 6f57 6f72 6c64 2e6ae...HelloWorld.j
000000a0: 6176 610c 0007 0008 0700 190c 001a 001bava.............
000000b0: 0100 0b68 656c 6c6f 2077 6f72 6c64 0700...hello world..
000000c0: 1c0c 001d 001e 0100 1863 6f6d 2f79 7378.........com/ysx
000000d0: 2f63 6865 636b 2f48 656c 6c6f 576f 726c/check/HelloWorl
000000e0: 6401 0010 6a61 7661 2f6c 616e 672f 4f62d...java/lang/Ob
000000f0: 6a65 6374 0100 106a 6176 612f 6c61 6e67ject...java/lang
00000100: 2f53 7973 7465 6d01 0003 6f75 7401 0015/System...out...
00000110: 4c6a 6176 612f 696f 2f50 7269 6e74 5374Ljava/io/PrintSt
00000120: 7265 616d 3b01 0013 6a61 7661 2f69 6f2fream;...java/io/
00000130: 5072 696e 7453 7472 6561 6d01 0007 7072PrintStream...pr
00000140: 696e 746c 6e01 0015 284c 6a61 7661 2f6cintln...(Ljava/l
00000150: 616e 672f 5374 7269 6e67 3b29 5600 2100ang/String;)V.!.
00000160: 0500 0600 0000 0000 0200 0100 0700 0800................
00000170: 0100 0900 0000 1d00 0100 0100 0000 052a...............*
00000180: b700 01b1 0000 0001 000a 0000 0006 0001................
00000190: 0000 000a 0009 000b 000c 0002 0009 0000................
000001a0: 0025 0002 0001 0000 0009 b200 0212 03b6.%..............
000001b0: 0004 b100 0000 0100 0a00 0000 0a00 0200................
000001c0: 0000 0d00 0800 0e00 0d00 0000 0501 000e................
000001d0: 0000 0001 000f 0000 0002 0010 0a .............
~
```
> 类文件结构结构
```java
ClassFile {
u4 magic;//文件标志
u2 minor_version;//小版本号
u2 major_version;//大版本号
u2 constant_pool_count;//常量池数量
cp_info constant_pool;//常量池
u2 access_flags;//访问修饰
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces;//一个类可以实现多个接口
u2 fields_count;//文件的字段属性
field_info fields;//一个类会有多个字段
u2 methods_count;//文件方法数量
method_info methods;//一个类可以有多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes;//属性表集合
}
```
- 魔术信息
0~3字节,表示它是否是【class】类型文件
00000000: **cafe babe** 0000 0034 001f 0a00 0600 1109.......4........
- 版本
4~7字节,,表示类版本信息 00 34表示java8
00000000: cafe babe **0000 0034** 001f 0a00 0600 1109.......4........
- 常量池
8~9字节表示常量池长度,001f(31),表示常量池有#1~#31项,注意#0项不计入,也没有值
00000000: cafe babe 0000 0034 **001f** 0a00 0600 1109.......4........
javap -verbose xxx.class:查看常量池信息
```text
##常量池信息
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // hello world
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // com/ysx/check/HelloWorld
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 MethodParameters
#14 = Utf8 args
#15 = Utf8 SourceFile
#16 = Utf8 HelloWorld.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 hello world
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 com/ysx/check/HelloWorld
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
```
| 类型 | 标 志 | 描述 |
| -------------------------------- | ------- | ---------------------- |
| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
| CONSTANT_Integer_info | 3 | 整形字面量 |
| CONSTANT_Float_info | 4 | 浮点型字面量 |
| CONSTANT_Long_info | 5 | 长整型字面量 |
| CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
| CONSTANT_Class_info | 7 | 类或接口的符号引用 |
| CONSTANT_String_info | 8 | 字符串类型字面量 |
| CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_MothodType_info | 16 | 标志方法类型 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
访问修饰符
| 标志名称 | 标志值 | 含义 |
| -------------- | ------ | --------------------------------------------- |
| ACC_PLUBLIC | 0x0001 | 表示public字段 |
| ACC_PRIVATE | 0x0002 | 表示private字段 |
| ACC_PROTECTED| 0x0004 | 表示protected字段 |
| ACC_STATIC | 0x0008 | 表示静态字段 |
| ACC_FINAL | 0x0010 | 是否为final字段,final字段表示常量 |
| ACC_VOLATILE | 0x0040 | 是否为volatile |
| ACC_TRANSIMENT | 0x0080 | 是否为transient,表示在持久化读写时忽略该字段 |
| ACC_SYNTHETIC| 0x1000 | 由编译器产生的方法,没有源码对应 |
| ACC_ENUM | 0x4000 | 是否是枚举 |
### 字节码指令
javap -vxxxx.class:反编译.class文件
```tex
Classfile /D:/learn/微服务/project/springboot03/src/main/java/com/ysx/check/com/ysx/check/HelloWorld.class
Last modified 2022-7-14; size 476 bytes //476字节
MD5 checksum 0a2135f01802ae39cedbced12513f84d//md5 校验签名
Compiled from "HelloWorld.java"//java原文件
public class com.ysx.check.HelloWorld//类全路径名称
minor version: 0
major version: 52//jdk版本 1.8
flags: ACC_PUBLIC, ACC_SUPER//访问修饰符
Constant pool://常量池
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // hello world
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // com/ysx/check/HelloWorld
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 MethodParameters
#14 = Utf8 args
#15 = Utf8 SourceFile
#16 = Utf8 HelloWorld.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 hello world
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 com/ysx/check/HelloWorld
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{//方法信息开始
public com.ysx.check.HelloWorld();//构造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
public static void main(java.lang.String[]);//main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
MethodParameters:
Name Flags
args
}
SourceFile: "HelloWorld.java"
```
> 条件判断指令
| 指令 | 助记符 | 含义 |
| ---- | --------- | ---------------- |
| 0x99 | ifeg | 判断是否==0 |
| 0x9a | ifne | 判断是否!=0 |
| 0x9b | ifit | 判断是否 < 0 |
| 0x9c | ifge | 判断是否 >= 0 |
| 0x9d | ifgt | 判断是否 > 0 |
| 0x9e | ifle | 判断是否 <= 0 |
| 0x9f | if_icmpeq | 两个int是否== |
| 0xa0 | if_icmpne | 两个int是否 != |
| 0xa1 | if_icmplt | 两个int是否< |
| 0xa2 | if_icmpge | 两个int是否 >= |
| 0xa3 | if_icmpgt | 两个int是否 > |
| 0xa4 | if_icmple | 两个int是否 <= |
| 0xa5 | if_acmpeq | 两个int是否 == |
| 0xa6 | if_acmpne | 两个int是否 != |
| 0xc6 | ifnull | 判断是否 == null |
| 0xc7 | ifnonnull | 判断是否 != null |
说明:
- byte,short,char都会按照int比较,因为操作数栈都是4字节
- goto用来进行跳转到指定行号的字节码
```java
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
```
```java
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12//if判断,如果不成立,跳转到第12行指令
6: bipush 10
8: istore_1
9: goto 15//goto如果不加,会继续走12行进行重新赋值
12: bipush 20//赋值操作,赋值20
14: istore_1
15: return
```
> 循环控制指令
备注:实际上还是条件判断指令
```java
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
```
```java
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
```
案例:
```java
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i ++;
}
System.out.println(x);
}
```
```java
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: bipush 10
7: if_icmpge 21
10: iload_2
11: iinc 2, 1//在操作数栈上自增1
14: istore_2 //从内存中获取为0,重新赋值给x
15: iinc 1, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
```
> 构造方法
> > <cinit>()v
```java
public class Test11 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
```
编译器会按从上至下的顺序,收集所有static静态代码块喝静态成员赋值的代码,合并为一个特殊的方法<cinit>()v
```java
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
```
<cinit>()v方法会在类加载的初始化阶段被调用
> > <init>()v
```java
public class Test12 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Test12(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Test12 d = new Test12("s3",30);
System.out.println(d.a);
System.out.println(d.b);
}
}
```
编译器会按从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
```java
public com.ysx.check.Test12(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0 //================ 愿有的构造方法====================
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
//======================================
38: return
LineNumberTable:
line 23: 0
line 13: 4
line 15: 10
line 17: 16
line 20: 22
line 24: 28
line 25: 33
line 26: 38
```
> 多态的原理
前提:jvm需要先进行禁止指针压缩操作
-XX:-UseCompressedOops -XX:-UseCompressedClassPointers
```java
public class Test14 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract classAnimal {
public abstract void eat();
@Override
public String toString() {
return "我是"+this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
```
jps查看进程号
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB:工具虚拟机底层的内存操作和地址
前提:需要在jdk根目录下运行
> 异常处理
```java
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
```
javap结果
```java
stack=1, locals=3, args_size=1
0: iconst_0 //0变量
1: istore_1//赋值到局部变量表1槽位中
2: bipush 10
4: istore_1
5: goto 12
8: astore_2//把异常变量的引用地址存储到局部变量表异常变量槽位2中
9: bipush 20
11: istore_1
12: return
Exception table:
from totarget type
2 5 8 Class java/lang/Exception//监听2,4行代码,如果发生异常且异常类型为监听异常,跳到第八行代码
LineNumberTable:
line 15: 0
line 18: 2
line 21: 5
line 19: 8
line 20: 9
line 23: 12
LocalVariableTable:
StartLengthSlotName Signature
9 3 2 e Ljava/lang/Exception; //异常局部变量
0 13 0args [Ljava/lang/String;
2 11 1 i I
```
- 一个Exception table的结构,[from,to)是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号
- 8行的字节码指令astore_2是蒋异常对象的引用存入局部变量表的slot 2位置
> > 多个single-catch
```java
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 20;
} catch (NullPointerException e) {
i = 30;
} catch (Exception e) {
i = 40;
}
}
```
javap编译结果
```java
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 20
11: istore_1
12: goto 26
15: astore_2
16: bipush 30
18: istore_1
19: goto 26
22: astore_2
23: bipush 40
25: istore_1
26: return
Exception table:
from totarget type
2 5 8 Class java/lang/ArithmeticException//多个exception会根据type进行异常捕捉
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 15: 0
line 18: 2
line 25: 5
line 19: 8
line 20: 9
line 25: 12
line 21: 15
line 22: 16
line 25: 19
line 23: 22
line 24: 23
line 27: 26
LocalVariableTable:
StartLengthSlotName Signature
9 3 2 e Ljava/lang/ArithmeticException;//同时只存在一个异常对象,所以存在槽位服用
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0args [Ljava/lang/String;
2 25 1 i I
```
- 因为异常出现时,只能进入Exception table中一个分支,所以局部变量slot 2 位置被共用
> > multi-catch
> finally
```java
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 40;
} finally {
i = 50;
}
}
```
```java
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
2: bipush 10//放入操作数栈栈顶
4: istore_1
5: bipush 50//finally代码
7: istore_1
8: goto 27
11: astore_2
12: bipush 40
14: istore_1
15: bipush 50//finally代码
17: istore_1
18: goto 27
21: astore_3
22: bipush 50//finally代码
24: istore_1
25: aload_3
26: athrow
27: return
Exception table:
from totarget type
2 5 11 Class java/lang/Exception
2 5 21 any //剩余的异常类型,比如Error 监听try块中异常(异常类型为Exception同级或父级异常出现时,Exception无法捕获)
11 15 21 any //剩余的异常类型,比如Error 监听catch块中异常
LineNumberTable:
line 15: 0
line 18: 2
line 22: 5
line 23: 8
line 19: 11
line 20: 12
line 22: 15
line 23: 18
line 22: 21
line 23: 25
line 25: 27
LocalVariableTable:
StartLengthSlotName Signature
12 3 2 e Ljava/lang/Exception;
0 28 0args [Ljava/lang/String;
2 26 1 i I
```
可以看到finally中的代码被复制了三份,分别放入try流程,catch流程以及catch剩余的异常类型流程
> synchronized 代码块
```java
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
```
```java
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup //复制对象引用
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1 //lock引用-> lock
8: aload_1 //对象引用压入操作数栈
9: dup
10: astore_2 //lock引用-> slot 2
11: monitorenter //lock引用加锁操作
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2 //<-slot 2(lock引用)
21: monitorexit //monitorexit(lock引用)解锁
22: goto 30
25: astore_3 //any -> slot 3
26: aload_2 //<-slot 2(lock引用)
27: monitorexit //monitorexit(lock引用)解锁
28: aload_3
29: athrow
30: return
Exception table:
from totarget type
12 22 25 any
25 28 25 any
LineNumberTable:
line 13: 0
line 15: 8
line 16: 12
line 17: 20
line 18: 30
LocalVariableTable:
StartLengthSlotName Signature
0 31 0args [Ljava/lang/String;
8 23 1lock Ljava/lang/Object;
```
注意:方法级别的synchronized不会再字节码指令中有所体现
### 编译期处理
所谓的 *语法糖*,其实就是指java编译器把*.Java源码编译为*.class字节码的过程中,自动胜场和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利
>自动拆装箱
此特性时jdk 5开始加入的,代码 片段:
```java
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
```
这段代码再jdk 5之前是无法编译通过的,必须改写为代码片段2:
```java
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
```
之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在jdk 5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转换为代码片段2
> 泛型结合取值
泛型也是jdk 5 开始加入的特性,但java在编译泛型代码后会执行 *泛型擦除* 的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当作了Object类型来处理
```java
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);//实际调用的是list。add(Object e)
Integer x = list.get(0);//实际调用的是Object obj = List.get(int index)
}
```
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作
```java
//需要蒋Object转为Integer
Integer x = (Integer) list.get(0)
```
如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:
```java
//需要蒋Object转为Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
```
```java
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic#4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5,2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6,2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 20
line 20: 31
LocalVariableTable:
StartLengthSlotName Signature
0 32 0args [Ljava/lang/String;
8 24 1list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
LocalVariableTypeTable:
StartLengthSlotName Signature
8 24 1list Ljava/util/List<Ljava/lang/Integer;>;
```
> 可变参数
可变参数也是jdk 5 开始加入的新特性
```java
public static void foo(String ...args) {
}
public static void foo(String[] args) {
}
```
其中String ...args在编译期间会变化为String[] args
注意:
如果调用了 *foo()* 则等价代码为 *foo(new String[]{})* ,创建一个空数组,而不会传递null值
> foreach 循环
```java
public static void main(String[] args) {
int[] array = {1,2,3,4,5};//数组赋初值的简化写法也是语法糖
for (int i : array) {
System.out.println(i);
}
}
```
被编译器转换后为:
```java
public static void main(String[] args) {
int[] array = new int[]{1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
int e = array;
System.out.println(e);
}
}
```
针对List集合
```java
List<Integer> list= Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
```
被编译器转换后为:
```java
List<Integer> list= Arrays.asList(1,2,3,4,5);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
```
注意:
foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器
### 类加载阶段
#### 加载
- 将类的字节码载入方法区中,内部采用c++的instanceKlass描述类,它的重要field有:
- _java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把class暴露给java使用
- _super即父类
- _fields即成员变量
- _methods即方法
- _constants即常量池
- _class_loader即类加载器
- _vtable虚方法表
- _itable接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意:
- instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中
- 可以通过前面介绍的HSDB查看
#### 链接
- 验证:验证类是否符合 *JVM* 规范,安全性检查
- 准备:为static变量分配空间,设置默认值
- static变量在jdk 7之前存储于instanceKlass末尾,从jdk7开始存储于_java_mirror末尾
- static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果static变量是final,但属于引用类型,那么赋值也会在初始化阶段完成
```java
public class Test18 {
static int a;
static int b = 10;
static final int c = 20;
}
```
```java
static int a;//只是声明,没有赋值
descriptor: I
flags: ACC_STATIC
static int b;//声明且赋值
descriptor: I
flags: ACC_STATIC
static final int c;//final变量在编译时就确定了值,所以在编译期就可以直接赋值
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20
public com.ysx.check.Test18();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 15: 0
static {};//在调用类的构造方法时,进行b变量的赋值
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field b:I
5: return
LineNumberTable:
line 17: 0
```
```java
static int a;//只是声明,没有赋值
descriptor: I
flags: ACC_STATIC
static int b;//声明且赋值
descriptor: I
flags: ACC_STATIC
static final int c;//final变量在编译时就确定了值,所以在编译期就可以直接赋值
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20
static final java.lang.String d; //final变量在编译时就确定了值,所以在编译期就可以直接赋值
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String aaaaaaaa
static final java.lang.Object e;//引用对象虽然是final但是无法确定值,需要在初始化阶段赋值
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public com.ysx.check.Test18();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 15: 0
static {};//在调用类的构造方法时,进行b变量的赋值
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field b:I
5: new #3 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #4 // Field e:Ljava/lang/Object;
15: return
LineNumberTable:
line 17: 0
line 20: 5
```
- 解析:将常量池中的符号引用解析为直接引用
```java
public class Test18 {
public static void main(String[] args)throws ClassNotFoundException, IOException {
ClassLoader classLoader = Test18.class.getClassLoader();
//loadClass方法不会导致类的解析和初始化
Class<?> e = classLoader.loadClass("com.ysx.check.E");
//会导致类的加载和解析,同时也会触发类中引用对象的加载和解析
new E();
System.in.read();
}
}
class E {
F f = new F();
}
class F {
}
```
注意:
- 在使用loadClass进行加载时,没有初始化和解析E对象,此时E中的f对象仅仅只是一个字符,没有与之匹配的内存地址,处于未解析的对象状态
- 使用new()时会导致f对象的加载和解析,此时f是作为一个对象被加载到内存中
#### 初始化
> <cinit>()V方法
初始化即调用<cinit>()V,虚拟机会保证这个类的【构造方法】的线程安全
> 发生的时机
概括的说,类初始化是【懒惰的】
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发父类初始化
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new会导致初始
不会导致初始化的情况
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
```java
public class Test18 {
static {
System.out.println("init main");
}
//main方法所在的类总是最先被初始化的
public static void main(String[] args)throws ClassNotFoundException, IOException {
//不会造成类初始化的情况
//访问类的静态变量不会导致初始化
// System.out.println(F.b);
//类对象的.class不会触发初始化
// System.out.println(F.class);
//创建该类的数组不会触发初始化
// System.out.println(new F);
//类加载器的loadClass不会触发初始化
// ClassLoader loader = Thread.currentThread().getContextClassLoader();
// loader.loadClass("com.ysx.check.F");
//Class.forName的参数2为false时
// ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
// Class.forName("com.ysx.check.F",false,loader2);
//会造成类初始化的情况
//首次访问这个类的静态变量或静态方法时
// System.out.println(E.a);
//子类初始化,如果父类还没初始化,会引发父类初始化
// Class.forName("com.ysx.check.F");
//子类访问父类的静态变量,只会触发父类的初始化
// System.out.println(F.a);
//Class.forName
// Class.forName("com.ysx.check.E");
//new 方法
// new E();
}
}
class E {
public staticint a= 10;
static {
System.out.println("init E");
}
}
class F extends E{
public static final double b = 5.0;
static boolean c = false;
static {
System.out.println("init F");
}
}
```
练习1:
```java
public class Test18 {
public static void main(String[] args)throws ClassNotFoundException, IOException {
// System.out.println(E.a);
// System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a= 10;
public static final String b = "1111";
public static final Integer c= 20;
static {
System.out.println("init E");
}
}
```
判断上述三个操作是否会导致类的初始化
结果:
a和b属性属于静态常量且符合基本类型和字符串的定义,在编译期就会进行赋值,不会导致初始化
c属性虽然是静态常量,但是不是基本类型,所以它的赋值是在类的初始化阶段进行操作的
联系2:
完成懒惰初始化单例模式
```java
public class Test19 {
public static void main(String[] args) {
SingleTon.getInstance();
}
}
class SingleTon {
private SingleTon() {
}
private static class LazyHolder {
private static final SingleTon SINGLETON = new SingleTon();
static {
System.out.println("LazyHolder 初始化");
}
}
public static SingleTon getInstance() {
return LazyHolder.SINGLETON;
}
public static void test() {
System.out.println("test");
}
}
```
注意:
由类加载器操控,可以保证线程安全
### 类加载器
以 *jdk 1.8* 为例:
| 名称 | 加载哪的类 | 说明 |
| ----------------------------------------- | --------------------- | --------------------------- |
| Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
| Extension ClassLoader(扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
| Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
| 自定义类加载 | 自定义 | 上级为Application |
#### 启动类加载器
用 Bootstrap类加载器加载类:
```java
public class Test1 {
static {
System.out.println("bootstrap Test1 init");
}
}
```
执行
```java
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException{
Class<?> aClass = Class.forName("com.ysx.check.loader.Test1");
System.out.println(aClass.getClassLoader());
}
}
```
```log
D:\learn\微服务\project\springboot03>java -Xbootclasspath/a:. com.ysx.check.loader.Test2
```
输出
```verilog
bootstrap Test1 init
null
```
--Xbootclasspath表示设置bootclasspath
- 其中/a:.表示当前目录追加至bootclasspath后
- 可以用这个办法替换核心类
- Java -Xbootclasspath:<new bootclasspath>
- Java -Xbootclasspath/a: <new bootclasspath>
- Java -Xbootclasspath/p: <new bootclasspath>
#### 扩展类加载器
```java
public class Test1 {
static {
System.out.println("classpath Test1 init");
}
}
```
执行
```java
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException{
Class<?> aClass = Class.forName("com.ysx.check.loader.Test1");
System.out.println(aClass.getClassLoader());
}
}
```
输出
```verilog
classpath Test1 init
sun.misc.Launcher$AppClassLoader@18b4aac2
```
使用的是应用程序加载器加载的Test1
```java
public class Test1 {
static {
System.out.println("ext Test1 init");
}
}
```
使用命令:`jar -cvf my.jar com\ysx\check\loader\Test1.class` 打完jar包后入`C:\Program Files\Java\jdk1.8.0_291\jre\lib\ext`下,重新运行
输出:
```java
ext Test1 init
sun.misc.Launcher$ExtClassLoader@36d64342
```
此时使用的是扩展类加载器加载
#### 双亲委派机制
所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则
注意:
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
源码分析
```java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//1.检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//启动类加载器在java获取为null,如果parent为null,则证明此时为扩展类加载器
if (parent != null) {
//2.有上级的话,委派上级loadClass
c = parent.loadClass(name, false);
} else {
//3.如果没有上级了(ExtClassLoader),则委派BootStrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//4.每一层都找不到,调用findClass方法(每个类加载器自己扩展)来加载
c = findClass(name);
// this is the defining class loader; record the stats
//5.记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
```
#### 线程上下文类加载器
我们再使用`JDBC`时,都需要加载Driver驱动,即使不屑
```java
Class.forName("com.mysql.jdbc.Driver")
```
也是可以让`com.mysql.jdbc.Driver`正确加载的,你知道是怎么做的吗?
根据源码分析:
```java
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
private static volatile int loginTimeout = 0;
private static volatile java.io.PrintWriter logWriter = null;
private static volatile java.io.PrintStream logStream = null;
// Used in println() to synchronize logWriter
private final staticObject logSync = new Object();
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@Code ServiceLoader} mechanism
*/
static {
//加载初始化驱动
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
```
DriverManager的类加载器使用的是启动类加载器,会到JAVA_HOME\jre\lib下搜索类,但是JAVA_HOME\jre\lib下显然没有mysql-connector-java-5.1.47.jar包,那么,在DriverManager的静态代码块种,怎么能正确加载`com.mysql.jdbc.Driver`?
```java
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
//1、使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
//2、使用jdbc.drivers定义驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//这里ClassLoader.getSystemClassLoader()就是应用程序类加载器,此时jdk打破了双亲委派机制,因为有些类在启动类加载器路径下找不到
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
```
> ServiceLoader
Service Provider Interface (SPI)
约定如下:在jar包的MATE-INF/services 包下,以接口权限定义名为文件,文件内容是实现类名称
这样就可以使用
```java
ServiceLoader<接口类型> loadedDrivers = ServiceLoader.load(接口类型.class);
Iterator<接口类型> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
```
来得到实现类,体现的是【面向接口变成+解耦】的思想,在下面的一些框架中都运用了此思想
- JDBC
- Service初始化器
- Spring容器
- Dubbo(对SPI进行了扩展)
```java
public static <S> ServiceLoader<S> load(Class<S> service) {
//得到当前线程的类加载器,称为线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
```
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载
```java
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn+ " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
```
#### 自动有类加载器
场景:
- 想加载非classpath随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
1. 继承ClassLoader父类
2. 要遵从双亲委派机制,重写findClass方法
- 注意不是重写loadClass方法,否则不会走双亲委派机制
3. 读取类文件的字节码
4. 调用父类的defineClass方法来加载类
5. 使用者调用该类加载器的loadClass方法
示例:
```java
public class TestLoader {
public static void main(String[] args) throws Exception {
LoaderTest loaderTest = new LoaderTest();
Class<?> c1 = loaderTest.loadClass("Test1");
Class<?> c2 = loaderTest.loadClass("Test1");
System.out.println(c1 == c2);
LoaderTest loaderTest2 = new LoaderTest();
Class<?> c3 = loaderTest2.loadClass("Test1");
System.out.println(c1 == c3);
}
}
class LoaderTest extends ClassLoader {
@Override//name就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "D:\\test\\"+name+".class";
ByteArrayOutputStream os = null;
try {
os = new ByteArrayOutputStream();
Files.copy(Paths.get(path),os);
byte[] bytes = ((ByteArrayOutputStream) os).toByteArray();
return defineClass(name,bytes,0,bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到");
}
}
}
```
### 运行期优化
#### 即时编译
> 分层编译
```java
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
```
JVM将执行状态分成了五个层次:
- 0层,解释执行(Interpreter)
- 1层,使用C1即时编译器编译执行(不带profiling)
- 2层,使用C1即时编译器编译执行(带基本的profiling)
- 3层,使用C1即时编译器编译执行(带完全的profiling)
- 4层,使用C2即时编译器编译执行
```tex
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
```
即时编译器(JIT)与解释器的区别
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- JIT是将一些字节码编译为机器码,并存储Code Cache,下次再遇到相同代码,直接执行,无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- JIT会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用diamond,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度,执行效率上简单比较一下Interpreter< C1 < C2总的目标是发现热点代码(hotspot名称的又来),进行优化
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用-XX:-DoEscapeAnalysis关闭逃逸分析,再运行刚才的示例观察结果
> 方法内联
(Inlining)
```java
private static int square(final int i) {
return i * i;
}
```
```java
System.out.println(square(9));
```
如果发现square是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置
```java
System.out.println(9*9);
```
还能进行常量折叠(constant folding)的优化
```java
System.out.println(81);
```
-XX:+UnlockDiagnosticVMOptions-XX:+PrintInlining (解锁隐藏参数)打印inlining信息
-XX:CompileCommand=dontinline,Test3.square 禁用方法内联
-XX:+PrintCompilation
```java
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
```
#### 字段优化
JVM 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh
#### 反射优化
```java
public class Reflect1 {
public static void foo() {
System.out.println("foo ......");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t",i);
foo.invoke(null);
}
System.in.read();
}
}
```
foo.invoke前面0~15此调用使用的是MethodAccessor的NativeMethodAccessorImpl(本地方法访问器)实现
源码分析
```java
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
```
```java
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
//当本地方法的invoke调用次数超过ReflectionFactory.inflationThreshold()时就会把本地方法访问器替换为运行期间动态生成的方法访问器
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
//替换原本的NativeMethodAccessorImpl
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
```
动态生成的方法访问器
```java
public class GeneratorMethodAccessor1 extend MethodAccessorImpl{
public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
//比较奇葩的做法,如果由参数,那么抛非法参数异常
block4 : {
if(arrobject == null || arrobject.length == 0)
throw new IllegalArgumentException();
}
try {
//此时,已经是直接调用了
Reflect1.foo();
//没有返回值
return null;
} catch (Throwable throwable) {
throw new InvocationTargetException();
} catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(Object.super.toString());
}
}
}
```
在0-15次循环过程中,jvm都是通过反射调用执行foo()方法,但是在第16次及其以后是由动态生成的方法访问器进行invoke调用,此时是直接使用对象.方法(Reflect1.foo)进行调用,
```java
public class ReflectionFactory {
private static boolean initted = false;
private static final Permission reflectionFactoryAccessPerm = new RuntimePermission("reflectionFactoryAccess");
private static final ReflectionFactory soleInstance = new ReflectionFactory();
private static volatile LangReflectAccess langReflectAccess;
private static volatile Method hasStaticInitializerMethod;
private static boolean noInflation = false;
private static int inflationThreshold = 15;//膨胀阈值
private static void checkInitted() {
if (!initted) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.out == null) {
return null;
} else {
String var1 = System.getProperty("sun.reflect.noInflation");
if (var1 != null && var1.equals("true")) {
ReflectionFactory.noInflation = true;
}
var1 = System.getProperty("sun.reflect.inflationThreshold");
if (var1 != null) {
try {
ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
} catch (NumberFormatException var3) {
throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
}
}
ReflectionFactory.initted = true;
return null;
}
}
});
}
}
}
```
注意:
通过查看ReflectionFactory源码得知
- sun.reflect.noInFlation可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较好事,如果仅反射调用一次,不划算)
- ```
sun.reflect.inflationThreshold可以修改膨胀阈值
```
- 也可以修改`sun.reflect.noInflation`禁用膨胀
## 内存模型
很多人将`Java内存结构` 和`java内存模型`分不清,【Java 内存模型】是java Memory Model(JMM)
简单的说,JMM定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
#### 问题
synchronized(同步关键字)
```java
synchronized (对象) {
要作为原子操作代码
}
```
#### 解决方法
```java
public class Test5 {
static int i = 0;
static Object obj = new Object();
public static void main(String[] args)throws InterruptedException {
Thread s1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
i++;
}
}
});
Thread s2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
i--;
}
}
});
s1.start();
s2.start();
s1.join();
s2.join();
System.out.println(i);
}
}
```
### 原子性
```java
static int i = 0;
static Object obj = new Object();
public static void main(String[] args)throws InterruptedException {
Thread s1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
i++;
}
});
Thread s2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
i--;
}
});
s1.start();
s2.start();
s1.join();
s2.join();
System.out.println(i);
}
```
以上结果可能是正数、负数、零,因为java 中对静态变量的自增,自减并不是原子操作,例如对i++而言,实际会产生如下的JVM指令
```java
getstatic i//获取静态变量1的值
iconst_1 //准备常量1
iadd //自增
putstatic i//将修改后的值存入静态变量
```
而对应i--也是类似的:
```java
getstatic i//获取静态变量1的值
iconst_1 //准备常量1
isub //自减
putstatic i//将修改后的值存入静态变量
```
而java 的内存模型如下,完成静态变量的自增,自减需要再主存和线程内存中进行数据交换
详情查看:***JMM内存模型.vsd***
如果是单线程以上8行代码是顺序执行没有问题;
```java
getstatic i//线程1-》获取静态变量1的值i = 0
iconst_1 //线程1-》准备常量1
iadd //线程1-》自增i = 1
putstatic i//线程1-》将修改后的值存入静态变量 i = 1
getstatic i//线程1-》获取静态变量1的值 i = 1
iconst_1 //线程1-》准备常量1
isub //线程1-》自减i = 0
putstatic i//线程1-》将修改后的值存入静态变量 i = 0
```
但是多线程下这8行代码可能会交错运行:
出现负数的情况
```java
//假设i的初始值为0
getstatic i//线程1-》获取静态变量1的值 i = 0
getstatic i//线程2-》获取静态变量1的值 i = 0
iconst_1 //线程1-》准备常量1
iadd //线程1-》自增i = 1
putstatic i//线程1-》将修改后的值存入静态变量 i = 1
iconst_1 //线程2-》准备常量1
isub //线程2-》自减 线程内 i = -1
putstatic i//线程2-》将修改后的值存入静态变量 i= -1
```
出现正数的情况
```java
//假设i的初始值为0
getstatic i//线程1-》获取静态变量1的值 i = 0
getstatic i//线程2-》获取静态变量1的值 i = 0
iconst_1 //线程1-》准备常量1
iadd //线程1-》自增i = 1
iconst_1 //线程2-》准备常量1
isub //线程2-》自减 线程内 i = -1
putstatic i//线程2-》将修改后的值存入静态变量 i= -1
putstatic i//线程1-》将修改后的值存入静态变量 i = 1
```
### 可见性
存在一个现象,mainxia昵称对run变量的修改对于t线程不可见,导致t线程无法停止
```java
static boolean run = true;
public static void main(String[] args)throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
//...
}
});
t.start();
Thread.sleep(1000);
run = false;//线程t不会如预想的停止
}
```
> 分析
1. 初始状态,t线程刚开始从主内存读取run的值到达工作内存
2. 因为t线程需要频繁的从主内存中读取run的值,JIT编译器会将run的值缓存到自己工作内存中的高速缓存中,减少对主内存中run的访问,提高效率
3. 1秒后,main线程修改了run的值,并同步至主存,而t是从自己工作内存的高速缓存中来读取这个变量的值,结果永远是旧值
> 解决
volatile(易变关键字)
它可以修饰成员变量喝静态成员变量,它可以避免线程从自己的工作内存中读取静态变量的值,必须要到主存中获取它的值,线程操作volatile变量都是直接操作主存
定义:保证在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
```java
getstatic run//线程t-》获取静态变量run的值 run true
getstatic run//线程t-》获取静态变量run的值 run true
getstatic run//线程t-》获取静态变量run的值 run true
getstatic run//线程t-》获取静态变量run的值 run true
getstatic run//线程t-》获取静态变量run的值 run true
putstatic run//线程 main 修改 run 值为 false
getstatic run//线程t-》获取静态变量run的值 run true
```
注意:
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对较低
如果在前面的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,为什么?
```java
//println中使用synchronized,保证了t线程只能从主内存中读取变量值
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
```
### 有序性
```java
public class Test5 {
int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(IResult r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(IResult r) {
num = 2;
ready = true;
}
}
class IResult {
public static int r1;
}
```
问结果存在几种?
1. 线程1先执行,此时reday = false,所以进入else分支,结果为1;
2. 线程2先执行num = 2,但还没来得及执行 ready = true,线程1执行,还是进入else,结果为1;
3. 线程2执行到ready = true,线程1执行,此时进入if分支,结果为4
但是可能还存在线程2执行ready = true,切换到线程1,进入if分支,相加为0,再切回线程2执行num = 2
这种现象叫做指令重排,是JIT编译器在运行时的一些优化,这个现象需要大量测试才能复现:
可以借助 java 并发压测工具 jcsstess复现:http://wiki.openjdk.java.net/display/CodeTools/jcstress
> 解决
volatile可以禁用指令重排
> 理解
同一个线程内,JVM会在不影响正确性的前提下,可以调整预警的执行顺序,由于不会对最终结果产生影响,可能会对代码的执行顺序进行调整
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的**double-checked locking(双重校验锁)**模型实现单例
```java
final class SingleTon {
private SingleTon() {}
private static SingleTon INSTANCE = null;
public static SingleTon getInstance() {
//实例没创建,才会进入内部的synchronized代码块
if (INSTANCE == null) {
synchronized (SingleTon.class) {
//也许有其他线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new SingleTon();
}
}
}
return INSTANCE;
}
}
```
特点:
- 懒惰实例化
- 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
但是在多线程环境下,上面的代码是有问题的,INSTANCE = new SingleTon();对应的字节码为:
```
0:new #2 //Class com/ysx/check/loader/SingleTon
3:dup
4:invokespecial #3 //Method "<init>":()V
7:putstatic #4 Field INSTANCE:L com/ysx/check/loader/SingleTon;
```
其中4,7两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按如下时间序列执行
```
1 t1线程执行到 INSTANCE = new SingleTon() ;
2 t1线程分配空间,为SingleTon对象生成了引用地址(0)
3 t1线程将引用地址赋值给 INSTANCE,此时 INSTANCE != null(7)
4 t2线程进入getInstance()方法,发现 INSTANCE != null (synchronized块外),直接返回INSTANCE
5 t1线程执行SingleTon构造方法(4)
```
此时t1还未完全将构造方法执行完毕,如果再构造方法中要执行很多的初始化操作,那么t2拿到的将是一个未初始化完全的单例
解决:
对`INSTANCE` 变量使用volatile变量修饰即可,可以禁用指令重排,但要注意在JDK 5以上版本的volatile才会有效
### happens-before
happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结
- 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
```java
staticint x;
staticObject m = new Object()
public static void main(String[] args) {
new Thread(() -> {
synchronized (m) {
x = 10;
}
},"t1").start();
new Thread(() -> {
synchronized (m) {
System.out.println(x);
}
},"t2").start();
}
```
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
```java
staticint x;
public static void main(String[] args) {
x = 20;
new Thread(() -> {
System.out.println(x);
},"t2").start();
}
```
- 线程结束前的写,对其他线程得知它结束后的读可见
```java
staticint x;
public static void main(String[] args)throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(x);
});
t1.start();
t1.join();
System.out.println(x);
}
```
- 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被大端口对变量的读可见
```java
staticint x;
public static void main(String[] args)throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true) {
//如果线程被打断后,跳出循环
if (Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
});
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
//打断t2
t2.interrupt();
},"t1").start();
while (!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
```
- 对变量默认值(0,false,null) 的写,对其他线程对该变量的读可见
- 具有传递性,如果x hb- > y 并且 y hp- > z ,那么有x hp- > z
### CAS 与原子类
#### CAS
CAS 即 Compare and Swap,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行+1操作;
```java
//需要不断尝试
while (true) {
int 旧值 = 共享变量;//比如拿到了当前的值 0
int 结果 = 旧值 +1;//在旧值 0 的基础上增加了 1 ,正确结果是 1
/*
此时如果别的线程把共享变量改成了 5 ,本线程的正确结果 1 就报废了,这时候 compareAndSwap 返回false,重新尝试
,直到:
compareAndSwap 返回true,表示我本线程做修改的同时,别的线程没有干扰
*/
if (compareAndSwap(旧值,结果)) {
//成功,退出循环
}
}
```
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。
- 没有使用synchronized,所以线程不会陷入阻塞,这是效率提升因素之一
- 但是如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响
CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,具体可查看`多线程高级并发JUC.md`
> 乐观锁和悲观锁
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试
- synchronized是基于悲观锁的思想:最悲观的估计,需要防范其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁以后你们才能改
#### 原子类
原子操作类:jdk1.5以后提供了juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用了CAS技术+volatile来实现的
例如:
```java
//创建原子整数对象
private static AtomicInteger x = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
x.getAndIncrement();//获取并且自增 i++
//x.incrementAndGet() ;//自增并且获取 ++i;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
x.getAndDecrement();//获取并自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
```
### synchronized 优化
java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark Word),Mark Word平时存储这个对象的 *哈希码*、分代年龄,当加锁时,这些信息就根据情况被替换为 *标记位*、线程锁记录指针、重量级锁指针、线程ID等内容
#### synchronized-轻量级锁
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
```java
static Object obj = new Object();
public static void method() {
synchronized (obj) {
//同步块A
method2();
}
}
public static void method2() {
synchronized (obj) {
//同步块B
}
}
```
| 线程1 | 对象Mark Word | 线程2 |
| --------------------------------------- | --------------------------- | --------------------------------------- |
| 访问同步块A,把Mark复制到线程1的锁记录| 01(无锁) | |
| CAS修改Mark为线程1锁记录地址 | 01(无锁) | |
| 成功(加锁) | 00(轻量锁)线程1锁记录地址 | |
| 执行同步块A | 00(轻量锁)线程1锁记录地址 | |
| 访问同步块B,把Mark 复制到线程1的锁记录 | 00(轻量锁)线程1锁记录地址 | |
| CAS修改Mark为线程1锁记录地址 | 00(轻量锁)线程1锁记录地址 | |
| 失败(发现是自己线程的锁) | 00(轻量锁)线程1锁记录地址 | |
| 锁重入 | 00(轻量锁)线程1锁记录地址 | |
| 执行同步块B | 00(轻量锁)线程1锁记录地址 | |
| 同步块B执行完毕 | 00(轻量锁)线程1锁记录地址 | |
| 同步块A执行完毕 | 00(轻量锁)线程1锁记录地址 | |
| 成功(解锁) | 01(无锁) | |
| | 01(无锁) | 访问同步块A,把Mark复制到线程1的锁记录|
| | 00(轻量锁)线程2锁记录地址 | CAS修改Mark为线程2锁记录地址 |
| | 00(轻量锁)线程2锁记录地址 | 成功(加锁) |
| | 00(轻量锁)线程2锁记录地址 | 执行同步块A |
| | 00(轻量锁)线程2锁记录地址 | 访问同步块B,把Mark 复制到线程2的锁记录 |
| | 00(轻量锁)线程2锁记录地址 | CAS修改Mark为线程2锁记录地址 |
| | 00(轻量锁)线程2锁记录地址 | 失败(发现是自己线程的锁) |
| | 00(轻量锁)线程2锁记录地址 | 锁重入 |
| | 00(轻量锁)线程2锁记录地址 | 执行同步块B |
| | 00(轻量锁)线程2锁记录地址 | 同步块B执行完毕 |
| | 00(轻量锁)线程2锁记录地址 | 同步块A执行完毕 |
| | 01(无锁) | 成功(解锁) |
#### 锁膨胀
如果在尝试轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁。
```java
static Object obj = new Object();
public static void method() {
synchronized (obj) {
//同步块
}
}
```
#### 重量级锁
重量级锁竞争的时候,还可以使用自旋进行优化,如果当前线程自旋成功(即这时候持锁线程一句退出了同步块,释放了锁),这时当前线程就可以避免阻塞
在java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- 自旋会占用CPU时间,单核CPU自旋就是浪费,需要在多核CPU场景下才能发挥优势
- 好比等红灯时汽车不是熄火,不熄火相当于自旋(等待短了时间划算,减少了线程上下文切换的时间),熄火相当于阻塞(时间长了划算)
- java7以后不能控制是否开启了自旋功能
#### 其他优化
- 减少上锁时间:同步代码块中尽量短
- 减少锁的粒度:将一个锁分为多个锁提高并发度
- ConcurrentHashMap
- LongAdder分为base和cells两部分。没有并发争用的时候或是cells数组正在初始化的时候,会使用CAS来累加值到base,有并发争用,会初始化cells数组,数组有多少个cell,就允许有多少线程冰心修改,最后将数组中每个cell累加,再加上base就是最终的值
- LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
- 锁粗化;多次循环进入代码块不如同步块内多次循环,另外JVM可能会做如下优化,,把多次append的加锁操作粗话为一次(因为都是对同一个对象加锁,没必要重入多次)
- `new StringBuffer().append("a").append("b").append("c")`
- 锁消除:JVM会进行代码的逃逸分析,例如某个加锁对象时方法内局部变量,不会被其他线程所访问到,这时候就会被即时编译器忽略掉所有同步操作
- 读写分离:
- CopyOnWriteArrayList
- CopyOnWriteSet
如果嫌太长可以下载zip的包,对应的流程图vsd都在里面,想学习的可以看下
技术贴,学习下 一起学习,感谢分享 谢谢楼主分享! 感谢楼主分享,又是学习的一天 楼主能提供视频学习下么 感谢大佬分享。
在校期间没有好好学习java,现在来补习真的是补到头秃:'(weeqw:'(weeqw:'(weeqw 支持一下 这个厉害了 感谢,学习了