JVM学习笔记
常见面试题:
- 请你谈谈对JVM的理解?java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlow?怎么分析?
- JVM常用调优参数有哪些?
- 内存快照如何获取,怎么分析Dump文件?
- 类加载器及双亲委派机制
1.JVM位置
运行在操作系统之上。
2.JVM的体系机构
JVM调优发生在堆中。
3.类加载器
类加载过程:虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象。
类加载器作用:通过一个类名获取该类的二进制字节流。
类加载器分为以下四种:
-
启动类加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;
-
扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
-
系统类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
-
自定义类加载器:由java语言实现,继承自ClassLoader;
4.双亲委派机制
当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类。
5.沙箱安全机制
沙箱是一个限制程序运行的环境,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
通俗来说就是虚拟机把代码加载到拥有不同权限的域里,然后代码就拥有了该域的所有权限。这样就能控制不同代码拥有不同调用操作系统和本地资源的权限。
6.Native本地方法接口JNI (Java Native Interface)
- 开拓Java的使用,融合不同的编程语言为Java所用,最初: C、C++
- 它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
- 在最终执行的时候,加载本地方法库中的方法通过JNI
7.PC寄存器
线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。
8.堆
堆内存中细分为三个区域:
- Young区(MINOR GC)
- Old区(FULL GC)
- Perm区 (这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存)
9.堆内存调优
调优命令
-Xms设置初始化内存分配大小,默认1/64
-Xmx设置最大分配内存,默以1/4
-XX: +PrintGCDetails // 打印GC垃圾回收信息
-XX: +HeapDumpOnOutOfMemoryError //下载dump文件
查看初始化内存、最大分配内存:
package Jvm;
public class JvmTest1 {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回虚拟机的总内存
long total= Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t");
System.out.println("total="+total+"字节\t");
//默认情况:总内存时电脑内存的1/4,最大内存为1/64
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails
}
}
产生OOM的情况,一直new对象:
package Jvm;
import java.util.ArrayList;
//dump文件用法 -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemory
//-Xms设置初始化内存大小、-Xmx设置最大分配内存
public class JvmTest3 {
byte [] array =new byte[1*1024*1024];
public static void main(String[] args) {
ArrayList<JvmTest3> list = new ArrayList<>();
int count=0;
try {
while(true){
list.add(new JvmTest3());
count=count+1;
}
}catch (Error e){
System.out.println(count);
e.printStackTrace();
}
}
}
OOM排查方法:
1.尝试扩大堆内存,-Xms,-Xmx
2.分析内存,用Jprofiler(内存快照工具),查看内存占用情况。
10.栈
- 一旦线程结束,栈就Over
- 栈中存放:8大基本类型+对象引用+实例的方法
- 每执行一个方法,就会产生一个栈帧。
- 栈帧:存储局部变量表、操作数、动态链接和方法返回
- 堆-栈-方法区 关系图:
11.堆与栈的区别
(1)申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于Java 需要手动 new Object()的形式开辟
(2)申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
(3)申请大小的限制
stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(默认值也取决于虚拟内存的大小),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的, 自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见, 堆获得的空间比较灵活,也比较大。
(4)申请效率的比较
stack:由系统自动分配,速度较快。但程序员是无法控制的。
heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
(5)heap和stack中的存储内容
stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址, 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
11.对象实例化过程(重要)
1.判断对象对应的类是否被加载、解析和初始化。如果没有,在双亲委派模式下,使用当前类加载器查找对应的.class文件。如果有,直接进行类加载,生成对应的class类对象。
2.为对象分配内存。首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
3.零值初始化。所有属性设置默认值。
4.设置对象头。将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。
5.执行init方法进行初始化。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
12.垃圾回收
java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法。
- 标记清除法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:在遍历一遍,将所有标记的对象回收掉。
特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC。 - 标记整理法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉。
特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生。 - 复制算法
将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除。
特点:不会产生空间碎片;内存使用率极低; - 分代收集算法: 根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
13.JMM(Java Memory Model)
作用:缓存一致性协议,用于定义数据读写的规则。
通过Happens-Before规则实现:
- 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
- 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
- volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
- 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
- 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
- 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C