一、什么是JVM
JVM是Java Virual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,他是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
二、JRE/JDK/JVM
JRE(Java Runtime Environment)是Java运行环境,也就是Java平台。所有的Java程序都要在JRE下才能运行。
JDK(Java Development Kit)是开发工具包,用来编译、调试Java程序,它也是Java程序,也需要JRE才能运行。
JVM(Java Virual Machine)是Java虚拟机,它是JRE的一部分,一个虚构出来的计算机,它支持跨平台。
三、JVM原理
JVM是Java核心和基础,在Java编译器和os平台之间的虚拟处理器。他可以在上面执行Java的字节码程序。Java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台的机器码,通过特定平台运行。
四、JVM体系结构
类加载器:加载class文件;
执行引擎:执行字节码或者执行本地方法
运行时数据区:包括方法区、堆、Java栈、PC寄存器、本地方法栈
五、JVM内存区域(运行时数据区)
JDK1.7
JDK1.8
线程共享:堆区、元数据去、直接内存
线程隔离:程序计数器、本地方法栈、java虚拟机栈
1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
这里介绍JDK1.8
1、堆区
Java堆区是被所有线程共享的一块内存区域,是JVM内存占用最大,管理最复杂的一个区域。在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都存放在这里分配内存。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见下图:
堆内存空间默认分配如下:
- 老年代 : 三分之二的堆空间
- 年轻代 : 三分之一的堆空间
- eden区: 8/10 的年轻代空间
- survivor0 : 1/10 的年轻代空间
- survivor1 : 1/10 的年轻代空间
2、元数据区
元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。
3、虚拟机栈
虚拟机栈(Java Virtual Machine Stack)描述的是Java方法执行的内存模型:每个方法被执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
4、本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常类似,它们之间的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务
5、程序计数器
每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。
只有程序计数器没有utOfMemoryError异常。
6、方法区(jdk1.7)
与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、类中的Field信息、类中的方法信息、静态变量即时编译器编译后的代码等数据。通过class对象中的getName等方法来获取信息时,实际这些数据是来源于方法区,方法区是全局共享的。
7、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
8、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现。
六、如何判断对象是否存活
a.引用计数法:给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;但很难解决循环引用问题;
b.可达性分析:通过一系列称为“GC Root”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象GC Roots没有任何引用链相连,则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
七、JVM垃圾回收
- 对新生代的对象的收集称为minor GC;
- 对老年代的对象的收集称为full GC;
- 程序中主动调用System.gc()强制执行的GC为full GC;
- 强引用:默认情况下,对象采用的均为强引用;
- 软引用:适用于缓存场景(只有在内存不够用的情况下才会被回收)
- 弱引用:在GC时一定会被GC回收
- 虚引用:用于判断对象是否被GC
八、垃圾收集算法
标记-清除算法:
如同它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
(1)、效率问题:标记和清除两个过程的效率都不高;
(2)、空间问题:标记清除后会产生大量不连续的内存空间,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够连续的内存而不得不提前出发另一次垃圾收集动作。
复制算法:
为了解决标记-清除算法中效率问题,一种复制算法出现了,它将可用的内存容量划分为大小相等的两块,每次只是用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:内存代价太高,每次基本上要浪费一块内存。
标记-整理算法:
复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。标记-整理算法的标记阶段同“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让存活的对象都向一段移动,然后清理掉端边界以外的内存。适用于老年代对象!
分代收集算法:
这中算法并没有什么新的思想,知识根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为存活率较高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法进行回收。
九、虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是java虚拟机的类加载机制,其实质是是将一个 class字节码文件实例化成class对象并进行相关初始化的过程。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段,其中验证、准备、解析3个部分统称为连接(Linking)。
十、类加载过程
加载
读取类文件产生的二进制流,并转化为特定的数据结构,然后创建对应的java.lang.Calss实例。
验证
为了确保Class文件的字节流包含的信息符合当前虚拟机的要求。
准备
为类变量分配内存并设置类变量初始化。注:这时候进行内存分配的仅包含类变量(被static修饰的的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
解析
解析类和方法,确保类与类之间的引用正确。
初始化
执行类构造器<clinit>()方法。
十一、类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定类名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块成为“类加载器”。
从虚拟机的角度来说,类加载器分为两种:一种是启动类加载器,这是使用c++语言实现的,是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器是由Java语言实现的,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。从Java开发人员来讲,类加载器还可以划分的更细致一些,绝大部分Java程序都会使用到以下三种系统提供的类加载器:
启动类加载器
这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中,启动类加载器无法被Java程序直接引用。
扩展类加载器
它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器
它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们将类加载器的这种层次模型称之为双亲委派模型:
双亲委派模型的工作过程是:
如果一个类加载器收到了类加载请求,它首先自己不会尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。