01_JVM与Java体系结构
本次内容主要参考
尚硅谷jvm 近68小时 381集
https://www.bilibili.com/video/BV1PJ411n7xZ?from=search&seid=17328646192254483360
多线程 近64小时 174集
https://www.bilibili.com/video/BV1hJ411D7k2?from=search&seid=3333051564860741228
及其他看过的一些jvm、多线程书籍
jvm参数查询
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
安装jclasslib插件
分享规划,主要是想分享一下jvm调优方面的,但是由于调优没有固定套路,需要实际情况实际分析,所以调优之前需要了解jvm的特性,结合自己项目情况进行调整,因此本次分享主要分jvm介绍和调优两个部分
1.由于我个人觉得我技术水平挺菜的
2.此次分享内容中加入了大量我个人的主观理解
因此本次分享采用互动的形式,如果大家觉得我说的不对欢迎及时打断,指正错误
通过jdk9默认g1,已经说明了g1的强大
jdk11引入的号称革命性的gc zgc,在项目还未使用jdk11前,不着急了解zgc,其次是jdk11引入zgc如果指定gc为 zgc 有提示 experimental(实验性的),在jdk15上才是(production-ready 可用于生产)
jdk版本选择 lts 如果要使用zgc,个人建议zgc在jdk17中使用
- 它采用解释器与即时编译器并存的架构
- java属于半编译半解释型语言
为啥两者并存,后面将介绍JIT的时候详细讨论
jvm执行流程
java编译器(将java源代码编译成字节码) -> 前段编译器
JIT编译器(字节码编译成机器可以直接执行的二进制)-> 后端编译器
这里先简单介绍一下jit,通过热点代码分析技术,将热点代码有字节码编译成机器可以直接执行的二进制,以加快执行
看过一篇阿里的文章 ,同配置的主备服务,进行切换时,假如主服务能承受的qps 1000,切换时要先将备服务预热才能切换,由于jit存在导致刚启动的服务无法达到1000 qps
03_运行时数据区概述及线程
并发程序开发,可以选择,进程>线程>协程(对单个线程复用,个人感觉实现类似于nio)
java中new Thread 对应的是操作系统的一个线程(一一对应),这就导致java中的锁设计分了以下维度
java中的锁实现有一种分类维度:
1.jvm层实现的,一般为乐观锁(轻量级) synchronized(锁升级尚未升级到重量级锁前) AtomicInteger(cas)
2.操作系统层面实现的,一般为悲观锁(重量级) 例如 ReentrantLock
04_程序计数器
细节:线程的程序计数器指向的位置(指向当前线程的栈帧中的操作数栈顶部)
我个人的理解:程序计数器相当于一个游标,指向当前线程正在执行的代码,我理解它的作用是在多线程场景下,当前线程释放cpu执行权时,记录当前的执行位置,方便下次获得cpu执行权时继续在此位置执行,类似于书签的作用
同理还有ThreadLocal为什么设定为线程私有(为了将数据和当前线程绑定)
05_虚拟机栈
设置栈内存大小
-Xss 设置最大栈空间
这里有个疑问?根据翻译
设置线程栈大小,到底指的是每个线程可用的栈大小,还是整个栈空间的大小
这个地方我也是弄的不太清楚,有没有懂这一块的给解答一下?
问什么在这个问题上较真,主要是涉及到jvm调优,如果对栈调整,有两个入口
- -Xss
- 创建线程的时候指定栈大小
个人验证:
/**
* new 一个线程
* -Xss1m
* 16495,16907,16595,16966,16886
*
* new 一个线程
* -Xss10m
* 608881,588071,185842,605181,605641
*
* 结论:增大 -Xss 可用增加递归深度
*/
public class TestStack {
private static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String next = scanner.next();
TestStack testStack = new TestStack();
Thread thread = new Thread("t1"){
@Override
public void run() {
try {
testStack.add();
} finally {
System.out.println(count.get());
}
}
};
thread.start();
}
public void add() {
count.incrementAndGet();
add();
}
}
我这里提一个问题,这个代码有没有问题,main方法执行完thread.start()结束,虚拟机会不会退出,导致Thread(“t1”)无法继续执行?
补充
第一种:程序执行完,没有可执行的代码了
第二种:如果出现异常未被处理
还有一种不存在非守护线程了
这里Thread(“t1”)是非守护线程,由于它的存在不会导致jvm退出
我个人建议设置成守护线程
疑问:线程a创建的守护线程b,如果a结束,那么b还存在吗?
package test2;
import java.util.concurrent.TimeUnit;
/**
* @author zhaoxingbang
* @date 2021/5/31
*/
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("main线程的守护线程正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.setDaemon(true);
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("非守护线程正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setDaemon(false);
t2.start();
TimeUnit.SECONDS.sleep(3);
}
}
通过arthas,main线程已经退出,但是他创建的守护线程还在运行
一个jvm运行实例对应一个RunTime实例,可以做一下jvm层面上的功能
添加关闭钩子,用于释放资源,his集群中分布式锁基于数据库实现,关闭前释放资源,释放锁,Tomcat stop.sh 等
调用gc
查看jvm内存相关使用情况
java调用其他语言(如python爬虫模块)
/**
* @author zhaoxingbang
* @date 2021/5/26
*
* -Xss1m 16683:18031 ,16790:20163, 21297:23541 , 18543:25401, 26129:16810
* 结论:增加线程数量 总递归深度并未变化,所以-Xss 是设置单个线程的栈空间
*/
public class TestStack2 {
private static AtomicInteger count1 = new AtomicInteger();
private static AtomicInteger count2 = new AtomicInteger();
private static CyclicBarrier barrier = new CyclicBarrier(2);
public static void main(String[] args) {
TestStack2 testStack1 = new TestStack2();
TestStack2 testStack2 = new TestStack2();
Thread thread1 = new Thread("t1"){
@Override
public void run() {
try {
barrier.await();
testStack1.add1();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(count1.get());
}
}
};
Thread thread2 = new Thread("t2"){
@Override
public void run() {
try {
barrier.await();
testStack2.add2();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(count2.get());
}
}
};
thread1.start();
thread2.start();
}
public void add1() {
count1.incrementAndGet();
add1();
}
public void add2() {
count2.incrementAndGet();
add2();
}
}
你以为这就完了吗,没完!
public class TestStack {
private static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
TestStack testStack = new TestStack();
Thread thread = new Thread(null,() -> {
try {
testStack.add();
} finally {
System.out.println(count.get());
}
}, "t1", 1111111111);
thread.start();
}
public void add() {
count.incrementAndGet();
add();
}
}
结论还可以通过Thread构造方法设置栈大小,对单个线程进行设置,根据注释此参数高度依赖于操作系统,可能失效(优先于-Xss)
验证
编译不通过。。。。。
去年考中移成研院机试遇到的类似题,我当时想的是哪个傻逼面试官出的这种问题,后面才发现原来小丑使我自己。。
这里提前提一下垃圾回收算法
1.垃圾判断算法:可达性分析(java,根据gcRoot判定的),引用计数算法(python)
2.垃圾回收算法:标记-清除算法(Mark-Sweep),复制算法(Copying),标记-整理算法(Mark-Compact)
以及:分区算法(g1及以后的新垃圾回收器使用),分代收集算法(Generational Collection),注意两者完全不一样,可以类比数据库分库中:水平分库、垂直分库都是分库,但是两者不一样
其中上面是经典的垃圾回收算法,下面两个后面被提出的,类似设计模式23种经典模式和其他(仓壁模式断路器)
这里涉及到调优如果能确定变量的声明周期,尽量把变量的生命周期缩小(局部变量,成员变量,线程变量ThreadLocal,静态变量),减少gc负担(标记阶段安全点等待,逃逸分析)
这里说一下 字节码指令 :操作符 + 操作数
例如 bipush + 15
istore_1 等价于 istore + 1 (对其封装)
同理,类似于Integer
接下来我化身执行引擎的解释器来带大家执行一下上面代码
字节跳动面试题 i++ 和 ++i的区别
类似可以想到 jmm java内存模型(由于内存和cpu缓存速度差很大,将内存中变量缓存到寄存器上,提高性能) 引发的问题,线程读到的是寄存器中的数据 和 内存中的不一致, volatile关键字,保证多线程环境下可见性(及时刷回内存)
volatile 可见性(实现:缓存一致性协议),有序性
我个人理解:
class编译后的字节码,可以看到上面有常量池
整个class在jvm运行被加载到方法区(又叫元空间,jdk7中叫永久代)
0: ldc #5 // String zxb
2: astore_1
3: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
上图是javap 反编译生成的字节码
ldc #5
表示 加载 #5
其中 #5 就是符号引用
但运行时要替换成直接引用,这就是动态链接的作用
这里说一下,我面试被问到过一个问题:谈谈你对多态的理解?
关于优化方面
静态连接性能好于动态链接(因为在动态链接时,执行引擎在执行方法时需要查找,先找当前类有没有方法的实现,没有在去父类找,直到找到为止),如果只考虑性能方面不建议使用多态,实际情况不现实
final 关键字加在方法上可以在实现静态连接提高性能,缺点不可重写了
方法重写的本质(多态)
虚方法表就是解决多态性能问题的
public class Test2 {
public static void main(String[] args) {
Test2 test2 = new Test2();
Thread t1 = new Thread("t1"){
@Override
public void run() {
test2.a();
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
test2.a();
}
};
t1.start();
t2.start();
}
public void a() {
System.out.println("a");
b();
}
public void b() {
System.out.println("b");
c();
}
public void c() {
System.out.println("c");
}
}
06_本地方法接口
jni,osgi,和操作系统打交道或调用c/c++,开发驱动
现在用的不多,rest,socket,webService,读写共享文件方式
07_本地方法栈
08_堆
所有线程共享堆
例外的情况是 对象栈上分配,栈上分配的前提是逃逸分析,对象不发生逃逸
jdk 7 堆结构
8之前 和 8区别,主要区别metaspace用的是系统的直接内存
oracle 推荐 -Xms 和 -Xmx两者相等
演示oom及对象分配、gc过程
package test5;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* -Xms600m -Xmx600m
* 演示工具 jvisualVm + visual gc插件
* @author zhaoxingbang
* @date 2021/7/5
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) throws InterruptedException, IOException {
List<HeapInstanceTest> list = new LinkedList<>();
System.in.read();
while (true) {
list.add(new HeapInstanceTest());
TimeUnit.MILLISECONDS.sleep(10);
}
}
}
除了-XX:TLABWasteTargetPercent=1 指定百分比
-XX:TLABSize=0 可以指定大小
’
synchronized 锁升级过程,第一个阶段无锁
标量替换
证明
package test5;
import java.util.concurrent.TimeUnit;
/**
*
* -Xmx200m -Xms200m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
* -Xmx200m -Xms200m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
*
*
* @author zhaoxingbang
* @date 2021/7/5
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
try {
TimeUnit.SECONDS.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void alloc() {
User user = new User();
user.id = 5;
user.name = "aaa";
}
static class User {
public int id;
public String name;
}
}
-
-Xmx200m -Xms200m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
第一次关闭逃逸分析
耗时:56ms
发生了4次young gc -
-Xmx200m -Xms200m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
消耗:3ms
未发生young gc
这里矛盾了。。。。。。下面是依据
09_方法区
避免full gc 这里建议要根据自己项目实际class的加载情况考虑一下,要不要调整 两个参数
方法区溢出
oom解决流程
方法区存什么?
为什么要常量池?
.
静态变量放在哪?
package jvm;
import java.util.concurrent.TimeUnit;
/**
* 结论:静态引用对应对象实体始终都在堆空间
* -Xms200m -Xmx300m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
* @author zhaoxingbang
* @date 2021/7/8
*/
public class StaticFieldTest {
/**
* 100m
*/
private static byte[] arr = new byte[1024 * 1024 *100];
public static void main(String[] args) throws InterruptedException {
System.out.println(StaticFieldTest.arr);
TimeUnit.HOURS.sleep(1);
}
}
结论:静态引用对应对象实体始终都在堆空间
staticObj:存放在堆中
instanceObj:随着test对象实例存放在堆中
localObj:存放在栈帧局部变量表中
所以静态变量在java8中存放在堆中
10_对象的实例化内存布局与访问定位
jvm选择第二种
11_直接内存
12_执行引擎
回边计数器作用
加快包含循环方法的编译触发
这里发现mix方式吞吐量最高,我又查了一下jvm参数
-Xcomp 说是首次执行的时候进行编译,我在想是不是首次执行的时候拖慢了速度于是,我在压测前先用postman主动调用一下,但是由于dispatcherServlet初始化很耗时,公平起见重新测试一遍
-Xint
-Xcomp
-Xmixed
jdk为64位时自动选择 server模式
13_StringTable
0.字符串常量池位置
字符串常量池 StringTable在堆中
1.字符串特性
不可变性
2.字符串常量池介绍
在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
string有两种创建方式:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
intern方法介绍
String#intern方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。
它的大体实现结构就是: JAVA 使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。
jdk7开始,默认值为60013
-XX:StringTableSize=8888
3.String的内存分配
- 证明字符串在常量池中是唯一的
package jvm;
/**
* -Xms10m -Xmx10m
* @author zhaoxingbang
* @date 2021/7/13
*/
public class StringTest2 {
public static void main(String[] args) {
char [] a = {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'};
String s3 = new String(a);
String s4 = new String(a);
String s1 = "aaaaaaaa";
String s2 = "aaaaaaaa";
s3.intern();
System.out.println();
}
}
4.字符串拼接
package jvm;
import org.junit.Test;
/**
* @author zhaoxingbang
* @date 2021/7/13
*/
public class StringTest3 {
@Test
public void test1() {
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2);
}
@Test
public void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
String s8 = s6.intern();
System.out.println(s3 == s8);
}
}
5.使用intern优化代码
package jvm;
import java.util.concurrent.TimeUnit;
/**
* @author zhaoxingbang
* @date 2021/7/14
*/
public class StringIntern {
static final int MAX_COUNT = 1000 * 10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) throws InterruptedException {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
long start = System.currentTimeMillis();
for (int i = 0; i < MAX_COUNT; i++) {
// arr[i] = String.valueOf(data[i % data.length]); // 6729
arr[i] = String.valueOf(data[i % data.length]).intern(); // 1068
}
long end = System.currentTimeMillis();
System.out.println("话费的时间为: " + (end - start));
TimeUnit.HOURS.sleep(1);
}
}
6.StringTable垃圾回收测试
package jvm;
/**
* @author zhaoxingbang
* @date 2021/7/15
*
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
*/
public class StringGCTest {
public static void main(String[] args) throws InterruptedException {
// int count = 0;
// int count = 100;
// int count = 10000;
int count = 1000000;
for (int i = 0; i < count; i++) {
String.valueOf(i).intern();
}
}
}
14_垃圾回收概述
oracle 关于gc的介绍
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
gc区域
15_垃圾回收相关算法
标记算法
判断对象存活一般有两种方式:引用计数算法,可达性分析算法
1.mat gc root溯源
package jvm;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* @author zhaoxingbang
* @date 2021/7/31
*/
public class GCRootsTest {
public static void main(String[] args) throws InterruptedException {
List<Object> numList = new ArrayList<>();
Date birth = new Date();
for (int i = 0; i < 100; i++) {
numList.add(String.valueOf(i));
}
System.out.println("数据加载完毕,请操作:");
new Scanner(System.in).next();
numList = null;
birth = null;
System.out.println("numList birth 已置空,请操作:");
new Scanner(System.in).next();
System.out.println("结束");
}
}
2.JProfiler gc root溯源
1.介绍
2.图示
3.优缺点
1.介绍
2.图示
3.优缺点
1.介绍
2.图示
3.优缺点