目录
第一章 jvm概述
一 知识点体系
二、 jdk, jre, jvm的关系
三、内存溢出场景模拟
四、jvm性能监控场景模拟
第二章
一、 Java的历史版本
二、1.8的新特性
三、Java 虚拟机的发展
第三章 - java 五大 内存区域
一、jvm内存
二、java 内存区域 - 程序计数器 - 行号
三、java 内存区域 - java虚拟机栈 - 存放栈帧
四、java 内存区域 - - java本地方法栈
五、java 内存区域 - - java堆
六、java 内存区域 - -方法区
七、java 内存区域 - -方法区 - 常量池
7.1 直接内存 - Nio
第四章 对象的创建
一、给对象分配内存 - 指针碰撞 & 空闲列表
二、线程安全问题 - 线程同步加锁 & 本地线程分配缓冲
第五章、探究对象的结构
一、垃圾回收几个著名的算法
第六章 对象的访问定位方式- 两种
第七章 垃圾回收 GC
7.1 GC概述
如何判断一个对象是垃圾对象? - 找到一个可回收的对象
如何回收?
何时回收?
7.2 垃圾回收-判断对象是否存活算法-引用计数法详解
7.3 垃圾回收-判断对象是否存活算法-可达性分析法详解
知道了哪些对象需要回收,接下来就要使用垃圾回收算法了,
7.4 标记-清除算法
7.5 复制算法 & 内存浪费的解决方案
7.6 标记 - 整理算法
7.7 分代收集算法
算法讲完了,接下来讲解关于垃圾回收器相关的问题
7.8 垃圾收集器-serial收集器详解.
7.9 垃圾收集器-ParNew收集器详解.
7.10 垃圾收集器-parallel scavenge收集器详解.
7.11 垃圾收集器-CMS收集器详解.
7.12 垃圾收集器-最牛的垃圾收集器-g1收集器详解
第八章、 内存分配
8.1 内存分配概述
8.2 内存分配-Eden区域 - 对象优先分配到Eden区
8.1 内存分配-大对象直接进老年代
8.1 内存分配-长期存活的对象进入老年代
8.1 内存分配 - 空间分配担保
8.1 内存分配-逃逸分析与栈上分配
第九章、虚拟机工具介绍
我们先介绍了jvm,内存结构,然后介绍了对象,对象的创建,内存分配原则,对象回收,访问定位等。通过参数调整JVM到最优状态,
9.1 虚拟机工具 - jps详解
9.2 虚拟机工具 - jstat详解
9.3 虚拟机工具 - jinfo详解
9.4 虚拟机工具 - jmap详解 (存堆快照信息)
9.5 虚拟机工具 - jhat详解 - heap analyse tools
9.6 虚拟机工具 - jstack详解
9.7 虚拟机工具 - jConsole详解
9.8 死锁的监控
第十章 visual VM - 监控JVM性能的可视化工具
第十二章、 JVM性能调优案例讲解
案例1 - 一个性能高机器部署一个普通的web应用,因为Full GC的问题,经常有用户反映长时间出现卡顿现象
案例2 - direct memory 内存小溢出问题:不定期的内存溢出,把堆内存加大,也无济于事,导出堆转储快照信息,没有任何信息,内存监控正常,把direct memory改大一些就没有问题了。不要忘记每一块内存区域的存在
案例3 - JVM奔溃,connect reset 问题: 两端消息通讯不对等,中间加消息队列
2.1 垃圾收集器
serial (单线程,结构简单,效率高)parnew(多线程,和CMS结合) parallel (服务的默认的,针对吞吐量的)cms (并行的)g1(效率最高的)
2.2 垃圾对象的标记算法
引用计数法(一般不用),可达性分析法
2.3 垃圾收集算法
标记-清除算法(慢)
复制算法(适用于新生代内存)
标记整理算法(适用于老年代内存)
分代收集算法(结合,根据不同代选择不同算法)
首先在Eden分配; 大对象直接进入老年代;长期存活对象进入老年代;空间分配担保;逃逸分析&栈上分配;
命令行: jps;jstat;jinfo;jmap;jhat;jstack;
图形化: jconsole; visual VM(基于插件)
jdk>jre> jvm
jre = jvm + java se api
package heap_error;
import java.util.ArrayList;
import java.util.List;
public class MainTest {
public static void main(String[] args) {
List demoList = new ArrayList();
while(true) {
demoList.add(new Demo());
}
}
}
这几个参数分别设置了 导出堆内存镜像,内存大小的设置
-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m
执行完后在项目下有个这个文件: java_pid5392.hprof, 是快照,分析内存
分析堆内存镜像,需要用到这个工具定位程序代码出现内存溢出的场景
64位下载链接: https://www.eclipse.org/downloads/download.php?file=/mat/1.9.0/rcp/MemoryAnalyzer-1.9.0.20190605-win32.win32.x86_64.zip&mirror_id=1142
经过分析,一眼就可以看出demo这个对象内存太大。
package jconsole;
import java.util.ArrayList;
import java.util.List;
public class JconsoleTest {
// 构造函数
public JconsoleTest() {
byte[] b1 = new byte[128 * 1024];
}
public static void main(String[] args) {
// 睡眠5s
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fill(1000);
}
private static void fill(int n) {
List jconsoleList = new ArrayList();
for(int i = 0; i< n; i++) {
jconsoleList.add(new JconsoleTest());
}
}
}
在jdk bin 目录下有jconsole这个监控工具,直接打开
会是这个东西
了解内存模型,了解垃圾回收机制,了解类分配策略
1. lambda函数式编程
package lambda;
import java.awt.Event;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
public class LambdaTest extends JFrame{
private JButton jButton;
public LambdaTest() {
this.setBounds(200,200,400,200);
this.setTitle("点击显示");
jButton = new JButton("点击");
this.add(jButton);
this.setVisible(true);
// jButton.addActionListener(new ActionListener() {
// @Override
// public void actionPerformed(ActionEvent e) {
// System.out.println("ddddddddddd");
// }
// });
jButton.addActionListener(event -> System.out.println("hhhhhh"));
}
public static void main(String[] args) {
new LambdaTest();
}
}
线程独占区就是每个线程都有这三个区域,自己家的厕所
线程共享区就是每个线程都可以共享, 公共厕所
设置栈大小 - 栈内存溢出
不设置栈大小,不断申请内存 - 内存溢出
常量池是方法区里面的一块小空间
java 和 c 是由内存动态分配和垃圾回收围起来的高墙
Java堆是被所有线程共享的一块内存区域,主要用于存放对象实例,为对象分配内存就是把一块大小确定的内存从堆内存中划分出来,通常有指针碰撞和空闲列表两种实现方式。
1.指针碰撞法
假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
2.空闲列表法
事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
如果想多了,创建一个对象还是挺麻烦的,需要这么多步骤,那么我们在开发过程中尽量非必须的对象创建呢?
创建对象有以下几个要点:
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
复制算法
标记 - 清除算法
标记 - 复制算法
分代收集算法
对象要保存到实例的指针, 也要保存到类的指针
垃圾回收算法一般JVM帮我们处理,但是当高并发下,需要提高性能的时候需要更改注意GC,
三个问题:
引用计数法
可达性分析法
回收策略算法
复制算法
标记 - 清除算法
标记 - 复制算法
分代收集算法
回垃圾回收器
Serial
Parnew
Cms
G1
本程序系统默认使用 parallel垃圾回收器
1. 定义
2. 存在问题演示
配置run config, 打印jvm信息 : -verbose:gc -XX:+PrintGCDetails
package gc_引用计数器;
public class Main_index_count {
private Object instance;
public static void main(String[] args) {
// 创建两个对象
Main_index_count m1 = new Main_index_count();
Main_index_count m2 = new Main_index_count();
// 两个对象相互引用
m1.instance = m2;
m2.instance = m1;
// 切断栈变量引用 - -verbose:gc -XX:+PrintGCDetails
m1 = null;
m2 = null;
System.gc();
}
}
显示日志
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" -verbose:gc -XX:+PrintGCDetails "-javaagent:D:\progmaInstall\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=10713:D:\progmaInstall\IntelliJ IDEA 2018.2.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;C:\work\IDEA_WS\JVM\out\production\JVM" gc_引用计数器.Main_index_count -verbose:gc -XX:+PrintGCDetails
[GC (System.gc()) [PSYoungGen: 2987K->808K(28672K)] 2987K->816K(94208K), 0.0028616 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(28672K)] [ParOldGen: 8K->664K(65536K)] 816K->664K(94208K), [Metaspace: 3490K->3490K(1056768K)], 0.0069500 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 28672K, used 246K [0x00000000e0980000, 0x00000000e2980000, 0x0000000100000000)
eden space 24576K, 1% used [0x00000000e0980000,0x00000000e09bd880,0x00000000e2180000)
from space 4096K, 0% used [0x00000000e2180000,0x00000000e2180000,0x00000000e2580000)
to space 4096K, 0% used [0x00000000e2580000,0x00000000e2580000,0x00000000e2980000)
ParOldGen total 65536K, used 664K [0x00000000a1c00000, 0x00000000a5c00000, 0x00000000e0980000)
object space 65536K, 1% used [0x00000000a1c00000,0x00000000a1ca6168,0x00000000a5c00000)
Metaspace used 3497K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
Process finished with exit code 0
引用计数的问题解析
1. 定义
2. 图示解析
两大问题:
1. 空间问题:收集之后空间很零散,重新分配对象时候寻址很难,又的调用一次GC机制,性能降低。
2. 效率问题
核心思想:把活的复制到servior区域,把eden中死的全清除,分配内存在Eden取,完整的片段,不浪费
不是直接清除,而是整理再清除,解决了复制算法内存担保的效率慢问题
内存分新生代和老年代,根据不同的分代选择不同的算法,对新生代内存回收率较高的会选择复制算法,老年代内存回收低,用标记整理算法
整体新能低,对分配内存小,速度快的桌面应用适用
public class MainEden {
public static void main(String[] args) {
byte[] b1 = new byte[2 * 1024 * 1024]; // 2M
byte[] b2 = new byte[2 * 1024 * 1024]; // 2M
byte[] b3 = new byte[2 * 1024 * 1024]; // 2M
byte[] b4 = new byte[4 * 1024 * 1024]; // 2M
System.gc();
}
}
Eden区域是新生代的一块区域
GC 和 full GC的区别,full GC可以system.gc() 或系统调用,频率比GC长的多得多。时间长,
-XX:PretenureSizehreshold=6M : 自己指定多大的对象才是大对象
大对象 指的是大字符串或者是大数组,Eden区域gc频繁,直接放在老年代GC慢,性能高
-XX:MaxTenuringThreshold = 15 : 指定相对的存活时间
-XX:+HandlePromotionFailure :开启空间分配担保, - 代表关闭
1. 先看自己老年代内存够不够新生代的复制
2. 检查内存担保有没有开启
逃逸分析: 分析对象的作用域,在方法体内,没有逃逸,反之亦然
我们在定义对象的时候,如果定义在方法体中,对象的作用域只在这个方法中,是不发生逃逸的,不逃逸,就把对象的分配直接放到栈内存当中去,执行完出栈,性能高,能创建局部对象就不要放外面引用。
public class StackAllocation {
public StackAllocation obj;
/** 方法返回StackAllocation对象, 发生逃逸 */
public StackAllocation getInstance(){
return obj == null ? new StackAllocation() : obj;
}
/** 为成员属性赋值, 发生逃逸 */
public void setObj(){
this.obj = new StackAllocation();
}
/** 对象的作用域仅在当前方法有效, 没有发生逃逸 */
public void useStackAllocation(){
StackAllocation s = new StackAllocation();
}
/** 引用成员变量值, 发生逃逸 */
public void useStackAllocation2(){
StackAllocation s = getInstance();
}
}
jps -l : 显示出来运行主类或者jar文件
jps -m : 显示program运行时接收的参数
jps -v : 显示JVM运行时接收的参数
jps -lmv : 显示JVM运行时接收的参数
jstat -gcutil 6692
jstat -gcutil 6692 1000 10 每隔1000ms 执行10次
实时查看和调整虚拟机各项参数
C:\Users\Administrator>jinfo
Usage:
jinfo [option]
(to connect to running process)
jinfo [option]
(to connect to a core file)
jinfo [option] [server_id@]
(to connect to remote debug server)
where
C:\Users\Administrator>jinfo -flag UseSerialGC 4816
-XX:-UseSerialGC // 表示禁用这个gc收集器
存堆快照信息:
jmap -dump:format=b, file=d:\a.bin 8264
-XX:+HeapDumpOnOutOfMemoryError
jmap -histo 8264 | more // more分页查看
1. 生成对快照信息
C:\Users\Administrator>jmap -dump:format=b,file=d:\a.bin 6044
Dumping heap to D:\a.bin ...
Heap dump file created
2. jhat进行分析
C:\Users\Administrator>jhat d:\a.bin
Reading from d:\a.bin...
Dump file created Wed Oct 02 22:55:31 CST 2019
Snapshot read, resolving...
Resolving 121119 objects...
Chasing references, expect 24 dots........................
Eliminating duplicate references........................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
3. 可视化查看
4. OQL 查询对象
select s from java.lang.String s where s.value.length > 1000
用于生成JVM当前时刻线程快照,当前虚拟机内,快照是:每一条线程方法堆栈的集合,目的:定位线程长时间停顿的原因
1. 内存的监控 - 相当于jstat工具
package jconsole;
import java.util.ArrayList;
import java.util.List;
public class JconsoleTest {
// 构造函数
public JconsoleTest() {
byte[] b1 = new byte[128 * 1024];
}
public static void main(String[] args) {
// 睡眠5s
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fill(1000);
}
private static void fill(int n) {
List jconsoleList = new ArrayList();
for(int i = 0; i< n; i++) {
jconsoleList.add(new JconsoleTest());
}
}
}
2. 线程的监控 - 相当于 jstack那个工具
模拟了三个状态, runable状态,while true 状态, wait状态
package jconsole_thread;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
/**
* 名称: main
* 状态: RUNNABLE
* 总阻止数: 0, 总等待数: 0
*
* 堆栈跟踪:
* java.io.FileInputStream.readBytes(Native Method)
* java.io.FileInputStream.read(FileInputStream.java:255)
* java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
* java.io.BufferedInputStream.read(BufferedInputStream.java:345)
*/
//1 输入等待
Scanner sc = new Scanner(System.in);
sc.next();
//2 while true 等待
/**
* 名称: while true
* 状态: RUNNABLE
* 总阻止数: 0, 总等待数: 0
*
* 堆栈跟踪:
* jconsole_thread.Main$1.run(Main.java:14)
* java.lang.Thread.run(Thread.java:748)
*/
new Thread(new Runnable() {
public void run() {
while (true){
}
}
}, "while true").start();
// 3 wait() 等待
/***
* 名称: wait
* 状态: java.lang.Object@26acf562上的WAITING
* 总阻止数: 0, 总等待数: 1
*
* 堆栈跟踪:
* java.lang.Object.wait(Native Method)
* java.lang.Object.wait(Object.java:502)
* jconsole_thread.Main$2.run(Main.java:53)
* java.lang.Thread.run(Thread.java:748)
*/
sc.next();
waittest(new Object());
}
private static void waittest(final Object obj) {
new Thread(new Runnable() {
public void run() {
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "wait").start();
}
}
1. DeadLock .java
package deadlock;
public class DeadLock implements Runnable {
// 定义两个资源
private Object obj1;
private Object obj2;
// 构造方法
public DeadLock(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
// 对两个临界资源进行加锁
public void run() {
synchronized (obj1){
// 死锁只有在非常慢的情况下才会发生,这里睡眠就行了
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("死锁演示");
}
}
}
}
2. MainTest .java 测试类
package deadlock;
public class MainTest {
public static void main(String[] args) {
// 定义两个临界资源
Object obj1 = new Object();
Object obj2 = new Object();
// 定义两个线程分别交叉抢占两个临界资源
new Thread(new DeadLock(obj1, obj2)).start();
new Thread(new DeadLock(obj2, obj1)).start();
}
}
下载地址
1 遇到问题
一个性能高机器部署一个普通的web应用,因为Full GC的问题,经常有用户反映长时间出现卡顿现象
2 处理思路
- 问题是 Full GC,每次gc时间长,20-30S
三条理论原则: 对象首先在Eden分配, 大对象直接进入老年代,长期存活对象放入老年代。
一年录入的考核数据很大,excel workbook对象大, 大对象直接进入老年代,所以一般的GC是不会回收它的,当老年代的内存不够的时候,平时不看,就某个考核点来用系统,大对象很多,就会发生Full GC,又因为堆内存分配的特别大,导致回收时间很长
3 解决办法 :
把堆内存改小,Full gc回收时间短
部署单机的6个Tomcat的集群,部署多个web服务,每个web容器分配4G的堆内存,
前面加一个Nginx,负载均衡采用IP 哈希的方式,没做session的共享,
4 经验总结
根据不同场景选择不同的GC垃圾回收器,使用监控工具
数据是类似于彩票,股票这样的数据
1 遇到问题
不定期的内存溢出,把堆内存加大,也无济于事,导出堆转储快照信息,没有任何信息,内存监控正常
把direct memory改大一些就没有问题了。
不要忘记每一块内存区域的存在
2 解决办法
放到一个8G内存,win7系统,问题久没有了,监控发现有个byteBuffer, 它是 NIO里面的,我们知道NIO为了提高性能,它会申请一些堆外内存,因为2G内存小,又把堆内存加大了,于是在进行NIO的时候,我们的直接内存,也就是堆外内存被撑爆了,然后它自己又不能触发垃圾收集,所以导致了内存溢出,不是堆内存溢出,而是操作系统的内存溢出。把direct memory改大一些就没有问题了。
不要忘记每一块内存区域的存在
积累大量任务,两端信息不对等,JVM奔溃,
中间弄一个消息队列,生产消费者模式