目录
一、JVM 是什么
二、JVM 运行流程
三、Java运行时数据区
1、程序计数器(线程私有)
2、栈区(线程私有)
3、堆
4、方法区
四、OOM内存溢出和内存泄漏
1、OOM内存溢出
2、内存泄漏
五、类加载过程
1、加载
2、连接
3、初始化
4、双亲委派模型
六、垃圾回收(GC)
1、 如何判断对象是死亡对象
(1)引用计数法
(2)可达性分析法
2、垃圾回收算法
(1)标记-清除算法
(2)复制算法
(3)标记-整理算法
(4)分代算法
一、JVM 是什么
JVM 是 Java虚拟机 。虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。即 JVM 是一台被定制过的现实当中不存在的计算机。
JDK、JRE、JVM之间的关系:
JDK:
Java开发工具包,提供给Java程序员使用,包含了JRE,同时还包含了编译器javac与自带的调试工具Jconsole、jstack等。
JRE:
java运行时环境,包含了JVM,Java基础类库。是使用Java语言编程写程序运行的所需环境。
JVM:
Java虚拟机,运行Java代码。
二、JVM 运行流程
程序在执行之前先要把Java代码转换成字节码文件,JVM首先要把字节码文件通过一定的方式用 类加载器 把文件加载到内存中的 运行时数据区。而字节码文件是JVM的一套指令集规范,底层的系统是看不懂的,因此需要用特定的命令解析器 执行引擎 将字节码翻译成底层指令系统再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口 来实现整个程序的功能。
Java进程启动的大概步骤:
① 初始化JVM参数
② 创建并启动 java虚拟机
③ 启动 main 线程执行入口函数 —— 入口类类加载,再执行 main 方法
④ 启动守护线程:GC(垃圾回收机制)线程等
三、Java运行时数据区
Java运行时数据区域也叫内存布局,有五部分组成。
1、程序计数器(线程私有)
程序计数器存的是CPU下一条要执行的指令的地址。比如1个CPU,但是有8个线程,它们并发执行(宏观上),可能第一个线程执行到第4行,第二个线程执行到第10行,第三个线程执行到第2行...
2、栈区(线程私有)
栈中存放的是局部变量,以及方法调用时候的相关信息。
Object o = new Object();
o : 栈里
new Object():堆里
3、堆
程序中创建的对象都保存在堆中。堆区主要有两个部分:新生代和老年代。新生代放新创建的对象,当经过一定GC次数后还存活的对象就会放到老年代。
新生代中还有两个部分:Endn + 两个Survivor(S0/S1)。垃圾回收的时候,会将Endn中存活的对象放到一个未使用的Survivor中,并把当前的End和正在使用的Survivor清理掉。
4、方法区
用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在《Java虚拟机规范》中把此区域称为 方法区。而在实现中,JDK 7时叫做 永久代,JDK 8时叫做 元空间。(可以理解为方法区的实现,也可以理解为不同的方法区的称呼)
类信息(类对象):
.class文件加载到内存里就是类对象,类对象中可以知道这个类里有哪些方法,有哪些静态成员,以及这些方法的权限:public、private等。
四、OOM内存溢出和内存泄漏
1、OOM内存溢出
OOM内存溢出是指程序出现 “java.lang.OutOfMemoryError”。内存对象确实应该存活。此时要根据JVM参数与物理内存比较检查,看是否还应该把JVM内存调大、或者检查对象的生命周期是否过长了。
除了程序计数器以外的5个区域都有可能出现OOM内存溢出:
- Java虚拟机栈、本地方法栈创建栈帧时,如果空间不足,则会OOM;
- 方法区、堆在类加载、创建对象时,如果空间不足,先GC(垃圾回收),GC后还是空间不足的话,就会OOM。
2、内存泄漏
指无用对象(不再使用的对象)持续占有着内存,或者无用对象的内存得不到及时的释放,从而造成内存空间的浪费。(泄漏对象无法被GC)
出现的情况:
随着Java程序运行的时间越来越长,无用对象越来越多,可用的空间越来越少;
java进程一直执行,内存泄漏最终导致OOM。
五、类加载过程
1、加载
加载属于类加载的一个环节,主要是把.class文件加载到内存中。
JVM主要完成下面三件事:
- 根据类名,找到.class文件
- 把.class文件加载到内存
- 创建一个类对象
2、连接
(1)验证:验证.class文件是否符合JVM标准,是否会危害到虚拟机。
(2)准备:给静态变量赋0值。
(3)解析:初始化常量的过程。(Java虚拟机将常量池中的符号引用替换为直接引用的过程)
常量池中,每个常量都有一个编号,最开始的时候,这个敞亮的引用,也就是a,对应的是这个常量的编号(符号引用),并不是“abcd”。这一步就是把编号替换成真是的值(“abcd”)。
3、初始化
执行构造方法的过程。
注意:
public class Test extends B{ public static void main(String[] args) { new Test(); new Test(); } } class A{ public A(){ System.out.println("A 的构造方法");//4 } { System.out.println("A 的构造代码块");//3 } static { System.out.println("A 的静态代码块");//1 } } class B extends A{ public B(){ System.out.println("B 的构造方法");//6 } { System.out.println("B 的构造代码块");//5 } static { System.out.println("B 的静态代码块");//2 } }
打印结果:
4、双亲委派模型
双亲委派模型是指,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此。因此所有加载请求最终都应该传送到最顶层的启动类中,只有当父加载器反馈回来说自己无法完成这个加载请求时(在它的搜索范围中没有找到所需的类),子加载器才会尝试自己完成加载。
启动类加载器:加载 JDK 中 lib 目录中的 Java 的核心类库。(JAVA_HOME/lib 目录)
扩展类加载器:加载 lib/ ext 目录下的类。
应用程序类加载器:加载我们写的应用程序。
自定义类加载器:根据自己的需求定制类加载器。
六、垃圾回收(GC)
因为有一些对象在使用完之后就不使用了,就会变成垃圾,因此JVM会自己进行清理。JVM主要有程序计数器、堆、栈(虚拟机栈、本地方法栈)、方法区组成,而程序计数器和栈是线程私有的,会自动回收掉垃圾,因此主要在堆中进行垃圾回收。(堆是JVM中最大的一块内存空间)
GC中内存划分:
1、 如何判断对象是死亡对象
(1)引用计数法
给对象增加一个引用计数器,每当有一个地方引用它是,计数器就+1,。当引用失效时,计数器就-1。当计数器为0时,就说明对象不再被使用了,即对象是死亡对象了。
优点:
简单
缺点:
- 会浪费一部分空间用来计数。(假如一个对象是2个字节,但是这个用来计数的空间是4个字节)
- 不能解决循环引用的问题。即不能回收循环引动的对象。(在主流的JVM中没有使用该方法)
(2)可达性分析法
通过一系列的被称为 “GC Roots” 的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径称为 “引用链”。当一个对象到 GC Roots没有任何引用链时(即GC Roots到这个对象是不可达的),就说明这个对象是死亡对象。
可以作为 GC Roots 的对象包含以下几种:
- 虚拟机栈中引用的对象。(栈帧中的本地变量表中)
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。(Native方法)
2、垃圾回收算法
(1)标记-清除算法
有两个阶段:标记和清除。先标记出所有需要回收的对象,在完成标记后统一回收所有被标记的对象。(后面的算法都是基于标记-清除算法的改进)
回收的时候:
在新生代中,将 Endn 中存活的对象放到一个未使用的Survivor中,并把当前的 Endn 和正在使用的 Survivor 清除掉。
缺点:
- 效率问题:标记和清除的过程效率都不高;
- 空间问题:标记清除后会产生大量的不连续的内存碎片,可能会导致在以后层序的运行过程中,需要分配较大的对象时,无法找到足够连续的内存而不得不提前触发新的垃圾回收。
(2)复制算法
为了解决 标记-清理 的效率问题。把一块内存划分为相同大小的两块,每次只使用其中一块(创建对象),回收时,把存活对象复制到另一块空置的内存中。
用于存活对象较少的情况。
是新生代的回收算法。
使用场景:对象存活率低。
缺点:内存利用率低,50%
(3)标记-整理算法
标记过程和标记清除过程差不多,只是在标记完后,不立马进行清除,而是将存活对象都向一端移动,将存活对象移动到一端连续的空间,最后清理掉端外剩余的内存。
是老年代的回收算法。
不会产生内存碎片,效率较高。
(4)分代算法
是jvm采取的算法。把内存划分为许多块,针对不同的内存对象的创建、回收特性,使用不同的算法。(类似一国两制)
GC内存划分,不同的内存,使用不同的回收算法:
新生代使用复制算法的优化版;
老年代使用标记清除算法或者标记整理算法。