jdk、jre、jvm的关系
jdk:Java Development Kit(java开发工具集)
jre:Java Runtime Environment(java运行环境)
jdk:Java Virtual Machine(java虚拟机)
参考官方文档说明:https://docs.oracle.com/javase/8/docs/
jvm初体验-内存溢出问题的分析和解决
内存溢出测试示例
查找解决问题方式
- 设置参数堆内存快照参数
命令:-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m
说明:
-XX:+HeapDumpOnOutOfMemoryError
:使用了标志-XX:+HeapDumpOnOutOfMemoryError,JVM会在遇到OutOfMemoryError时拍摄一个“堆转储快照”,并将其保存在一个文件中。
-Xms20m
:最小可用内存
-Xmx20m
:最大可用内存
- 执行
执行结果如下:
执行完后,会在项目的目录产生一个快照文件,如图:
- 用eclipse memory工具打开快照文件
方式有两种:-
eclipse memory插件打开
插件安装好后,打开快照文件,其它操作参考eclipse memory工具,eclipse memory插件使用截图:
-
官网下载eclipse memory工具打开
官网地址:http://www.eclipse.org/mat/downloads.php
-
用国内的站点下载快点
解压完成后,打开软件界面如下:
打开快照文件
打开堆内存树信息
到此基本就可以确定问题所在
JVM监控工具
-
jdk自带的监控工具在jdk安装目录的bin包下的jconsole.exe可执行程序,如图:
-
双击选择监控进程
这个不需要在意
-
打开后界面如图:
-
内存监控测试
- 测试代码
- 执行后,在jconsole程序工具栏上点击链接—>新建链接,选择要监控的测试程序,如图:
- 点击内存签,即可观察程序整个过程中各个内存的变化情况。
- 测试代码
了解历史
java发展历史
java8新特性
- 接口的默认方法和静态方法
Java8使用两个新概念扩展了接口的定义:默认方法和静态方法。默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。 - Lambda表达式和函数式编程
测试代码:
public class LanmbdaTest extends JFrame{
private JButton jb;
public LanmbdaTest() {
this.setBounds(200, 200, 400, 200);
this.setTitle("Lanmbda测试");
jb = new JButton("click");
/*jb.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("clicked");
}
});*/
jb.addActionListener(event -> System.out.println("Hello"));//这一行代码与注释代码作用等同
this.add(jb);
this.setVisible(true);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
public static void main(String[] args) {
new LanmbdaTest();
}
}
- Date API
- 重复注解
- 更好的类型推断
Java8编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,从而使得代码更为简洁。 - Nashorn JavaScript引擎
使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。
java虚拟机
Java虚拟机(JVM)一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。
也就是说jvm是一种规范,而实现这个规范的就是jvm的具体实现,而实现jvm规范的常见的有以下几种
-
Sun Classic VM
- 世界上的第一款商用jvm,因此具有特殊的意义。现已被淘汰
- 只能用纯解释器的形式来执行java代码
-
Exact VM
- Exact Memory Management 准确式内存管理
- 编译器和解释器混合工作以及两级即时编译器
- 只在Solaris平台发布
- 英雄气短
-
HotSpot VM
- HotSpot的历史
- 优势
- 称霸武林
-
KVM (Kilobyte)
- 简单、轻量、高度可移植
- 在手机平台运行
-
JRockit
- BEA
- 世界上最快的Java虚拟机
- 专注服务器端应用
- 优势
- 垃圾收集器
- MissionControl服务套件
BEA JRockit Mission Control,用来诊断泄露并指出根本原因。该工具的开销非常小,因此可以使用它来寻找生产环境中的系统的内存泄露。
BEA JRockit Mission Control,(以下简称JRMC)于2005年12月面世,并从JRockit R26.0.0版本开始捆绑了这个工具套件,目前最新的版本是2.0.1。它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。它包括三个独立的应用程序:内存泄漏监听器(Memory Leak Detector)、JVM运行时分析器(Runtime Analyzer)和管理控制台(Management Console)。
- J9
IBM
IBM Technology for Java virtual Machine 简称IT4J - Azul VM
- Liquid VM
- Dalvik Vm
Dex dalvik Executalbe - Microsoft JVM
微软推出的jvm - TaobaoVM
阿里为满足自己的业务,深度定制的Java虚拟机
**高性能JVM **
Azul VM 和Liquid VM
JVM内存
简介
内存区划分
线程独占区:每个线程独自的内存区
线程共享区:所有线程共享的内存区
程序计数器
- 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- 程序计数器处于线程独占区
- 如果程序执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined
- 此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
Java虚拟机栈
- 虚拟机栈描述的是Java方法执行的动态内存模型
- 栈帧
- 每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈、动态链接、方法出口等。
- 局部变量表
- 存放编译期可知的各种基本数据类型,引用类型,returnAddress类型
- 局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量的大小
- 大小
- StackOverflowError: 在单线程操作中,无论是栈深度无限增加,还是栈帧(每个方法调用执行时都会在栈中创建一个栈帧,用来存储局部变量,操作数栈,动态链表,方法出口等信息)占的空间太大,都出现的是StackOverflowError
- OutOfMemory:不断创建新的线程的实践中会出现OutofMemoryError的错误
本地方法栈
基本上都与Java虚拟机栈相同,不同点在于:
- 虚拟机栈为虚拟机执行Java方法服务
- 本地方法栈为虚拟机执行native方法服务
HotSpotVM 中本地方法栈和虚拟机栈合并为一个方法栈。
堆内存
- 存放对象实例
- 垃圾收集器管理的主要区域
- 新生代、老年代、Eden空间
- OutOfMemory
- Xmx -Xms(修改堆内存最大和最新内存空间参数)
方法区
- 存储虚拟机加载的类信息、常量、静态常量,即时编译器编译后的代码等数据
- 类的版本、字段、方法、接口等。
- 方法区和永久代
- 垃圾回收在方法区的行为
- 异常的定义
- OufOfMemoryError
直接内存和运行时常量池
- 直接内存
- 常量池
- 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
- 在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
- 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
- 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
- 在JDK7.0中,StringTable的长度可以通过参数指定:
-XX:StringTableSize=66666
参考博客:https://blog.csdn.net/zm13007310400/article/details/77534349
测试代码:
public class Test4 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
String s3 = new String("abc");
System.out.println(s1 == s3);
System.out.println(s1 == s3.intern());
}
}
执行结果
true
false
true
String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用,否则,将新的字符串放入常量池,并返回新字符串的引用’
上面的s1和s2字符串都是引用的字符串常量,而字符串常量是放到常量池中的,也就是s1和s2在虚拟机栈中指向的是常量池里的"abc"字符串,而常量池中对于同一个字符串,只会维护一份,因此s1和s2引用的是常量池中的同一个地址,而通过new创建的字符串是存放在堆内存中的(不在常量池内),因此s1≠s3,而执行String.intern()后,s1和s3都是引用的常量池中的字符串地址,所以相等。
关于String.intern()可以参考博客:https://blog.csdn.net/baidu_31657889/article/details/52315902 、 http://www.importnew.com/21024.html
对象在内存中的布局
-
对象的创建
- 创建流程
- 给对象分配内存
- 指针碰撞
- 空闲列表
- 线程安全性问题
- 线程同步
- 本地线程分配缓冲 TLAB
- 初始化对象
- 创建流程
-
对象的结构
- Header (对象头)
- 自身运行时的数据(Mark Word)
- 哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
- 类型指针
确定对象是哪一个类的实例
- 自身运行时的数据(Mark Word)
- InstanceData
- Padding:填充占位的作用
- Header (对象头)
深入理解对象的访问定位
- 使用句柄:
定义: Java堆中将会划分出一块内存来作为句柄池,refenerce中存储的就是对象的句柄的地址,而句柄中包 含了对象实例数据与类型数据各自的具体地址信息
优点 : 最大的好处就是reference中存储的是稳定的句柄的地址,在对象被移动(垃圾回收时移动对象是很常见的行为)时只会改变句柄中的实例数据的地址,而reference本身不需要修改
- 直接指针:
定义 : reference中存储直接对象的地址,但是必须考虑放置访问类型数据的相关信息
优点 : 访问速度快,节省了一次指针定位的时间开销
注:HotSpot使用第2种方法,但是使用句柄的方法也很常见
此处参考博客 :https://www.cnblogs.com/start1225/p/6690282.html
内存分配
-
内存分配策略
- 优先分配到eden
- 大对象直接分配到老年代
-XX:PretenureSizeThreshold
:用来指定对象大小超过多少被判定为大对象
- 长期存活的对象分配到老年代
-XX:MaxTenuringThreshold
:用来指定存活超过多少年龄(对象GC存活次数)被分配到老年代中
- 空间分配担保
-XX:+HandlePromotionFailure
:可以用该参数关闭分配担保(+:开启、-:关闭)
- 动态对象年龄判断
-
逃逸分析与栈上分配
- 逃逸分析:分析对象的作用域
- 栈上分配:如果对象的作用域仅存在于当前方法中,那儿么该对象就会被分配到栈帧内存上
垃圾回收
概述
- 如何判定对象为垃圾对象
- 引用计数法
- 可达性分析法
- 如何回收
- 回收策略
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
- 垃圾回收器
- Serial
- Parnew
- Cms
- G1
- 回收策略
- 何时回收
如何判定对象为垃圾对象
引用计数算法
- 在对象添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1
- 打印垃圾回收日志信息参数
-verbose:gc
:打印简略的垃圾回收日志信息-xx:+PrintGCDetail
:配合上面的命令,可以打印详细的垃圾回收日志信息
可达性分析算法
- 用GCRoots的对象,一个一个判断堆内存中有哪些对象被引用,堆内存中剩下没有被引用的就会被垃圾回收机制回收
- 作为GCRoots的对象
- 虚拟机栈
- 方法区的类属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中引用的对象
回收策略
标记清除算法
- 要回收的对象被标记,垃圾回收线程执行时,清除被标记的对象
- 问题
- 效率问题
- 空间问题(使用时间越长,剩余的空间越零碎)
复制算法
- 将内存分区,垃圾回收完后的对象被重新放到一个空闲内存中以此保证内存地址连续,但是存在内存浪费问题。
- 堆
- 新生代
- Eden 伊甸园
- Survivor 存活区
- Tenured Gen
- 老年代
- 新生代
- 方法区
- 栈、 本地方法栈、程序计数器
标记整理算法和分代收集算法
- 标记整理算法
- 需要回收的对象被标记后移到垃圾回收区的内存区,GC线程执行时,清空垃圾回收内存区的所有对象
- 分代收集算法
- 将标记整理算法和复制算法混合使用的算法
垃圾回收器
Serial收集器
- 最基本、发展最悠久
- 单线程垃圾收集器(用户线程和GC线程并发进行,也就是说在某一个时间点,执行的要么是GC线程,要么是用户线程,不能同时存在)
- 桌面应用
Parnew收集器
- 与Serial收集器类似,只是GC线程由单线程编程多个GC线程同时执行,以达到缩短GC处理时间
Parallel Scavenge收集器
- 复制算法(新生代收集器)
- 多线程收集器
- 达到可控制的吞吐量
- 吞吐量:CPU用户运行用户代码的时间 与 CPU消耗的总时间的比值
- 吞吐量:(执行用户代码时间)/(执行用户代码时间+垃圾回收所占用的时间)
-XX:MaxGCPauseMillis
:垃圾收集器停顿时间(毫秒)-XX:GCTimeRatio
:吞吐量大小- 范围:0~99
- 默认:99
CMS收集器Concurrent Mark Sweep
- 工作过程
- 初始标记
- 并发标记
- 重新标记
- 并发清理
- 优点
- 并发收集
- 低停顿
- 缺点
- 占用大量的CPU资源
- 无法处理浮动的垃圾
- 出现Concurrent Mode Failure
- 空间碎片
G1 收集器
- 历史
- 优势
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
- 步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选标记
- 与CMS比较
虚拟机工具
介绍
- jdk自带的有虚拟机工具包,在jdk根目录下的bin目录中,里面很多工具都依赖了lib包中的tools.jar包,需要时,项目中可以导入该jar包在项目中使用虚拟机工具包。
- 需要介绍以下工具
- Jps
- Jstat
- Jinfo
- Jmap
- Jhat
- Jstack
- Jconsole
Jps
- Jps:Java process status (Java进程状态)
- 本地虚拟机唯一id:lvmid(local virtual machine id)
jps -m
:运行时传入主类的参数jps -v
:虚拟机参数jps -l
:运行的主类全名或者jar包名称
Jstat
- 类装载、内存、垃圾收集、jit编译的信息
- 该命令的官方文档介绍地址 http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
- 元空间的本质和永久代类似,都是对jvm规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
Jinfo
- 实时查看和调整虚拟机的各项参数
Jmap
- 堆快照信息
Jhat
- Jhat:JVM heap analysis tool(JVM堆分析工具)
Jstack
- 用于生成当前虚拟机的线程快照(定位线程问题)
Jconsole
- 内存线程等可视化监控工具
- 内存监控
- 线程监控
使用示例
示例一:
public class ThreadTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
sc.next();
}
}
- 启动后,用Jconsole工具进入该方法的线程监控,步骤参考上面(点击跳转)
- 可以发现在main线程发生阻塞,需要手动输入值,如图
- 控制台输入值后,main线程执行完成,jconsole就会提示线程连接断开,如图:
示例二:
测试代码
public class ThreadTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
sc.next();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
}
}
},"while true").start();
sc.next();
testWait(new Object());
}
private static void testWait(Object object) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"wait").start();
}
}
-
运行代码,用jconsole连接测试代码线程,界面如下
-
控制台随便输入值后,执行了while true线程,点开while true线程查看状态,如图:
-
控制台再次输入值后,就会又启动一个wait线程,wait线程状态如图:
此时main线程已经执行完成,在线程列表中是找不到的。
示例三,死锁:
示例代码
//类1
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();
}
}
//类2
public class DeadLock implements Runnable{
private Object obj1;
private Object obj2;
public DeadLock(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println("Hello world!");
}
}
}
}
- 启动main方法后,会发现线程死锁,jconsole界面显示内容如下:
为了方便对比,我开了三个窗口观察,左一看的是线程一的状态,中间看的是线程二的状态信息,右一是点击下面的检测死锁之后检测出来的死锁线程。
VisualVM
- 官网地址:https://visualvm.github.io/index.html
- 插件地址:https://visualvm.github.io/pluginscenters.html
- 简介:VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为您提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时它还支持在 MBeans 上进行浏览和操作。简单说就是加强版的jconsole
- eclipse 安装visualvm插件,参考博客:https://blog.csdn.net/wbcg111/article/details/52782197
界面如图:
jdk目录下也自带该工具,如图:
工具界面
性能调优案例实战
案例一
- 问题:经常有用户反映长时间出现卡顿的现象。
- 处理思路
优化sql监控CPU监控线程监控内存- Full GC 20~30S
- 总结经验
- 问题的根本原因:堆内存比较大的时候,老年代内存也比较大,里面的有许多的大对象并且资源也多,Full GC回收老年代内存时费时
- 解决方案:部署多个web容器,减小每个web容器的堆内存,这样就减小了Full GC的老年代回收时间。
案例二
- 问题:不定期内存溢出,把堆内存加大也无济于事。导出堆转储快照信息,没有任何信息。内存监控,正常。
- 处理思路
- 定位问题:通过长时间的监控,监控到NIO(NIO是申请堆外的机器内存,而项目中机器内存较小,在申请堆外内存时,如果申请不到足够的堆外内存,就会出现内存溢出)的一个StringBuffer引起的内存溢出。
认识类的文件结构
Class文件简介和发展历史
Class文件结构概述
- Class文件是一组以8为字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
- 当遇到8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
- Class文件中有两种数据类型,分别是无符号数和表。
- Class文件结构
- 魔数
- Class文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
Class文件设计理念以及意义
- 运行在jvm之上的语言:Clojure、groovy、JRuby、Jython、Scala
文件结构
魔数
工具及准备
- 工具准备
-
安装二进制文件查看工具,我这里使用的是binary viewer ,官网下载地址:http://www.proxoft.com/binaryviewer.aspx
-
打开class文件,界面如下:
-
测试和编译demo代码
3.1. 编写helloworld测试代码
3.2. eclipse Terminal编译helloworld,如图:
注意:测试的时候不要加package,否则会如下报错:
去掉包名后测试成功, 如图:
-
win10自带的进制转换计算器
4.1. win+r 输入calc
打开后菜单上选择程序员,界面如图:
-
class文件的前四位表示魔数(固定的),第二个四位表示版本,如图
不同的版本编号
将16进制的34转换为10进制为52,如图:
常量池
访问标志
类索引
字段表集合
方法表集合
属性表集合
字节码指令简介
- java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字,称之为操作码,以及跟随其后的零至多个代表次操作所需参数的操作数而构成。
- 操作码的长度为1个字节,因此最大只有256条
- 基于栈的指令集架构