目录
一、JVM 体系结构概述 ★★★
栈 (stack)
堆 (Heap)
新生区、养老区、永久代(元空间)
堆参数调优
OOM 问题定位解决
二、GC 垃圾回收
1、垃圾判定
2、垃圾回收算法 ★★
3、垃圾收集器 (了解)
说出你常遇到的五个运行时异常
请谈谈你对JVM 的理解?java8 的虚拟机有什么更新?
什么是OOM ?什么是StackOverflowError?有哪些方法分析?
JVM 的常用参数调优你知道哪些?
内存快照抓取和MAT分析DUMP文件知道吗?
谈谈JVM中,对类加载器你的认识?
谈谈你对JVM的理解:
1. 入口-出口,类加载器 执行引擎
2. 没有垃圾回收:本地方法栈,本地方法接口,栈,程序计数器
3. 生命周期长:方法区、堆
运行环境:JVM是运行在操作系统之上的,它与硬件没有直接的交互
Java能够实现跨平台,得益于JVM虚拟机
Java能跨平台的原因:JAVA程序不是直接在平台上运行的,是在java虚机(JVM)上进行的
结构图
负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
类的加载过程 ★
(1)加载:load
将class文件加载到内存
(2)链接:link
① 验证:校验被加载的class文件的合法性,并且不会危害虚拟机的自身安全
(所有的java的字节码文件 cafebaby 开头)。② 准备:为 类变量(成员变量) 分配内存 (方法区中) 并设置默认值 (0,null,false),为静态常量赋初始值 (常量池中)。
③ 解析:把字节码中的符号引用 (类似全类名) 替换为对应的直接地址引用
(3)初始化:initialize
1. 静态成员变量 显示赋值语句
2. 静态代码块内容
双亲委派机制 ★★★
不同的类通过不同的类加载器完成加载
① 避免类的重复加载
② 保护程序安全,防止核心类库被随意篡改类加载器的分类:
(1)引导(启动)类加载器(Bootstrap ClassLoader)C++ 又称为根类加载器
它负责加载jre/lib/rt.jar核心类库 ($JAVA_HOME中jre/lib/rt.jar里所有的class),它本身不是Java代码实现的(HotSpot VM 中C++实现的),也不是ClassLoader的子类,获取它的对象时往往返回null。
(2)扩展类加载器 (Extension ClassLoader) Java
它负责加载jre/lib/ext扩展库 ($JAVA_HOME中jre/lib/*.jar ),它是ClassLoader的子类,Java代码编写。
(3)应用程序类加载器 (Application ClassLoader) Java
也称为系统类加载器System Class Loader,它负责加载项目的classpath路径下的类,它是ClassLoader的子类,Java代码编写。(加载自己写的类)
(4)自定义类加载器。(略)
Execution Engine执行引擎负责解释命令,提交操作系统执行。
JVM是运行在操作系统之上的,它与硬件没有直接的交互。native 修饰的方法,交给操作系统执行。
本地方法接口:Java诞生时,给C++留下的接口,在内存中专门开辟了一块区域处理标记为native的代码
本地方法栈:它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
本地方法库:DLL文件,Windows版的Jar包
记录程序执行的顺序。每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
5、方法区 (Method Area)
此区属于共享区间,(生命周期长,垃圾回收少)
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池 存在方法区中
实例变量存在堆内存 中,和方法区无关
软件系统 = 数据结构 + 算法 (理论) ; 软件系统 = 业务需求 + 框架 (实际应用)
队列:先进先出;栈 :先进后出
栈是什么?
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建 (私有的),它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。
栈存储什么?
栈中的数据都是以 栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。
栈帧中主要保存3 类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
- 栈操作(Operand Stack):记录出栈、入栈的操作。
- 栈帧数据(Frame Data):包括类文件、方法等等。
PC寄存器:作用在栈中,指向下一个方法的指针,决定方法执行顺序
栈溢出 (不是栈内存):StackOverflowError 通常出现在递归调用时。解决:递归调用要有出口。
存储实例对象
Java虚拟机规范,一套规范,落地的产品:
三种JVM:•Sun公司的HotSpot •BEA公司的JRockit •IBM公司的J9 VM
(1) Java7 及以前堆的组成
Heap 堆:一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存逻辑上分为三部分:
也称为:新生代(年轻代)、老年代、永久代(持久代)。
70%、80会触发垃圾回收。老年区满后 内存溢出。永久代不属于堆内存的范畴,属于方法区
(2) Java8 后堆逻辑上的变化
Jdk1.6及之前: 有永久代,常量池1.6在方法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: 无永久代,常量池1.8在堆中。元空间(对方法区的实现)
永久代与元空间的最大区别之处:
永久代使用的是jvm的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小仅受本地内存限制。
(3) 物理层面,堆内存划分
默认在新生代抗过15次gc,升级到养老区。From与To区会进行交换,随后清空Eden、To
新生区 (新生代)
新new出的对象,会存入伊甸区,80%空间被占用后,产生轻gc:伊甸园区中销毁没有被其他对象引用的对象。将伊甸园中的剩余对象移动到幸存 From区。
GC垃圾回收过程:复制 -> 清空 -> 互换
1、幸存者从 eden、From 复制到 To区,年龄+1
2、清空 eden、From 区 (垃圾回收)
3、To 和 From 互换 (To区永远是空的,From区存储对象)
大对象特殊情况:幸存区存不下,直接进入养老区
养老区 (老年代)
经历多次GC仍然存在的对象(默认是15次),老年代的对象比较稳定,不会频繁的GC
若养老区也满了,那么这个时候将产生重GC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现 java.lang.OutOfMemoryError: Java heap space 异常,说明Java虚拟机的堆内存不够(堆空间出现的内存溢出)。
原因有二:
(1) Java虚拟机的堆内存设置不够,通过参数-Xms(初始)、-Xmx(最大) 来调整。
(2) 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集 (存在被引用)。
什么对象一定进入养老区:池对象 (连接池、线程池)
永久区 (永久代) (非堆)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class、Interface 的元数据,运行时所需要的环境。关闭 JVM 才会释放此区域所占用的内存。
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开(物理上)。
Jdk1.6及之前: 有永久代,常量池1.6在方法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: 无永久代,常量池1.8在堆中。元空间(对方法区的实现)
永久代与元空间的最大区别之处:
永久代使用的是jvm的堆内存,但是java8以后的元空间是使用本机物理内存,并不在虚拟机中。因此,默认情况下,元空间的大小仅受本地内存限制。
1、1.7、1.8参数调优:-Xms(初始堆内存大小) -Xmx(最大堆内存大小)
为什么效率会变高:内存变大,重GC次数变少
常用JVM参数
怎么对jvm进行调优?通过参数配置
参数 | 备注 |
---|---|
-Xms | 初始堆大小。只要启动,就占用的堆大小,默认是内存的1/64 |
-Xmx | 最大堆大小。默认是内存的1/4 |
-Xmn | 新生区堆大小 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory(); // 堆的最大值,默认是内存的1/4
long totalMemory = Runtime.getRuntime().totalMemory(); // 堆的当前总大小,默认是内存的1/64
System.out.println("maxMemory = " + maxMemory);
System.out.println("totalMemory = " + totalMemory);
}
idea运行时设置方式如下:
执行前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
OOM演示:
System.gx(); 是否能执行垃圾回收?
程序结束时,内存快满时,才会触发GC
final finally finalize区别?
final 修饰的对象(类 方法 变量),不可变
try/catch/finally 无论是否异常都会执行
finalize 垃圾回收释放内存时候调用的方法
1. MAT 工具 (Eclipse专用)
2、生成
2. Idea分析dump文件
public class JVMTest {
byte[] byteArray = new byte[1*1024*1024];//1MB
public static void main(String[] args) {
Listlist = new ArrayList();
int count = 0;
try {
while(true)
{
list.add(new JVMTest());
count = count + 1;
}
} catch (Throwable e) {
System.out.println("*********count="+count);
e.printStackTrace();
}
}
}
(1) 配置参数,运行测试案例生成dump文件
-Xmx50m -Xms10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\
(2) 使用java自带的分析工具
面试题:
- - JVM内存模型以及分区,需要详细到每个区放什么
- - 堆里面的分区:Eden,survival from to,老年代,各自的特点。
- - Minor GC与Full GC分别在什么时候发生
- - GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方?
JVM垃圾判定算法:(对象已死?)
- - 引用计数法(Reference-Counting)
- - 可达性分析算法(根搜索算法)
GC垃圾回收主要有四大算法:(怎么找到已死对象并清除?)
- - 复制算法(Copying)
- - 标记清除(Mark-Sweep)
- - 标记压缩(Mark-Compact),又称标记整理
- - 分代收集算法(Generational-Collection)
1. 引用计数法(Reference-Counting) (不再使用)
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
*优点:简单、高效
*缺点:1. 引用和去引用伴随着加减算法,影响性能,2. 很难处理循环引用,相互引用的两个对象则无法释放。
2. 可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
真正标记以为对象为可回收状态至少要标记两次。
第一次标记:不在 GC Roots 链中,标记为可回收对象。
第二次标记:判断当前对象是否实现了finalize() 方法,如果没有实现则直接判定这个对象可以回收,如果实现了就会先放入一个队列中。并由虚拟机建立一个低优先级的程序去执行它,随后就会进行第二次小规模标记,在这次被标记的对象就会真正被回收了!
四种引用 (了解)
平时只会用到强引用和软引用。
强引用:只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象 Object obj = new Object();
软引用:在系统要发生内存溢出异常之前,才会将这些对象列进回收范围之中进行二次回收。SoftReference 类实现软引用。
弱引用:对象只能生存到下一次垃圾收集之前。
虚引用:无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
1、复制算法 (Copying)
两块内存空间,在一块上存储对象,快满时进行标记,幸存的对象复制到另一块内存空间,清除原有内存。
优点:实现简单、效率高,不产生内存碎片
缺点:浪费一半内存空间,如果对象存活率高,复制这一工作浪费时间
使用地点:新生代 Minor GC,这种GC算法采用的是复制算法(Copying)。 (年轻代内存空间小,存活率低)。复制必交换,谁空谁是TO
2、标记清除 (Mark-Sweep)
1. 标记出需要回收的对象,使用的标记算法均为可达性分析算法。
2. 回收被标记的对象。
优点:节省内存
缺点:效率问题 (扫描两次),空间问题 (产生内存碎片)
使用地点:养老区 FullGC
3、标记压缩 (Mark-Compact)
标记-整理法是标记-清除法的一个改进版。 先进行标记清除,然后将所有存活对像都向一端移动,然后直接清除边界以外的内存。
优点:没有内存碎片,节省内存
缺点:效率低
使用地点:养老区 FullGC
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
4、分代收集算法 (Generational-Collection)
内存效率:复制算法>标记清除算法>标记整理算法 (此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
年轻代(Young Gen):特点是区域相对老年代较小,对像存活率低,使用复制算法
老年代(Tenure Gen):区域较大,对像存活率高。使用标记清除、标记压缩
回收器名称 | 分类算法 | 作用区域 | 是否多线程 | 类型 | 备注 |
---|---|---|---|---|---|
Serial | 复制算法 | 新生代 | 单线程 | 串行 | 简单高效,服务暂停,淘汰。 |
ParNew | 复制算法 | 新生代 | 多线程 | 并行 | 唯一和CMS搭配的新生代回收器 |
Parallel Scavenge | 复制算法 | 新生代 | 多线程 | 并行 | 更关注吞吐量 |
Serial Old | 标记-整理 | 老年代 | 单线程 | 串行 | 搭配所有 young gc 使用 |
Parallel Old | 标记-整理 | 老年代 | 多线程 | 并行 | 搭配 Parallel Scavenge |
CMS | 标记-清除 | 老年代 | 多线程 | 并发 | 追求最短的暂停时间 |
1. Serial/Serial Old 串行收集器
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代复制算法、老年代标记-压缩。垃圾收集的过程中会服务暂停(Stop The World) 。
2. ParNew 收集器
ParNew收集器收集器其实就是Serial收集器的多线程版本(借助于硬件发展)。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩。
3. Parallel / Parallel Old 收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。 可以通过参数来打开自适应调节策略。新生代复制算法、老年代标记-压缩。
4. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于"标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些。
优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量
5. G1收集器 (最新的)
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
并行与并发、分代收集、空间整合、可预测的停顿