文章目录
请大家带着以下几个问题来学习!
- 谈谈你对JVM的理解?
- java8虚拟机和之前的变化/更新?
- 什么是OOM(内存溢出),什么是栈溢出?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM,类加载器?
0、虚拟机
虚拟机就是一款软件,用来执行以系列虚拟计算机指令!
java虚拟机特点:
- 一次编译到出执行!
- 自动内存管理!
- 自动回收垃圾!
1、JVM组成及位置
组成:
- 类加载器(Class Loader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地方法接口(Native Method Interface)
用途:
程序在执行前要先把Java代码转换为字节码(class文件),jvm首先需要把字节码通过类加载器把文件加载到内存中的运行时数据区,而字节码文件是java的一套指令规范,不能把这个文件直接丢给底层操作系统去执行,因此需要特定的命令解析器即执行引擎将字节码翻译为底层系统指令再丢给CPU执行,这个过程还需要调用非java代码的本地方法接口来实现整个程序的功能!
2、JVM的体系结构
3、类加载器
作用: 加载class文件
- 虚拟机自带的加载器
- 启动类(根)加载器 jre\lib\rt.jar
- 扩展类加载器 jre\lib\ext
- 应用程序加载器 classLoader
往上找!
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
// 获取类加载器 -- app加载器 AppClassLoader
System.out.println(aClass1.getClassLoader());
// 获取类加载器的父亲 -- 扩展类加载器 ExtClassLoader \jre\lib\ext.jar
System.out.println(aClass1.getClassLoader().getParent());
// 获取类加载器的祖父 -- 启动类加载器 --> null的原因 1. java程序获取不到 2. 不存在 rt.jar
System.out.println(aClass1.getClassLoader().getParent().getParent());
}
}
/*
460141958
1163157884
1956725890
685325104
685325104
685325104
*/
new Car是通过一个类模板出来的,getClass是同一个对象
不管你new多少次,Car模板就只有一个
4、双亲委派机制
保证安全!
先加载系统类,因为害怕我们自己写的类将系统的类替换!
- 类加载器收到类加载的请求
- 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器。 app(应用程序加载器) –> 扩展类加载器 –> 根加载器
- 启动加载器检查是否能够加载当前类,能加载就结束,使用当前加载器,否则,通过子加载器加载 root(根) -> ext(扩展) -> app(应用)
- 重复3
5、沙箱安全机制
java安全模型的核心就是java沙箱
- 作用: 防止恶意污染Java源代码
比如我们自己定义一个包为java.lang,类为String,众所周知,这个类本来是属于jdk的,如果没有沙箱安全机制,如果没有沙箱安全机制的话,这个类会污染到系统中的String。也是有了沙箱安全机制,当类加载的时候会委托顶层的启动类加载器来查看是否能加载这个类,有的话直接加载,没有的话,向下委托给扩展类加载器,重复步骤,直至找到为止!那么这么一来,我们就可以有效的避免java源代码被恶意污染!
6、类的主动使用和被动使用
前言------jvm规定,每个类或者接口被首次主动使用时才对其进行初始化
6.1、主动使用
- 使用new关键字!
- 访问类的静态变量,包括读取和更新!
- 访问类的静态方法!
- 对某个类进行反射操作!
- 初始化子类对导致父类的初始化!
- 执行该类的main函数!
6.2、被动使用
-
引用该类的静态常量,不会导致初始化,但是也会有意外情况发生!此处的常量是指已经指定字面的常量,对于那些需要一些计算机才能得出结果的常量就会导致初始化!
-
// 不会导致初始化 public final static int NUM = 5;
-
// 会导致初始化! public final static int RANDOM = new Random().nextInt();
-
-
构造某个类的数组时不会导致该类额初始化
-
Car[] car = new Car()[10];
-
主动和被动的区别就是 类是否会被初始化 !!!
7、Native
- 凡是带了native关键字,就说明java处理不了。他要调用底层的c语言的库
- 进入本地方法栈,调用本地方法接口JNI java native interface
- 作用,扩展java的使用,融合不同语言为java所用
- 诞生!c,c++横行,为了立足,必须调用c库代码
- 他在内存区域中专门开辟一块标记区域,Native Methond Stack(本地方法栈) 登记native方法
- 在最终执行的时候,加载本地方法库中的方法
Thread中start方法的源码! private native void start0();
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
8、PC寄存器
程序计数器: Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区的方法字节码(用来存储指向对象的一条指令,和即将要执行的代码),在执行引擎读取下一条指令时,是一个非常小的空间,几乎可以忽略不记!
8.1、面试Q?
- Q: 使用程序计数器存储字节码指令地址有什么用?
- A: 因为CPU要不停的切换各个线程,这个时候切换回来就可以知道从哪里接着继续执行!
- Q: 程序计数器为什么是线程私有?
- A: 多线程在一个特定的时间段内只会执行一个线程,CPU会不停的切换线程,如果只有一个程序计数器的话,在多个线程的环境下,会出现下记错,记混等情况!所以为了准确的记录各个线程正在执行的字节码指令地址,最好的方法就是给每个线程都分配一个程序计数器,这样一来,每个线程之间可以独立计算,互不干扰!
9、方法区
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码定义也在此,简单说,所有定义的方法的信息都保存在该区域,次区域属于共享空间
**静态变量(static),常量(final),信息类(构造方法, 接口定义),运行时我的常量池存储在方法区!**实例变量存储在堆内存,和方法区无关
9.1、方法区的GC
- 有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾 收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的, 提到过可以不要求虚拟机在方法区中实现垃圾收集。
- 一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相 当苛刻。但是这部分区域的回收有时又确实是必要的。 方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用 的类型。 回收废弃常量与回收 Java 堆中的对象非常类似。(关于常量的回收比较简单, 重点是类的回收)
9.2、满足GC的三个条件
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子 类的实例。
2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加 载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通 过反射访问该类的方法。
10、栈
一种数据结构!
程序 = 数据结构 + 算法
程序 = 框架 + 业务逻辑
先进后出!
为什么main()方法先执行,后结束?
因为当运行main方法时,将main方法压入栈,当调用其他方法时,继续压入栈,执行完毕后,弹栈,最后弹出main方法
递归造成的内存溢出亦是如此 StackOverFlow
栈不存在垃圾回收,如果存在那么程序就崩了!
栈 : 8大基本类型 + 对象引用 + 实例的方法!
原理: 栈帧
栈放的是对象的引用!!!
栈+ 堆+ 方法区交互关系
10.1、面试Q?
- Q: 什么情况下会出现栈溢出(StackOverFlow)?
- A: 所谓栈溢出就是方法执行时创建的栈帧超过了栈的深度。典型例子 —> 递归!
- Q: 通过调整栈大小,可以保证不出现栈溢出吗?
- A: 不能
- Q: 给栈分配的内存或大越好吗?
- A: 不是,分配给栈的内存越大,只会延缓栈溢出的出现,甚至还会影响到其他内存空间!
11、三种JVM(了解)
- Sun公司 HotSpot(我们目前用的!)
- BEA公司 JRockit
- IBM公司 J9 VM
12、堆
Heap,一个jvm只有一个堆内存,堆内存大小是可以调节的!
类加载器读取了类文件后,一般会把 类,常量,方法,变量放到堆中。
堆内存分为3个大区域
- 新生区
- 老年区
- 永久区
12.1、为什么分区?
将对象根据存活概率进行 分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短!
堆空间!
元空间逻辑上存在,物理上不存在!
我们来手动设置堆的内存大小:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- Xms :设置初始化内存分配大小
- Xmx : 设置最大分配内存
之后重新运行程序!
因为会有内存丢失,所以不可能会那么准确
PSYoungGen 内存 + ParOldGen 内存 --> 305664K + 699392K = 1005056k = 981.5M = 虚拟机试图使用的最大内存!
即说明元空间在逻辑上存在,物理上不存在!
public class Memory {
public static void main(String[] args) {
// 虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
// jvm的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
// 默认情况下: 分配的总内存 是 电脑内存的1/4; 初始化内存是电脑的 1/64!
System.out.println("最大内存 max = " + maxMemory + " 字节; " + (maxMemory / (double) 1024 / 1024) + " MB ");
System.out.println("总内存 total = " + totalMemory + " 字节; " + (totalMemory / (double) 1024 / 1024) + "MB");
// 我们分配堆空间大小! 命令
// -Xms1024m -Xmx1024m -XX:+PrintGCDetails
}
}
所以说堆空间如下也是算对的!
12.2、分代收集思想Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生 区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集, 一种是整堆收集.
部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:
- 新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
- 老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
- 混合收集(Mixed GC):收集整个新生区以及部分老年区的垃圾.
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集. 整堆收集出现的情况:
- System.gc();时
- 老年区空间不足
- 方法区空间不足
开发期间尽量避免整堆收集.
12.3、TLAB机制
12.3.1、什么是TLAB机制?
TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线 程专用的内存分配区域。
如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的 内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在 自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。 JVM 使用 TLAB 来避免多线程冲突,在给对象分配内存时,每个线程使用自己的 TLAB, 这样可以避免线程同步,提高了对象分配的效率。
TLAB 空间的内存非常小,缺省情况下仅占有整个 Eden 空间的 1%,也可以通过选项 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。
12.3.2、为什么有TALB
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.
由于对象实例创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的.
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.
13、新生区
- 类 -> 诞生和成长的地方,甚至死亡!
- 伊甸园区
- 所有的对象都是在伊甸园区new出来的!
- 幸存者区(动态!)
- 0区 / from
- 1区 / to
14、永久区
存储Java运行时的一些环境,用来存放jdk自身携带的Class对象,不存在垃圾回收!关闭虚拟机,就会释放这个区域的内存 !
一个启动类,加载了大量的第三方jar包,tomcat部署了太多应用,大量动态生成的反射类,可能会导致OOM,
- jdk 1.6 : 永久代,常量池在方法区
- jdk 1.7 : 永久代,慢慢退化了,去永久代,常量池在堆中
- jdk 1.8 : 无永久代,常量池在元空间
15、堆内存调优
jvm内存调优? 就是对堆的调优
OOM故障怎么办?
- 扩大内存!
- 内存快照分析工具! MAT(eclipse集成的内存快照分析工具),Jprofiler(idea 集成的内存快照分析工具)
MAT,Jprofiler作用?
- 分析Dump内存文件,快速定位内存泄露
- 获取堆中的数据
- 获得大的对象
Jprofiler下载网址 https://www.ej-technologies.com/download/jprofiler/files
Jprofiler9.2激活码
-
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
在IDEA中下载Jprofiler插件并重启,在工具栏会有图标如下:
下载Jprofiler插件
安装好我们的Jprofiler工具!
测试: 我们故意造成内存溢出
public class JprofilerTest {
// 1m
byte[] arr = new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList list = new ArrayList<String>();
// -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
while (true){
list.add(new JprofilerTest());
}
}
}
会爆出异常!
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.tian.jvm.JprofilerTest.<init>(JprofilerTest.java:16)
at com.tian.jvm.JprofilerTest.main(JprofilerTest.java:25)
之后我们需要借用Jprofiler来dump内存错误!
同上 我们在VM options中输入 指令 : -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
意思就是如果出现了内存溢出错误 dump出来!
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9740.hprof ...
Heap dump file created [7683930 bytes in 0.034 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.tian.jvm.JprofilerTest.<init>(JprofilerTest.java:16)
at com.tian.jvm.JprofilerTest.main(JprofilerTest.java:25)
注意 Dumping heap to java_pid9740.hprof …
表明已经dump下来了,那我们就需要找到这个文件在哪,于此同时,我们在idea的左边文件栏回生产一个文件夹
我们右键我们的类找到我们dump的文件
向上找!找到生成的hprof文件即我们dump下的文件
由于我们已经安装了Jprofiler Ap,我们直接双击,运行!
我们可以看到 list对象分配了90%的内存,太离谱了简直,当然,这是我们的测试!
Jprofiler提示的错误位置!
idea中的位置,
这么一来,我们就很方便的找到了哪里,是什么原因造成的OOM!方便解决
16、GC
GC在垃圾回收时,不是对这三个区域统一回收。大部分都是,回收新生代
- 新生代
- 幸存区(from to)
- 老年区
17、GC算法
17.1、什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是回收的垃圾!
如果不对其进行垃圾回收,那么内存中会一直保存这些垃圾对象,知道应用程序结束,被保留的对象无法用其空间,甚至会造成内存溢出!
标记清除算法
我们来脑补一下,见名知意,我们没用一次对象,就给他进行一次标记!然后清除!
- 好处:不需要额外的空间!
- 坏处:两次扫描严重浪费时间! 会产生内存碎片
标记压缩(整理)算法
标记压缩算法是来优化标记清除算法的!、
- 好处:没有了内存碎片!
- 坏处:多了移动成本!
复制算法
- 好处: 没有内存碎片!
- 坏处:浪费内存空间 ! 因为他要保证to是空的! 极端情况下(对象不会被回收!)
- 最佳使用场景:对象存活度较低! —> 新生区
引用计数器算法(不用了几乎)
18、JMM
java内存模型 (Java Memory Model )
jmm是类似于MSI等 缓存一致性协议,用于定义数据读写的规则!
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
解决共享对象可见性: voliate关键字
19、总结
内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度: 复制算法 = 标记压缩算法 > 标记清除算法
内存利用率: 标记压缩算法 = 标记清除算法 > 复制算法
没有最好的算法,只有适合的算法! 分代收集算法
年轻代: 存活率低 使用复制算法
老年代: 存活率高 使用标记清除(内存碎片不是很多)+ 标记压缩混合(内存碎片多了之后压缩一次)