目录
一.引言
1.什么是JVM
2.学习路线
二.内存结构
1.程序计数器
1.1定义
1.2作用
2.虚拟机栈
2.1定义
2.2栈内存溢出(StackOverflowError)
2.3线程运行诊断
3.本地方法栈
4.堆
4.1定义
4.2堆内存溢出(OutOfMemoryError)
4.3堆内存诊断
5.方法区
5.1定义
5.2组成
5.3方法区内存溢出
5.4运行时常量池
5.5StringTable
5.6StringTable特性
5.7StringTable位置
5.8StringTable垃圾收
5.9StringTable性能调优
6.直接内存
6.1定义
6.2直接内存基本使用
6.3直接内存回收原理
7垃圾回收
7.1判别对象可以回收
7.1.1引用计数法
7.1.2可达性分析算法
7.1.3四种引用
7.2垃圾回收算法
7.2.1标记清除算法
7.2.2标记整理算法
7.2.3复制算法
7.3分代垃圾回收
7.3.1相关VM参数
7.4垃圾回收器
7.4.1串行垃圾回收器
7.4.2吞吐量优先垃圾回收器
7.4.3响应时间优先垃圾回收器
7.4.4 G1
7.5垃圾回收调优
7.5.1调优领域
7.5.2确定目标
7.5.3不发生GC最快
7.5.4新生代调优
7.5.5老年代调优
7.5.6案例
三.类加载与字节码技术
1.类文件结构
2.字节码指令
2.1 javap工具
2.2 图解方法执行流程
2.3 练习分析a++
2.4 构造方法
2.5 方法调用
2.6多态的原理
2.7异常处理
2.8Synchronized
3. 编译期处理(语法糖)
4. 类加载阶段
4.1 加载
4.2 链接
4.3 初始化
5. 类加载器
5.2 双亲委派模式
5.3 自定义类加载器
6. 运行期优化
6.1 分层编译
6.2 方法内联
6.3 反射优化
7. java 内存模型 (JMM)
7.1 原子性
7.2 可见性
7.3 有序性
7.4 happens-before
8. CAS与原子类
8.1 CAS
8.2 乐观锁和悲观锁
9. synchronized 优化
9.1 轻量级锁
9.2锁膨胀
9.3重量锁
9.4偏向锁
9.5 其他优化
JVM:java virtual machine(java虚拟机),Java程序的运行环境,是二进制字节码运行环境
好处:
比较:jvm jre jdk
基础类库:集合类,线程类,IO类等
编译工具:javac,javap
应用服务器工具:tomcat
而程序计数器就会记住下一条需要执行的的jvm指令的序号
特点:
栈:线程运行的内存空间
栈帧:每个方法运行时需要的内存(存放参数,局部变量,返回地址)
问题辨析:
垃圾回收是否涉及栈内存?
栈内存分配越大越好吗?
方法内的局部变量是否线程安全?
造成原因:
案例1:cpu占用过多
定位步骤:
执行top命令:
jstack 进程id命令
注:nid是pid的16进制转换
案例2:程序运行很长时间没有结果
定位同上,执行jstack 进程id命令。
查询结果是线程死锁。
给本地方法运行提供内存空间。
本地方法:用c或c++写的系统本地的方法(native修饰的),例如:object类中的clone(),hashCode(),notify()等
通过new关键字创建的对象都会使用堆内存。
特点:
代码演示:
package com.erp.payroll.test.VO;
import java.util.ArrayList;
import java.util.List;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/17 15:29
*/
public class JvmDemo {
public static void main(String[] args) {
int i = 0;
try {
List list = new ArrayList<>();
String s = "haha";
while (true) {
list.add(s);
s = s + s;
i++;
}
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println(i);
}
}
}
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.erp.payroll.test.VO.JvmDemo.main(JvmDemo.java:20)
26
堆空间内存也可以通过-Xmx 设置 ,例如:设置8m -Xmx8m。
工具:
1.jps工具
2.jmap工具(某一时刻)
3.jconsole工具
演示:当执行1时候,堆中没有过多的内存被占用,但是在创建bytes后,就会增加10mb,在执行了2之后,会调用gc清理,堆内存占用又会减少。
package com.erp.payroll.test.VO;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/17 15:29
*/
public class JvmDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("1=============");
TimeUnit.SECONDS.sleep(30);
byte[] bytes = new byte[1024 * 1024 * 10];//10mb
System.out.println("2=============");
TimeUnit.SECONDS.sleep(30);
bytes = null;
System.gc();
System.out.println("3=============");
TimeUnit.SECONDS.sleep(50);
}
}
执行命令
F:\***\erp-payroll>jps
得到
8844 JvmDemo
打印1=============之后
执行
F:\***\erp-payroll>jmap -heap 8844
堆内存使用:18187392
Heap Usage:
PS Young Generation
Eden Space:
capacity = 58720256 (56.0MB)
used = 18187392 (17.3448486328125MB)
free = 40532864 (38.6551513671875MB)
30.972943987165177% used
打印2=============之后,执行
Heap Usage:
PS Young Generation
Eden Space:
capacity = 58720256 (56.0MB)
used = 28673168 (27.344863891601562MB)
free = 30047088 (28.655136108398438MB)
48.83011409214565% used
打印3=============之后,执行
Heap Usage:
PS Young Generation
Eden Space:
capacity = 58720256 (56.0MB)
used = 2348848 (2.2400360107421875MB)
free = 56371408 (53.75996398925781MB)
4.000064304896763% used
jconsole工具使用
启动main方法,执行命令
F:\***\erp-payroll>jconsole
弹出视图工具
动态查看堆内存使用情况
案例:
垃圾回收后,内存占用仍然很高(详情见视频)
通过jsp可以看出新生代堆内存清理后,老年代还在被占用(对象被引用,无法释放)
另一个视图工具
执行命令:(高版本jdk没有内置,需自主安装)
F:\****\erp-payroll>jvisualvm
方法区是所有线程共享的区域。存储了跟类的结构相关的信息,有成员变量,方法数据,成员方法,构造方法,运行时常量池等
方法区是在类启动时被创建。
不同版本的jdk内存结构有所不同
1.6中方法区占用了堆内存;字符串常量池放在了方法中的运行时常量池中
1.8之后方法区放在了本地内存中;字符串常量池放进了堆中。
1.8之后方法区放在本地内存中,所以内存溢出不太容易出现,可以把相关参数设置较小再进行测试。
元空间参数设置 -XX:MaxMetaspaceSize=8m
永生代参数设置 -XX:MaxPermSize=8m
并且两种版本号内存溢出报错也有所不同
元空间内存溢出:OutOfMemoryError:Metaspace
永生代内存溢出:OutOfMemoryError:PermGen space
package com.erp.payroll.test.VO;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/17 15:29
*/
/**
* 元空间内存溢出:OutOfMemoryError:Metaspace
* 永生代内存溢出:OutOfMemoryError:PermGen space
* 元空间参数设置 -XX:MaxMetaspaceSize=8m
* 永生代参数设置 -XX:MaxPermSize=8m
*/
public class JvmDemo extends ClassLoader {// 可以用来加载类的二进制字节码
public static void main(String[] args) throws InterruptedException {
int j = 0;
try {
JvmDemo test = new JvmDemo();
for (int i = 0; i < 10000; i++, j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,public,类名,包名,父类,接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i,
null, "java/lang/Object", null);
//返回byte[]
byte[] code = cw.toByteArray();
//执行类加载器
test.defineClass("Class" + i, code, 0, code.length);//Class对象
}
} finally {
System.out.println(j);
}
}
}
演示查看字节码详情中的常量池:
代码:
package com.erp.payroll.test.VO;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/18 10:53
*/
//二进制字节码(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
操作指令:
1.先cd到文件的上一级目录
2.使用javac编译出字节码
***test\VO>javac HelloWorld.java
3.使用javap查看字节码详情
****test\VO>javap -v HelloWorld.class
二进制字节码详情:(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
类的基本信息:
Last modified 2022-2-18; size 450 bytes
MD5 checksum 284a6f82e2f72e0f5adafd276b197cef
Compiled from "HelloWorld.java"
public class com.erp.payroll.test.VO.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
运行时常量池:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/erp/payroll/test/VO/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/erp/payroll/test/VO/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
类的方法定义:包含了虚拟机指令
{
public com.erp.payroll.test.VO.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 9: 0
public static void main(java.lang.String[]);
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 11: 0
line 12: 8
}
SourceFile: "HelloWorld.java"
虚拟机指令:
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
常量池作用:
例如行号3:对应的#3,表示进入运行时常量池中的#3进行翻译
运行时常量池中的#3对应的#18,继续翻译
#18对应 Utf8 hello world!
表示对应的是字符串形式的 hello world!
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量信息。
运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
面试题:
package com.erp.payroll.test.VO;
import java.sql.SQLOutput;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/18 14:02
*/
public class StringDemo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
//问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();
//问
System.out.println(x1 == x2);
//调换最后两行代码位置呢,如果是jdk1.6呢
String x3 = new String("c") + new String("d");
x3.intern();
String x4 = "cd";
System.out.println(x1 == x2);
}
}
常量池和串池的关系:
public class StringDemo2 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
查看二进制字节码:
Last modified 2022-2-18; size 334 bytes
MD5 checksum 7b08065901c43270c4a91a4d85f35540
Compiled from "StringDemo2.java"
public class com.erp.payroll.test.VO.StringDemo2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = String #16 // a
#3 = String #17 // b
#4 = String #18 // ab
#5 = Class #19 // com/erp/payroll/test/VO/StringDemo2
#6 = Class #20 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 StringDemo2.java
#15 = NameAndType #7:#8 // "":()V
#16 = Utf8 a
#17 = Utf8 b
#18 = Utf8 ab
#19 = Utf8 com/erp/payroll/test/VO/StringDemo2
#20 = Utf8 java/lang/Object
{
public com.erp.payroll.test.VO.StringDemo2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 9: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 14: 9
}
SourceFile: "StringDemo2.java"
常量池中的信息都会被加载到运行时常量池中(这时a,b,ab都是常量池中的符号,还没变为java字符串对象)。
当执行到引用的代码上才会成为java字符串对象,
String s1 = "a";
0: ldc #2 // String a
#2 = String #16 // a
#16 = Utf8 a
ldc #2会把a符号变成“a”字符串对象,在这个过程中会先准备串池StringTable[ ]的空间,并先以a为key查询串池中是否存在(不存在则放入串池)。 StringTable[ ]是hashTable结构
s4执行时做了什么:
String s4 = s1 + s2;
二进制字节码:
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu
ilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu
ilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
在这个过程中它做了这些事情:new StringBuilder( ).append("a").append("b").toString( )
但是查看toString()源码会发现,它是new了一个新的String(new String(“ab”))。
所以s4指向了一个堆内的地址。s3不等于s4。
s5:
String s5 = "a" + "b";
29: ldc #4 // String ab
31: astore 5
它是直接拿到了字符串ab。与String s3 = “ab”相同 (是在编译期确定的为ab的)
案例1:
String s = new String("a") + new String("b");
动态的拼接是存在于堆中的,相当于new了一个“ab”
String s1 = s.intern();
此方法会将s(“ab”)放到串池中。如果串池中有“ab”,则不会放入,如果没有,则会放入(s的地址指向串池中“ab”),并将串池中的对象返回 。s1一定是串池中的对象。
结论:
System.out.println(s=="ab");//true
System.out.println(s1=="ab");//true
但是如果没有 String s1 = s.intern(); 则s==“ab”为false(ab是串池中的对象,s是堆中的对象)。
案例2:
String x = "ab";
String s = new String("a") + new String("b");
String s1 = s.intern();
System.out.println(s=="ab");
System.out.println(s1=="ab");
此代码与案例1代码不同之处在于在执行到x=“ab”时就已经将ab放进了串池中,所以在执行到s1时候将s(在堆中)放进串池,发现串池中已经存在(则没有放进串池,地址就没办法指向串池),则s还是堆中的ab。s1还是指向的串池中的ab。
回看面试题:
package com.erp.payroll.test.VO;
import java.sql.SQLOutput;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.VO
* @date 2022/2/18 14:02
*/
public class StringDemo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//常量拼接编译期就优化直接指向串池
String s4 = s1 + s2;//动态拼接相当于在堆中new了一个ab
String s5 = "ab";//常量池
String s6 = s4.intern();//如果串池中没有则将s4放入串池。有则不放入。s6一定是串池中对象
//问
System.out.println(s3 == s4);//false,s4在堆中
System.out.println(s3 == s5);//true,都在串池中
System.out.println(s3 == s6);//true,都在串池中
String x1 = new String("c") + new String("d");
String x2 = "cd";
x1.intern();
//问
System.out.println(x1 == x2);//false,x1在堆中,x2在串池中
//调换最后两行代码位置呢,如果是jdk1.6呢
String x3 = new String("e") + new String("f");
x3.intern();//1.8将x3放入串池,如果没有则放入(堆与串池同地址),有则不放(与串池对象不同)
//1.6将x3浅复制放进串池,x3与串池对象一定不同。
String x4 = "ef";
System.out.println(x3 == x4);//true
}
}
因StringTable引起堆内存不够用时,垃圾回收机制也会对StringTable进行垃圾回收。
StringTable的底层是hash表。
所以hash表的桶的个数对StringTable的性能息息相关。
定义:
IO读写
使用DirectBuffer
直接内存是操作系统和java代码都可以访问的一块区域,无需将代码从系统内存复制到java堆内存,从而提高了效率。(NIO是对DirectBuffer 直接内存做了进一步优化)
public class Code_06_DirectMemoryTest {
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接内存 是被 unsafe 创建与回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
// 演示 直接内存被 释放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
}
}
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
allocateDirect方法是创建了一个DirectByteBuffer对象。
第二步:DirectByteBuffer 类
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size); // 申请内存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
att = null;
}
类中包含了申请内存的过程并且还调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。
public void clean() {
if (remove(this)) {
try {
// 都用函数的 run 方法, 释放内存
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法。
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 释放内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
直接内存的回收机制总结
一个对象被另一个对象引用时,引用计数就会加1,不被此引用时,计数就会减1。当引用计数值为0时,说明对象没有被引用,就可以被垃圾回收器回收。
但是这种方法还是有一种弊端,如果出现两个类循环引用时就会导致计数器永远不会为0,使两个对象都无法被释放。
补充:虚引用和引用队列同时存在,betybuffer没有强行用了 虚引用就会进入虚引用队列,一个检查虚引用队列的线程会启用unsafe把直接内存释放掉,采用虚引用的目的就是释放直接引用
软引用:Object a = new Object(); SoftReference
弱引用:Object a = new Object(); WeakReference
软引用演示:
/**
* 演示 软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Code_08_SoftReferenceTest {
public static int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
method2();
}
// 设置 -Xmx20m , 演示堆内存不足,
public static void method1() throws IOException {
ArrayList list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
// 演示 软引用
public static void method2() throws IOException {
ArrayList> list = new ArrayList<>();
for(int i = 0; i < 5; i++) {
SoftReference ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for(SoftReference ref : list) {
System.out.println(ref.get());
}
}
}
method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。
method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:
上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下:
// 演示 软引用 搭配引用队列
public static void method3() throws IOException {
ArrayList> list = new ArrayList<>();
// 引用队列
ReferenceQueue queue = new ReferenceQueue<>();
for(int i = 0; i < 5; i++) {
// 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
SoftReference ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
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 ref : list) {
System.out.println(ref.get());
}
}
这样对应的软引用对象也会被清理。
弱引用演示:
public class Code_09_WeakReferenceTest {
public static void main(String[] args) {
// method1();
method2();
}
public static int _4MB = 4 * 1024 *1024;
// 演示 弱引用
public static void method1() {
List> list = new ArrayList<>();
for(int i = 0; i < 10; i++) {
WeakReference weakReference = new WeakReference<>(new byte[_4MB]);
list.add(weakReference);
for(WeakReference wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
}
// 演示 弱引用搭配 引用队列
public static void method2() {
List> list = new ArrayList<>();
ReferenceQueue queue = new ReferenceQueue<>();
for(int i = 0; i < 9; i++) {
WeakReference weakReference = new WeakReference<>(new byte[_4MB], queue);
list.add(weakReference);
for(WeakReference wake : list) {
System.out.print(wake.get() + ",");
}
System.out.println();
}
System.out.println("===========================================");
Reference extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
for(WeakReference wake : list) {
System.out.print(wake.get() + ",");
}
}
}
JVM不是使用单一的回收算法,而是结合使用这些回收算法,协同工作。
所谓的清除不是直接清零内存,而是把被清除对象占用的起始内存地址放到空闲列表中,下次被分配的对象会从内存地址中找,找到空间合适的进行覆盖。
标记清除算法,垃圾回收速度快,但是容易产生内存碎片(大内存对象(数组)无法放入某一个内存碎片)。
整理的是内存碎片,不会产生内存碎片
整理时内存地址是进行变化的,相对应对象的引用也会有所变化,所以涉及改变的东西比较多,会影响垃圾回收速度。
交换FROM和TO两块内存空间。
复制算法没有内存碎片但是会占用双倍的内存空间。
长时间使用的对象放在老年代中,容易被丢弃的对象回放到新生代当中。新生代中垃圾回收频繁。
当添加一个大对象时(超过新生代总内存),新生代无法放入,就会直接放入老年代。
伊甸园放不下时候会发生一次MinorGC(标记算法),然后把存活的对象再放进幸存区To中,并将幸存这些存活的对象幸存寿命加1(复制算法),并将幸存区From,To进行交换。
MinorGC会引发stop the world。(其他用户线程停止动作,等垃圾回收结束后,才会恢复),因为对象移动时会造成地址的改变。如果不停止工作会造成对象引用找不到。
然后第二次向伊甸园添加数据。 伊甸园放不下时候又会发生一次MinorGC(标记算法),但是本次会将幸存区From也进行MinorGC。
并继续执行存活的对象幸存寿命加1(复制算法),将幸存区From,To进行交换。
当幸存寿命达到阈值(最大是15,4bit)时,就会将该对象晋升到老年代。
当老年代空间不足,会先触发一次MinorGC,如果执行完新生代同样空间不足时就会发生一次FullGC(对新生代,老年代都进行回收)。
FullGC也会引起stop to world,并且时间会更长(标记整理算法)。
FullGC执行后如果空间还是不足,就会报错(OutOfMemoryError: Java heap space)
public class Code_10_GCTest {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
}
}
提前设置vm参数后,可以执行代码查看新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况。
当一个线程抛出OOM异常(OutOfMemoryError: Java heap space)后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行。
-XX:+UseSerialGC=serial + serialOld
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=rario 直接设置吞吐量的大小
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)
单位时间内垃圾回收所花费时间对于吞吐量,垃圾回收的速度对应响应时间
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能仍然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
定义: Garbage First
JDK9默认的垃圾回收器
适用场景:
相关参数:
JDK8 并不是默认开启的,所需要参数开启
-XX:+UseG1GC 启动开关
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段
新生代垃圾回收 -> 新生代垃圾回收+并发标记(老年代达到阈值) -> 混合收集(对伊甸园,幸存区,老年代都进行规模较大的垃圾回收) -> 新生代垃圾回收(又重新开始)
2)Young Collection
E:伊甸园
伊甸园回收时会STW
S:幸存区
伊甸园清理后内存依然紧张就会将数据复制到幸存区。
O: 老年代
又经过一段时间幸存区内存也不足,并且幸存年龄达到一定值后就会执行垃圾回收,并将其放进老年代。其中伊甸园中的数据和幸存区不够年龄的依然会复制到幸存区,增加幸存年龄。
3)Young Collection + CM
4)Mixed Collection
会对伊甸园(E),幸存区(S),老年代(O)进行全面的垃圾回收。
老年代的垃圾回收是比较浪费时间的,所以并不会对全部的老年代进行垃圾回收,它会根据设置的MaxGCPauseMills停顿时间对一部分(快被占满的区域)老年代回收。
老年代回收时也会做复制操作,不被回收的被复制到一个老年代区域
5)Full GC
垃圾回收器 | 新生代内存不足发生垃圾回收 | 老年代内存不足发生垃圾回收 |
Parallel GC | minor gc | full gc |
Serial GC | minor gc | full gc |
CMS GC | minor gc | 1.如果并发垃圾收集速度快于垃圾产生速度则是并发垃圾收集 2.如果并发垃圾收集速度慢于垃圾产生速度则是full gc |
G1 | minor gc |
6)Young Collection 跨代引用
有一部分Root对象放在老年代,但是老年代数据比较多,遍历一次会耗费大量时间。
所以就采用对老年代的区域细分,分成一个个card(512B),如果card中有对象引用了新生代的对象,就会标记为脏card。这样下次GC Root遍历时就不会遍历所有的老年代,只关注脏card。
新生代被引用的对象会在Remembered Set中记录被引用的信息(脏card),在垃圾回收时会先读取Remembered Set中的记录,找到对应的GC Root,遍历找到存活对象,清理其它对象。
每次引用变更时会对脏card进行更新,这个过程是一个异步操作,不会立刻更新,会先将更新指令放进脏card队列中,然后会有一个线程进行更新操作。
7)Remark(重新标记阶段)
并发标记过程
问题:C对象先是被B对象引用,但是在并发标记过程中C对象断开了对B对象的引用,这时B对象处理完成,C对象就会呈现被处理的状态,但是在此过程中有用户线程对C对象操作,引用了A对象。这时C就不应该被处理。
解决:并发标记期间对象引用关系发生变化就加入队列,在重新标记时再次处理,重新标记时触发STW,对象的引用关系不会再改变
过程:
8)JDK 8u20 字符串去重
例:
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}
创建两个String对象,其底部是创建了两个相同的char数组。
去重过程:
注意:其与 String.intern() 的区别
优点与缺点
JVM参数(默认开启)
-XX:+UseStringDeduplication
9)JDK 8u40 并发标记类卸载
JVM 参数默认启用
在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类
10)JDK 8u60 回收巨型对象
上面H在不被老年代所引用时,就会在下一次新生代垃圾回收时被处理掉。
11)JDK 9 并发标记起始时间的调整
预备知识:
查看虚拟机参数命令:
D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
JVM调优优先考虑新生代调优。(老年代垃圾清理在新生代之后)
新生代特点:
新生代调优:是不是新生代内存越大越好?(建议:堆内存的1/4~1/2)
结论:新生代内存设置空间还是尽可能的大。(容纳所有【并发量*(请求--响应)】的数据)
幸存区:大到能保留【当前活跃对象+需要晋升对象】
问题:新生代晋升老年代时,让长时间存活对象尽快晋升,但是又不想让存活时间较短的对象晋升到老年代。所以需要合理的配置晋升阈值。
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
以CMS为例
-XX:CMSInitiatingOccupancyFraction=percent
案例1:Full GC和Minor GC频繁
案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS)
案例3:老年代充裕情况下,发生Full GC(CMS jdk1.7)
跳转:类文件结构附件
Oracle 提供了 javap 工具来反编译 class 文件
[root@localhost ~]# javap -v 类名.class
1)java代码
package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2)编译后的字节码文件
[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 //
java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public cn.itcast.jvm.t3.bytecode.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field
java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method
java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
MethodParameters:
Name Flags
args
}
3)常量池载入运行时常量池
java代码被执行时,是由类加载器将main方法所在的类的class字节数据加载到内存中。
常量池中的数据会放到运行时常量池中。
注意:运行时常量池是方法区的一部分
比较小的数字(小于short的数字)并不会存放在常量池中,他们会和字节码指令存放在一起
4)方法字节码载入方法区
方法的字节码会放在方法区
5)main线程开始运行,分配栈帧内存
main方法运行之前会先分配栈帧内存
栈帧:局部变量表、操作数栈、动态链接、方法出口
(stack=2,locals=4):局部变量表是4,操作数栈是2
执行引擎开始执行字节码
bipush 10
istore 1
ldc #3
istore 2
将操作数栈中的元素弹出,放到局部变量表的 2 号位置
iload1 iload2
将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中。
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置。
getstatic #4
在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中
iload 3
将局部变量表中 3 号位置的元素压入操作数栈中。
invokevirtual #5
return
完成 main 方法调用,弹出 main 栈帧,程序结束
从字节码角度分析a++相关题目
package cn.itcast.jvm.t3.bytecode;
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
分析:
步骤分析:
图解:
练习2
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
分析:
Code:
stack=2, locals=3, args_size=1 // 操作数栈分配2个空间,局部变量表分配 3 个空间
0: iconst_0 // 准备一个常数 0
1: istore_1 // 将常数 0 放入局部变量表的 1 号槽位 i = 0
2: iconst_0 // 准备一个常数 0
3: istore_2 // 将常数 0 放入局部变量的 2 号槽位 x = 0
4: iload_1 // 将局部变量表 1 号槽位的数放入操作数栈中
5: bipush 10 // 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
7: if_icmpge 21 // 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
10: iload_2 // 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0
11: iinc 2, 1 // 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值为 1
14: istore_2 //将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了0
15: iinc 1, 1 // 1 号槽位的值自增 1
18: goto 4 // 跳转到第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
JVM层面:
代码层面:
cinit()V:类的初始化
public class Code_12_CinitTest {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i); // 30
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
init()V:对象初始化
public class Code_13_InitTest {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Code_13_InitTest(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Code_13_InitTest d = new Code_13_InitTest("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后.
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()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
代码层面讲解:类加载
public class Code_14_MethodTest {
public Code_14_MethodTest() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Code_14_MethodTest obj = new Code_14_MethodTest();
obj.test1();
obj.test2();
obj.test3();
Code_14_MethodTest.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
Code:
stack=2, locals=2, args_size=1
0: new #2 //
3: dup // 复制一份对象地址压入操作数栈中,相当于两个,其中一个在invokespecial之后就会删除处理
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
静态方法被对象调用时候同样会执行类调用的字节码指令,所以建议直接使用类调用。
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
多态的某个方法是子类方法还是父类方法,是直接体现在vtable中的,vtable会根据具体地址直接直接指向对性的字节码文件,也就直接找到了所在的对象。
try-catch
public class Code_15_TryCatchTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应的字节码指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
finally
public class Code_17_FinallyTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应字节码
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
// try块
2: bipush 10
4: istore_1
// try块执行完后,会执行finally
5: bipush 30
7: istore_1
8: goto 27
// catch块
11: astore_2 // 异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
// catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
// 出现异常,但未被 Exception 捕获,会抛出其他异常,这时也需要执行 finally 块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow // 抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
finally中的return
public class Code_18_FinallyReturnTest {
public static void main(String[] args) {
int i = Code_18_FinallyReturnTest.test();
// 结果为 20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 // 暂存返回值
5: bipush 20
7: istore_0
8: iload_0
9: ireturn // ireturn 会返回操作数栈顶的整型值 20
// 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
10: astore_2
11: bipush 20
13: istore_0
14: iload_0
15: ireturn // 这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常!
Exception table:
from to target type
0 5 10 any
被吞掉的异常
public static int test() {
int i;
try {
i = 10;
// 这里应该会抛出异常
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
会发现打印结果为 20 ,并未抛出异常
finally 不带 return
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 // 赋值给i 10
3: iload_0 // 加载到操作数栈顶
4: istore_1 // 加载到局部变量表的1号位置
5: bipush 20
7: istore_0 // 赋值给i 20
8: iload_1 // 加载局部变量表1号位置的数10到操作数栈
9: ireturn // 返回操作数栈顶元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 // 加载异常
15: athrow // 抛出异常
Exception table:
from to target type
3 5 10 any
返回的是10
public class Code_19_SyncTest {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
对应字节码
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
4: invokespecial #1 // Method java/lang/Object."":()V
7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
8: aload_1 // 加载到操作数栈
9: dup // 复制一份,放到操作数栈,用于加锁时消耗
10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
11: monitorenter // 加锁
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 // 加载对象到栈顶
21: monitorexit // 释放锁
22: goto 30
// 异常情况的解决方案 释放锁!
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
// 异常表!
Exception table:
from to target type
12 22 25 any
25 28 25 any
待学习。。。。。。
4.2.1 验证
验证类是否复核jvm规范,安全性检查
4.2.2 准备
为static分配空间,设默认值
4.2.3 解析
将常量池中的符号引用解析为直接引用
public class Demo1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ClassLoader loader = Demo1.class.getClassLoader();
//只加载不解析
Class> c = loader.loadClass("com.nyima.JVM.day8.C");
//用于阻塞主线程
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
loader不会触发解析,也不会触发初始化。
使用HSDB查看
查看类C的常量池,可以看到类D未被解析,只是存在于常量池中的符号
附:HSDB的使用
jps
Copy
java -cp F:\JAVA\JDK8.0\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
Copy
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的构造方法的线程安全
注意
编译器手机的顺序是由语句在源文件中出现的顺序决定的,惊天语句块中只能访问到定义的静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但不能访问
类的初始化是懒惰的,以下情况不会初始化
以下情况会初始化
验证类是否被初始化,可以看该类的静态代码块是否被执行
java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”的动作放到了java虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器
5.1 类与类加载器
类加载器虽然只用于实现类的加载过程,但是它在java程序中起到的作用却远超类加载阶段
对于任意一个类,都必须由加载他的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。通俗讲就是:比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义。否则,即使两个类来源于同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类也必定不相等。
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(拓展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
启动类加载器
可以通过在控制台输入指令,使得类被启动类加载器加载
拓展类加载器
如果classpath和JAVA_HOME/jre/lib/ext 下有同类名,加载时会使用拓展类加载器加载。当应用程序类加载器会发现拓展类加载已将该同名类加载过了,则不会再次加载。
双亲委派模式,即调用类加载器ClassLoader的loadClass方法时,查找类的规则
loadClass源码:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先查找该类是否已经被该类加载器加载过了
Class> c = findLoadedClass(name);
//如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//看是否被启动类加载器加载过
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//捕获异常,但不做任何处理
}
if (c == null) {
//如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,如果还是没找到,就抛出异常
//然后让应用类加载器去找classpath下找该类
long t1 = System.nanoTime();
c = findClass(name);
// 记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
步骤:
使用场景:
步骤:
class MyClassLoader extends ClassLoader {
@Override
protected Class> findClass(String name) throw ClassNotFoundException {
String path = "e:\\myclasspath\\"+name+".class";
try{
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Path.get(path), os);
//得到字节组
byte[] bytes = os.toByteArray();
//byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e){
e.printStackTrace();
throw new ClassNotFoundException("类文件没有找到",e);
}
}
}
public class Test {
pubulic static void main(String[] args) throws Exception {
MyClassloader classLoader = new MyClassLoader();
Class> c1 = classLoader.loadClass("test1");
Class> c2 = classLoader.loadClass("test1");
System.out.println(c1==c2);
MyClassloader classLoader2 = new MyClassLoader();
Class> c3 = classLoader2.loadClass("test1");
System.out.println(c1==c3);
}
}
ture
false
JVM将执行状态分为5个层次
普通代码的执行都是0层执行,但是部分代码被多次调用之后会自动上升为1层。
c2和c1比较,c2即时编译器优化更彻底。
profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
即时编译器和解释器的区别
解释器:
即时编译器:
对于大部分的不常用的代码,通常是直接采用解释执行的方式运行;对于仅占据小部分的热点代码,我们则可以将其编译成成机器码,以达到理想的运行速度。执行效率上简单比较是 interpreter 逃逸分析 逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术 逃逸分析的 JVM 参数如下: JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。 第二个原因则更重要:方法内联 如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如: java内存模型和java内存结构是不同的,java内存模型是定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、原子性的规则和保障 7.1.1 问题解析 提出问题:两个线程对初始值是0的静态变量一个做自增,一个做自减,各5000次,结果是0吗? 答案是不一定,结果可能是正数 、零 、负数。因为java中对静态变量的自增,自减并不是原子操作。 其中 i++产生的JVM字节码指令: i--产生的字节码指令: 而java的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换。 所以在多线程下就会出现字节码交错执行 正常执行顺序: 出现负数情况: 出现正数: 7.1.2 解决办法 synchronized(同步关键字) 语法: 解决问题: 从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的。 monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。 7.2.1 退不出的循环 当主线程对静态变量run修改后,t 线程也没有停止 原因: 增加易变关键字 volatile [ˈvɑːlətl] volatile可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量值,必须到主内存中获取它的值,线程操作volatile修饰的变量都是直接从主内存中获取值的,保证了共享变量的可见性,但是并不能保证原子性。 注意: 是因为println方法 可以看出加了 7.3.1 问题 先看一个例子: 其实这里会有三种情况出现: 其实还有可能出现为0的情况! 这种现象就是指令重排。(出现概率小)。 7.3.2 解决办法 volitile修饰的变量,可以禁用指令重排 7.3.3 有序性的理解 同一线程内,jvm会在不影响正确性的前提下,可以调整语句的执行顺序,例如: 可以看到先执行i,还是先执行j对结果没有影响,所以,上面代码真正执行时可以是 也可以是 这种特性称为指令重排,多线程下指令重排会影响正确性,例如著名的 可能会发生指令重排 以上的实现特点是: 上面的代码看似已经很完美了,但是在多线程环境下还是会有指令重排问题! 其中4 7 两步顺序不是固定的,也许 jvm 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行构造方法,如果两个线程 t1,t2 按如下时间顺序执行: 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将 是一个未初始化完毕的单例 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才 会真正有效 happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见 具有传递性,如果 以上变量都是指共享变量即成员变量或静态资源变量 CAS即compare and swap,它体现的是一种乐观锁的思想 比如多个线程要对一个共享的整型变量执行+1操作: 获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈。多核cpu的场景下。 CSA底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,下面是直接使用Unsafe对象进行线程安全保护的一个例子: 8.3 原子操作类 juc(java.util.concurrent)中提供了原子操作类,可以供线程安全的操作,例如:AtomicIteger、AtomicBoolean等,两个类的底层都是采用了CAS技术和volitile来实现的。可以使用Atomiclnteger改写之前的例子: java HotStop虚拟机中,每个对象都有对象头(包括class指针和mark word)。mark word 平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就会根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容 如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么可以使用轻量级锁来优化。这就好比: 学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明有竞争,继续上他的课。 如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程A 随即升级为重量级锁,进入重量级锁的流程。 而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来,假设有两个方法同步块,利用同一个对象加锁 每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 如果尝试加轻量级锁的过程中,CAS操作无法成功,这种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候锁线程已经退出了同步块,释放了锁),这时线程就可以避免阻塞。 在java6之后自旋锁是适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性高,就会自旋多次;反之,就少自旋甚至不自旋,总之,比较智能。 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS (1)减少上锁时间 同步代码块中尽量短 (2)减少锁的粒度 将一个锁拆成多个锁提高并发度,例如: (3)锁粗化 多次循环进入同步块不如同步块内多次循环,另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次) (4)锁消除 JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。 (5)读写分离 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的 参考:【JVM】JMM内存模型_ΘLLΘ的博客-CSDN博客
6.2 方法内联
private int add4(int x1, int x2, int x3, int x4) {
//这里调用了add2方法
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
private int add4(int x1, int x2, int x3, int x4) {
//被替换为了方法本身
return x1 + x2 + x3 + x4;
}
6.3 反射优化
7. java 内存模型 (JMM)
7.1 原子性
public class Demo1 {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic i // 线程1-获取静态变量i的值 线程内i=1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减 线程内i=0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=0
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
synchronized( 对象 ) {
要作为原子操作代码
}
public class Demo1 {
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);//输出为0
}
}
7.2 可见性
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不会如预想的停下来
}
7.2.2 解决办法
public class Demo1 {
volatile 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不会如预想的停下来
}
}
synchronized
语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但 缺点是synchronized
是属于重量级操作,性能相对更低System.out.println()
会发现即使不加 volatile 修饰符,线程 t 也 能正确知道 run 变量的修改了public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
synchronized
,保证了每次run
变量都会从主存中获取7.3 有序性
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;//可以禁用指令重排
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;
i = ...; // 较为耗时的操作
j = ...;
j = ...;
i = ...; // 较为耗时的操作
double-checked locking
模式实现单例public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
//实例没创建,才会进入内部的 synchronized 代码块
if (INSTANCE == null) {
//可能第一个线程在synchronized 代码块还没创建完对象时,第二个线程已经到了这一步,所以里面还需要加上判断
synchronized (Singleton.class) {
//也许有其他线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
INSTANCE = new Singleton()
对应的字节码为:0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "
时间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 处)
7.4 happens-before
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start()
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
t1.isAlive()
或 t1.join()
等待它结束)static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
t2.interrupted
或 t2.isInterrupted
)static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);//0
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);//0
}
x hb-> y
并且 y hb-> z
那么有 x hb-> z
8. CAS与原子类
8.1 CAS
// 需要不断尝试
while(true) {
int 旧值 = 共享变量 ; // 比如拿到了当前值 0
int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
/*
这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
compareAndSwap 返回 false,重新尝试,直到:
compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
*/
if( compareAndSwap ( 旧值, 结果 )) {
// 成功,退出循环
}
//不一样,继续循环尝试
}
public class TestCAS {
public static void main(String[] args) throws InterruptedException {
DataContainer dc = new DataContainer();
int count = 5;
Thread t = new Thread(() -> {
for (int i = 0; i < count; i++) {
dc.increase();
}
});
t.start();
t.join();
System.out.println(dc.getData());
}
}
class DataContainer {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
try {
// Unsafe 对象不能直接调用,只能通过反射获得
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET =
unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public void increase() {
int oldValue;
while (true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue +
1)) {
return;
}
}
}
public void decrease() {
int oldValue;
while (true) {
oldValue = data;
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue -
1)) {
return;
}
}
}
public int getData() {
return data;
}
}
8.2 乐观锁和悲观锁
public class TestCAS {
//创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); //获取并且自增 i++
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndDecrement(); //获取并且自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);//0
}
}
9. synchronized 优化
9.1 轻量级锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
Mark Word
9.2锁膨胀
9.3重量锁
9.4偏向锁
:-UseBiasedLocking
禁用偏向锁9.5 其他优化
new StringBuffer().append("a").append("b").append("c");
-XX:-UseBiasedLocking
禁用偏向锁