JVM性能优化简介
01. JVM是什么
概述:
大白话:
全称Java Virtual Machine(Java虚拟机), 它是一个虚构出来的计算机, 通过实际的计算机来模拟各种计算机的功能.
专业版:
JVM是一个进程, 用来模拟计算单元, 将.class字节码文件转成计算机能够识别的指令.
//这里可以联想以前大家学的"VM ware", 它也是一个虚拟机.
//它们的区别就在于: VM Ware是你能看见的, JVM是你看不见的.
回顾:
我们以前写的Java程序是: 编写 --> 编译 --> 运行三个阶段的.
.class文件是Java语言独有的, 只有JVM能识别, 其他任何软件都识别不了.
所以Java语言的"跨平台性(一次编译到处运行)"就是由JVM来保证的.
画图演示:
JVM把.class字节码文件 转成 计算机能够识别的指令的过程.
代码演示:
D:\compile\Worker.java文件, 通过"jps"命令查看启动的进程.
02. JVM虚拟机运行的流程
JVM是一个进程,接下来我们来研究它的: 工作机制, 这个问题是很深奥的, 不亚于研究一个完整VM Ware虚拟机,但是诸如"硬盘, CD/DVD这些部分和我们都没关系", 所以研究JVM的工作机制就是在研究它的: 运算机制.
首先, 请你思考一个问题: 如果我给你一个A.class字节码文件, 想把它运行起来, 你会做哪些事情?
画图演示:
1. 读取字节码文件所在的路径.
//类加载机制
2. 获取字节码文件中具体的内容.
//方法区: 用来存放类的描述信息.
3. 获取该类的实例(对象)
//堆(Heap): 用来存储对象的(所有new出来的内容)
4. 通过对象名.的方式调用方法.
//栈(Stack): 用来存放局部变量及所有代码执行的.
今天我们的学习顺序, 就是按照这个流程来走的.
03. JVM虚拟机类加载机制(一):运行顺序
首先, 我们先来研究JVM的类加载机制, 类加载机制就是把类给读取出来, 我们来看一下它是如何运行的.
画图演示:
JVM底层加载类依靠三大组件:
BootStrapClassLoader //启动类加载器
//负责加载: jre\lib\rt.jar //rt: runtime, 运行的意思
//windows最早不支持java, 没有JRE, 后来Sun公司打官司赢了, windows开始默认支持JRE.
ExtClassLoader: //扩展类加载器
//负责加载: jre\lib\ext\* 文件夹下所有的jar包
//这两个加载器执行完毕后, JVM虚拟机基本上就初始化完毕了.
APPClassLoader: //应用程序类加载器
//负责加载: 用户自定义的类的.
//就是加载: 用户配置的classpath环境变量值的.
//UserClassLoader //自定义类加载器
//自定义类加载器就是自定义一个类继承ClassLoader, 然后重写findClass(), loadClass()两个方法即可.
加载顺序是: BootStrap --> ExtClassLoader --> AppClassLoader --> UserClassLoader
代码演示:
1) 随便编写一个A类, 然后演示: jar包的加载过程(rt.jar, ext\*等相关的jar包)
2) 打印类加载器对象:
//1. 获取当前线程的类加载器
ClassLoader load = Thread.currentThread().getContextClassLoader();
//2. 打印当前线程的类加载器.
System.out.println(load); //AppClassLoader
//3. 打印当前线程的类加载器的父类(加载器).
System.out.println(load.getParent()); //ExtClassLoader
//4. 打印当前线程的类加载器的父类的父类(加载器).
System.out.println(load.getParent().getParent()); //null: 其实应该是BootStrapClassLoader, 但是它是C语言写的, 所以打印不出来.
04) JVM虚拟机类加载机制(二):检查顺序
刚才我们学完了JVM类加载机制的"加载循序", 现在, 我们来研究下它的"检查顺序", 请你思考,
假设: D:\compile, ext\*.jar, rt.jar三类中都有 A.class, 那么A.class是否会被加载3次, 如果不会, 它的加载顺序是什么样的?
不会, BootStrap会加载A.class.
运行顺序是:
bootstrap --> ext --> app
1) bootstrap先加载 A.class
2) ext检查A.class是否加载:
是: 不加载A.class
否: 加载A.class
3) app检查A.class是否加载:
是: 不加载A.class
否: 加载A.class
例如:
UserClassLoader
APPClassLoader
ExtClassLoader
BootStrapClassLoader
总结:
自上而下检查, 自下而上运行.
05) JVM的内存模型(方法区, 堆区, 栈区, 程序计数器)
到目前为止我们已经知道类加载器是用来加载字节码文件的, 那加载完字节码文件之后, 是不是要运行起来啊?
那它是怎么运行的呢? 在我的课件中有一个"JVM运行时内存数据区", 接下来我们详细的来学习一下.
1) A.class字节码文件被加载到内存.
//存储在方法区中, 并且方法区中也包含常量池.
2) 创建本类的实例对象, 存储在堆中(heap)
3) 通过对象名.的形式调用方法, 方法执行过程是在: 虚拟机栈中完成的.
//一个线程对应一个虚拟机栈, 每一个方法对应一个: 虚拟机栈中的栈帧
4) 程序计数器区域记录的是当前程序的执行位置, 例如:
线程1: print(), 第3行
5) 将具体要执行的代码交给: 执行引擎来执行.
6) 执行引擎调用: 本地库接口, 本地方法库来执行具体的内容.
//这部分了解即可, 用native修饰的方法都是本地方法.
7) 本地方法栈: 顾名思义, 就是本地方法执行的区域.(C语言, 外部库运行的空间)
//了解即可.
8) 直接内存: 大白话翻译, 当JVM内存不够用的时候, 会找操作系统"借点"内存.
//了解即可.
06) JVM的一个小例子
1) 编写源代码.
//创建一个A类, 里边有个print()方法.
public class A {
public void print() {
System.out.println("h");
System.out.println("e");
System.out.println("l");
System.out.println("l");
System.out.println("o");
}
}
2) 在A类中, 编写main()函数, 创建两个线程, 分别调用A#print()方法.
/*
java A //运行Java程序
加载类:
1) bootstrap 加载rt.jar
2) ext 加载 jre\lib\ext\*.jar
3) app 加载 A.class
具体运行:
1) 主函数运行. 栈中有个主线程, 调用MainThread.main();
2) 执行第23行, A a = new A(); 将a对象存储到堆区.
3) 执行第24行, 调用a.print()方法, 生成一个栈帧, 压入主线程栈.
-----> 执行, 运行print()方法的5行代码.
4) 栈中有个新的线程, t1,
t1 --> run栈帧 --> print栈帧
5) 栈中有个新的线程, t2,
t2 --> run栈帧 --> print栈帧
*/
public class A {
public void print() {
System.out.println("h");
System.out.println("e");
System.out.println("l");
System.out.println("l");
System.out.println("o");
}
public static void main(String[] args) {
A a = new A();
a.print();
//创建两个线程对象, 调用A#print();
//线程是CPU运行的基本单位, 创建销毁由操作系统执行.
new Thread(new Runnable() {
@Override
public void run() {
a.print();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
a.print();
}
}).start();
}
}
3) 画图演示此代码的执行流程.
4) 时间够的情况下, 演示下: 守护线程和非守护线程.
07) 线程安全和内存溢出的问题
到目前为止, 大家已经知道了JVM的内存模型, 也知道了各个模块的作用,
接下来, 请你思考一个问题: 上述的模块中, 哪些模块会出现线程安全的问题,
哪些模块有内存溢出的问题?
举例:
public class A{
int i;
public void add() {
i++;
}
}
//当两个线程同时调用add()方法修改变量i的值时, 就会引发线程安全问题.
画图演示上述代码.
结论:
1) 存在线程安全问题的模块.
堆: 会. //多线程, 并发, 操作同一数据.
栈: 不会. //线程栈之间是相互独立的.
方法区: 不会. //存储常量, 类的描述信息(.class字节码文件).
程序计数器:不会.//记录程序的执行流程.
2) 存在内存溢出问题的模块.
堆: 会. //不断创建对象, 内存被撑爆.
栈: 会. //不断调用方法, 内存被撑爆.
方法区: 会. //常量过多, jar包过大, 内存被撑爆.
程序计数器: 会. //理论上来讲会, 因为线程过多, 导致计数器过多, 内存被撑爆.
其实我们研究JVM性能优化, 研究的就是这两个问题, 这两个问题也是常见面试题.
//面试题:说一下你对 线程安全和内存溢出这两个问题的看法.
总结:
研究这两个问题, 其实主要研究的还是"堆(Heap)内存".
08) JDK1.7的堆内存的垃圾回收算法
JDK1.7 将堆内存划分为3部分: 年轻代, 年老代, 持久代(就是方法区).
年轻代又分为三个区域: //使用的是 复制算法(需要有足够多的空闲空间).
Eden: 伊甸园
//存储的新生对象, 当伊甸园满的时候, 会将存活对象复制到S1区.
//并移除那些垃圾对象(空指针对象).
Survivor: 幸存者区1
//当该区域满的时候, 会将存活对象复制到S2区
//并移除那些垃圾对象.
Survivor: 幸存者区2
//当该区域满的时候, 会将存活对象复制到S1区.
//并移除那些垃圾对象.
大白话翻译:
s1区 和 s2区是来回互相复制的.
年老代: //使用的是标记清除算法, 标记整理算法.
//当对象在S1区和S2区之间来回复制15次, 才会被加载到: 年老代.
//当年轻代和年老代全部装满的时候, 就会报: 堆内存溢出.
持久代: //就是方法区
存储常量, 类的描述信息(也叫: 元数据).
09) JDK1.7默认垃圾回收器 //所谓的回收器, 就是已经存在的产品, 可以直接使用.
Serial收集器:
单线程收集器, 它使用一个CPU或者一个线程来回收对象,
它在垃圾收集的时候, 必须暂停其他工作线程, 直到垃圾回收完毕.
//类似于: 国家领导人出行(封路), 排队点餐(遇到插队现象)
//假设它在回收垃圾的时候用了3秒, 其他线程就要等3秒, 这样做效率很低.
ParNew收集器:
多线程收集器, 相当于: Serial的多线程版本.
Parallel Scavenge收集器:
是一个新生代的收集器,并且使用复制算法,而且是一个并行的多线程收集器.
其他收集器是尽量缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量:
吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
(虚拟机总共运行100分钟,垃圾收集时间为1分钟,那么吞吐量就是99%)
//因为虚拟机会根据系统运行情况进行自适应调节, 所以不需要我们设置.
CMS收集器: //主要针对于年老代.
整个过程分为:
初始标记; //用户线程等待
并发标记; //用户线程可以执行
重新标记; //用户线程等待
并发清除; //用户线程可以执行
可以理解为是:
精细化运营, 前边的垃圾收集器都是一刀切(在回收垃圾的时候, 其他线程等待), 而CMS是尽可能的降低等待时间, 并行执行程序, 提高运行效率.
以上为JDK1.7及其以前的垃圾回收器, JDK1.8的时候多了一个: G1.
G1在JDK1.9的时候, 成为了默认的垃圾回收器.
10) VM宏观结构梳理
1) Java程序的三个阶段:
编写: A.java
编译: javac A.java
运行: java A.class
2) 类加载器
bootstrap
ext
app
3) JVM的内存结构
堆:
年轻代
年老代
持久代(也就是方法区)
元数据(类的描述信息, 也就是.class字节码文件), 常量池
栈:
有n个线程栈, 每个线程栈又会有n个栈帧(一个栈帧就是一个方法)
程序计数器:
用来记录程序的执行流程的.
本地方法栈:
C语言, 外部程序运行空间.
11) G1垃圾回收器
在上个图解上做优化, 用G1新图解, 覆盖之前堆中的内容.
1) 将内存划分为同样大小的region(区域).
2) 每个region既可以是年轻代, 也可以是老年代, 还可以是幸存者区.
3) 程序运行前期, 创建大量对象的时候, 可以将每个region看做是: Eden(伊甸园).
4) 程序运行中期, 可以将eden的region变成old的region.
5) 程序运行后期, 可以缩短Eden, Survivor的区域, 变成Old区域.
//这样做的好处是: 尽可能大的利用堆内存空间.
6) H: 存储大对象的.
7) G1是JDK1.8出来的, 在JDK1.9的时候变成了: 默认垃圾处理器.
12) G1中的持久代(方法区)不见了
方法区从JVM模型中迁移出去了, 完全使用系统的内存.
方法区也改名叫: 元数据区.
13) 内存溢出的代码演示
1) 堆内存溢出演示: main.java.heap.PrintGC_demo.java
//创建对象多, 导致内存溢出.
2) 栈内存溢出演示:
main.java.stack.StackOverFlow(递归导致的)
//不设置的话在5000次左右, 设置256K后在1100次左右.
main.java.stack.Thread(不断创建线程导致的)
//这个自行演示即可, 电脑太卡, 影响上课效果.
3) 方法区内存溢出演示:
main.java.method.MethodOOM //常量过多
main.java.direct.DirectMenOOM //jar包过大, 直接溢出.
总结:
可能你未来的10年都碰不到JVM性能调优这个事儿, 先不说能不能调优, 而是大多数的
公司上来就撸代码, 很少会有"JVM调优"这个动作, 即使遇到了"JVM调优", 公司里边
还有架构师呢, 但是我们马上要找工作了, 把这些相关的题了解了解, 看看, 对面试会
比较有帮助.
//JVM调优一般是只看, 不用, 目前只是为了面试做准备.
14) 引用地址值比较
直接演示src.main.method.ATest类中的代码即可.
//讲解==比较引用类型的场景.
15) JVM调优案例赏析
百度搜索 --> JVM调优实践, 一搜一大堆的案例.
16) GC的调优工具jstat //主要针对于GC的.
1) 通过Dos命令运行 D:\compile\Worker.java
2) 重新开启一个Dos窗口:
//可以通过jps指令查看pid值.
jstat -class 2041(Java程序的PID值) //查看加载了多少个类
jstat -compiler 2041(Java程序的PID值) //查看编译的情况
jstat -gc 2041(Java程序的PID值) //查看垃圾回收的统计
jstat -gc 2041 1000 5 //1秒打印1次, 总共打印5次
17) GC的调优工具jmap //主要针对于内存使用情况的.
1) 通过Dos命令运行 D:\compile\Worker.java
2) jmap -heap 2041(Java程序的PID值) //查看内存使用情况
jmap -histo 2041 | more //查看内存中对象数量及大小
jmap -dump:format=b,file=d:/compile/dump.dat 2041 //将内存使用情况dump到文件中
jhat -port 9999 d:/compile/dump.dat //通过jhat对dump文件进行分析
//端口号可以自定义, 然后在浏览器中通过127.0.0.1:9999就可以访问了.
18) GC的调优工具jstack-死锁 //针对于线程的.
1) 线程的六种状态:
新建, 就绪, 运行(运行的时候会发生等待或者阻塞), 死亡.
2) 编写一个死锁的代码.
//两个线程, 两把锁, 一个先拿锁1, 再拿锁2, 另一个先拿锁2, 在拿锁1.
3) 通过jstack命令可以查看Java程序状态.
jstack 2041 //查看死锁状态
19) GC的可视化调优工具 //jstat, jmap, jstack
1) 本地调优.
1.1) 该工具位于 JDK安装目录/bin/jvisualvm.exe
//双击可以直接使用.
1.2) 以IntelliJ Platform为例, 演示下各个模块的作用.
1.3) 该工具涵盖了上述所有的命令.
2) 远程调优. //自行测试(目前先了解即可).
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=9999 DeadLock
这几个参数的意思是:
-Dcom.sun.management.jmxremote :允许使用JMX远程管理
-Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口
-Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用户都可以连接
-Dcom.sun.management.jmxremote.ssl=false :不使用ssl
20) JVM的总结
1) 什么是JVM?
2) JVM类加载机制.
//bootstrap, ext, app
3) JVM内存模型.
4) 垃圾回收算法.
复制算法:
针对于年轻代.
标记清除算法:
标记整理算法:
针对于老年代
5) JVM垃圾回收器.
Serial单线程.
ParNew多线程.
Parallel Scavenge: 并发多线程.
CMS: 以获取"最短垃圾回收停顿时间"为目标的收集器.
G1: JDK1.8出现的, JDK1.9被设置成默认垃圾回收器.
6) JVM调优工具:
jstat, jmap, jstack, 可视化调优工具(jvisualvm.exe).
//以下内容是为了面试用, 找工作前一周, 看看下面的题即可.
21) JVM的线程安全与锁的两种方式
线程安全:
多线程, 并发, 操作同一数据, 就有可能引发安全问题, 需要用到"同步"解决.
"同步"分类:
同步代码块:
格式:
synchronized(锁对象) {
//要加锁的代码
}
注意:
1) 同步代码块的锁对象可以是任意类型的对象.
//对象多, 类锁均可.
2) 必须使用同一把锁, 否则可能出现锁不住的情况. //String.class
同步方法:
静态同步方法:
锁对象是: 该类的字节码文件对象. //类锁
非静态同步方法:
锁对象是: this //对象锁
22) 脏读-高圆圆是男的
1) 演示main.java.thread.DirtyRead.java类的代码即可.
2) 自定义线程修改姓名后, 要休眠3秒, 而主线程休眠1秒后即调用getValue()打印姓名和年龄,
如果getValue()方法没加同步, 会出现"脏读"的情况.
23) 了解Lock锁.
1) Lock和synchronized的区别
1.1) synchronized是java内置的语言,是java的关键字
1.2) synchronized不需要手动去释放锁,当synchronized方法或者synchronized代码块执行完毕。
系统会自动释放对该锁的占用。
而lock必须手动的释放锁,如果没有主动的释放锁,则可能造成死锁的问题
2) 示例代码
public class Demo02 {
private Lock lock = new ReentrantLock();
public void method01() {
lock.lock();
System.out.print("i");
System.out.print("t");
System.out.print("c");
System.out.print("a");
System.out.print("s");
System.out.print("t");
System.out.println();
lock.unlock();
}
public void method02() {
lock.lock();
System.out.print("我");
System.out.print("爱");
System.out.print("你");
System.out.print("中");
System.out.print("国");
System.out.println();
lock.unlock();
}
}
https://blogs.oracle.com/jonthecollector/our-collectors