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.*(递归导致的)
//不设置的话在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
上一篇: 数据库&MySQL