JVM探索
JVM内存模型
JVM运行时,内存区域划分为:
- 堆:线程共享。所有对象的实例都在堆上分配内存。堆区分为年轻代和年老代,其比例为1:2。年轻代中又分为eden区和s0、s1,其比例为8:1:1。
- 栈:线程私有。一个每个方法执行时会创建一个栈帧,方法调用就对应着出入栈。每个栈帧包含局部变量表、操作数栈、动态连接、返回地址。
局部变量表用于存储方法参数和局部变量。
操作数栈用于把字节码指令传到操作数栈,以及接收方法返回结果。
动态连接用于把符号引用的方法,改为实际方法的直接引用。 - 元空间:在jdk1.7之前,有方法区的概念(也叫永久代)。常量池就位于方法区。jdk1.7之后,方法区被取消,常量池被移到了堆中。并新增了元空间,用于存放运行时常量池和Class文件。
- 程序计数器:线程私有。用于记录当前线程下虚拟机正在执行的字节码的指令地址。
- 本地方法栈:线程私有。用于执行本地native方法。
GC
定义
Java CG即java的垃圾回收机制。在java中一般不需要开发者自行编码去进行垃圾清除和内存回收。
GC算法
复制算法
把内存分为两块。先遍历使用过的一块,并标记出可以回收的对象。然后把这些对象清除,之后再把存活的对象复制到另一块内存中。
实际上,年轻代上eden区和s0、s1区的比例是8:1:1。每次只使用eden区和其中一个survivor区,只空着一个survivor区,这样不会浪费太多的内存空间。
当存活的对象在s0和s1间来回复制超过一定次数(默认15次)后,就会进入年老代。或者,survivor区中超过一半的对象年龄相同,则如果有对象的年龄超过这些对象,则直接进入年老代。
标记清除
遍历一遍,标记出可以回收的对象,然后清除这些对象。但是会带来内存碎片的问题。
标记压缩
遍历一遍,标记出存活的对象,然后把对象压缩到内存的一端,再回收另一端的对象,防止内存碎片的出现。
判断对象是否可回收
即主要是看该对象有没有被引用。有两种方法:
- 引用计数法:即对象每当被引用,就在对象的引用计数器上+1,否则就-1。当计数器为0时,说明对象没有被引用,可以回收。但是引用计数法会出现循环引用的问题。
- 引用链法:找到GC ROOT对象,从GC ROOT对象开始,往下搜索,这个路径就是引用链。如果一个对象到CG ROOT对象没有引用链,则说明对象可以回收。在Java GC中就是用这种方法判断。
CG ROOT对象一般包括:
- 栈中引用的对象。
- 静态变量和常量引用的对象。
- 本地方法栈native方法引用的对象。
垃圾回收器
在年轻代上,使用的都是复制算法,用到的回收器有:
- serial:串行回收器。单线程进行垃圾回收,回收的时候会stop the world(即暂停所有工作线程)。
- parallel:并行回收器。多线程进行垃圾回收。
- parNew:serial的多线程版本。
在年老代上,使用的是标记清除和标记压缩算法,用到的垃圾回收器有:
- serial old:串行回收器的年老代版本。
- parallel old:并行回收器的年老代版本。
- cms:是一个以获取最短停顿时间为目标的收集器。它的GC过程分为4步。
1.标记GC ROOT关联的对象。
2.并发标记所有可回收对象。
3.修正并发标记期间的标记。
4.并发清除对象。
G1垃圾回收器,是JDK9后的默认垃圾回收器。它可以跟用户程序并发执行回收。它将堆划分为多个大小相等的region,不再分年老代和年轻代。G1回收器在停顿和延迟可控情况下尽可能提高了吞吐量。
YGC和FullGC
当一个对象在堆中请求内存分配时,eden区无法满足内存分配的需求,则触发ygc。如果ygc后,内存仍然不足,则对象进入年老代。年老代也无法分配空间,则触发fullgc。如果fullgc也不能满足所需空间,则抛出oom异常。
类加载
类加载器
- Bootstrap类加载器:即启动类加载器,负责加载jdk中rt.jar中的类文件。它是所有类加载类的父类加载器。
- Extension类加载器:即扩展类加载器,负责加载java的扩展类库,即/jre/lib/ext下的类文件。
- System类加载器:即系统类加载器,负责把用户类路径(即java的classpath或-Djava.class.path变量所指的目录,也就是当前类所在路径及其引用的第三方类库的路径)下的类文件。
- 自定义类加载器。
双亲委派模型
当一个类加载的时候,首先会向上询问父类加载器是否已经加载,如果没有则依次往上询问。如果父类加载器都没有加载,则从上往下依次尝试当前类加载器是否能加载。
逆双亲委派模型
即不按照双亲委派模型的顺序去进行加载。比如jndi,它的代码由启动类加载器加载。但jndi需要调用classpath下的jndi接口。由于启动类加载器不知道这些代码,所以就需要通过thread类的setContextClassLoader设置线程上下文类加载器(即thread context classloader),用这个父类加载器去请求子类加载器完成加载。
类的生命周期
- 加载:将字节码转为二进制流。
- 验证:验证该二进制文件是否符合class文件规范。
- 准备:为静态变量、常量赋值。
- 解析:把常量池中的符号引用改成直接引用。
- 初始化:执行初始化代码。
类对象的实例化过程
- 判断类是否加载。如果未加载,则需要先执行类加载。
- 类加载后,首先需要分布内存空间。
- 接着,为实例赋默认值。
- 设置对象头,包括hashcode,cg年龄等。
- 执行构造函数初始化。