1、JVM在哪
2、JVM体系结构
3、类加载器
运行流程:
分类:
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
4、双亲委派机制
原理:
- 一个类加载器接收到了加载类的请求,它不会自己加载这个类,而是向上委托给父类加载器进行类加载
- 如果父类加载器还存在父类加载器,则会进一步委托给父类的父类,依次向上委托直到达到顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回;若不能完成加载任务,则向下委托子类加载器加载
- APP–>EXT—B0OT(最终执行)
优点:
- 避免了类的重复加载
- 保护了程度安全
5、Native
/*
凡是带native的关键字,则说明不在java的作用范围内,需要调用底层C/C++语言的库
会进入本地方法栈,调用本地方法接口(JNI)
JNI:扩展java的使用,融合不同的编程语言为Java所用
在最终执行的时候,通过JNI加载本地方法库中的方法,如Java驱动打印机
*/
private native void hello();
6、方法区
- 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法:如构造函数、接口代码也在此定义
- 所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区与常量池:
- 方法区存储的是class文件的信息和动态常量池;class文件的信息包括类信息和静态常量池,类信息是class文件的框架,其具体内容是存储在静态常量池中。
- 动态常量池中不仅存储了静态常量池中的数据,还将静态常量池里的符号引用转换为直接引用,且动态常量池的内容是可以动态添加的
方法区与永久代:
- 方法区在虚拟机启动时创建,是堆的逻辑组成部分,但与堆是分开的,通常又叫“非堆”
- 方法区与永久代像Java中的接口和类的关系,类实现了接口,方法区是接口,永久代是类,永久代是虚拟机规范方法区的一种实现方法
- jdk1.6及之前,常量池在方法区/永久代中,方法区与堆是独立的
- jdk1.7时永久代退化,将常量池移到了堆中
- jdk1.8及之后,将永久代改为元空间 ,存放到了本地内存;但没有对常量池做出改动,常量池仍然在堆中
7、对象初始化过程
- JVM先找到指定的类的字节码文件,并加载进内存;
- 在内存中开辟一段空间,分配内存地址;
- 在对象的内存空间中对 对象的属性进行默认初始化;
- 如果属性有显示初始化时,开始对属性显示初始化;
- 调用对应的构造函数进行初始化;
- 初始化完毕后,该对象实例化完成;
public class Test2 {
int num = 9;
String className = new String("class A");
/*
空参数构造方法,如果不显示指定,会有默认的空参构造方法
*/
Test2() {
System.out.println("Test2无参构造函数");
}
void show() {
System.out.println("show方法调用");
System.out.println(num + "----" + className);
}
/*
实例化过程:
1. Test2.class先加载进内存;
2. new Test2()--->开辟内存空间,分配地址(假设为0x0045);
3. num和className默认初始化
num = 0;
className = null;
4. 显示初始化
num = 9;
className = new String("class A");
5. 构造函数初始化
show()方法被调用
6. 对象实例化完成
c = 0x0045
*/
public static void main(String[] args) {
Test2 c = new Test2();
c.show();
}
}
8、堆
- 一个JVM只有一个堆内存,堆内存的大小是可以调节的
- 类加载器读取了类文件后,一般会把类、 方法、常量、变量存放到堆中,保存我们所有引用类型的真实对象;
- 堆内存中还要细分为三个区域:新生区(伊甸园区) Young/New 养老区old 永久区Perm
- GC垃圾回收,主要是在伊甸园区和养老区
- JDK8之后,将永久存储区改位了元空间
9、分析OOM原因
idea中安装JProfiler插件
安装JProfiler客户端
编写一个OOM程序:
//-Xms 设置初始化内存分配大小 1/64
//-Xmx 设置最大分配内存 1/4
//-XX:+PrintGCDetails 打印GC垃圾回收
// -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError 测试OOM Dump
public class Test3 {
public static void main(String[] args) {
Byte[] bytes=new Byte[1024*1024];
List<Object> list=new ArrayList<>();
//使用一个while死循环不断地向集合中添加数组,直到内存溢出
while (true){
list.add(bytes);
}
}
}
更改VM参数:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
运行程序,在src目录下打开生成的.hprof文件
可以看到是char数组内存溢出
可以在JProfiler中的Thread Dump中的main线程里清楚的看到代码的第几行内存溢出
10、GC
一旦发生GC 就是将10%的from和80%的eden中存活的对象转移到to去当中 接下来将90%除to区之外的空间全部释放
10.1 引用计数法
给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时候计数器都为0的对象就是不可能再被使用的,就会被GC回收
10.2 复制算法
GC开始时,对象只会存在于Edem区和幸存区中的From区。随着GC的进行,将Eden区存活的对象和from区中没有到阈值(默认15)的对象复制到To区中,Eden和Fron区中对象清空,且Fron区和To区交换角色;始终保持To区为空(谁空谁是To);再多次GC后,如果对象达到了阈值,则会将幸存区中存活的对象移动到养老区。
优点:没有内存碎片
缺点:浪费了内存空间,To区永远的空的
复制算法最佳使用场景:对象存活度较低,新生区
10.3 标记压缩清除算法
标记清除:
- 先将有效的对象进行标记,在GC时只清除未做标记的对象
- 优点:不会浪费内存空间
- 缺点:两次扫描,浪费时间,且会产生大量的内存碎片
标记压缩:
再次扫描,将所有有效对象压缩到一块,防止内存碎片产生
总结:
- 内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
- 内存整齐度:复制算法=标记压缩算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法