本文是基于jdk8进行分析的
Java虚拟机包含类装载器子系统、执行引擎、运行时数据区、本地方法接口和垃圾收集模块。其中垃圾收集模块在Java虚拟机规范中并没有要求Java虚拟机垃圾收集,但是在没有发明无限的内存之前,大多数JVM实现都是有垃圾收集的。
首先通过编译器把Java代码转换成字节码,类加载器再把字节码加载到内存中(运行时数据区的方法区内),而字节码文件只是JVM的一套指令集规范,不能直接交给底层系统去执行,所以需要特定的命令解析器执行引擎将字节码翻译成底层系统指令,再交给CPI去执行,而这个过程需要调用其他语言的本地库接口来实现整个程序的功能。
Java类加载机制就是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载器分为启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器。各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。除了启动类加载器(Bootstrap ClassLoader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。
java类加载分为5个过程,加载-->验证-->准备-->解析-->初始化。这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
java.lang.Class
对象,作为对方法区这个类的各种数据的访问入口。双亲委派机制就是当某个类加载器收到加载类的请求,如果这个类没有被加载过,该类加载器不会直接加载,会先为委派给父加载器,如果父加载器没有加载过,依次往上传递,直到顶层启动类加载器。如果父加载器可以完成加载任务,则父加载器加载返回;如果父加载器不能完成加载任务,才会自己去进行加载。一句话概述双亲委派机制加载流程就是,从下往上检查类是否已经被加载,从上往下尝试去加载。
双亲委派机制的优点:
双亲委派机制加载类的核心代码 ClassLoader类的loadClass()方法:
1 protected Class<?> loadClass(String name, boolean resolve)
2 throws ClassNotFoundException
3 {
4 synchronized (getClassLoadingLock(name)) {
5 // 首先会检查该类是否已经被本类加载器加载,如果已经被加载则直接返回
6 Class<?> c = findLoadedClass(name);
7 if (c == null) {
8 // 如果没有被加载,则委托父加载器去加载
9 long t0 = System.nanoTime();
10 try {
11 if (parent != null) {
12 // 让父加载器对象去调用loadClass方法
13 c = parent.loadClass(name, false);
14 } else {
15 // parent==null,说明父加载器是启动类加载器。启动类加载器是C++编写的,这里去调用本地方法区尝试加载该类。
16 c = findBootstrapClassOrNull(name);
17 }
18 } catch (ClassNotFoundException e) {
19 }
20 if (c == null) {
21 // If still not found, then invoke findClass in order
22 // to find the class.
23 long t1 = System.nanoTime();
24 // 如果父加载器没有加载到该类,则自己去加载。这里会调用URLClassLoader类的findClass()方法
25 c = findClass(name);
26 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
27 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
28 sun.misc.PerfCounter.getFindClasses().increment();
29 }
30 }
31 if (resolve) {
32 resolveClass(c);
33 }
34 return c;
35 }
36 }
全盘负责委托机制就是当一个Classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class通常也由这个Classloader负责加载。
打破双亲委派机制就是我们希望自定义类加载器去直接加载指定类,而不是先委托父加载器去加载或者是自定义类加载器加载不到才让父加载器去进行加载。
了解双亲委派机制以及打破双亲委派机制之后,我们可以自己写一个自定义类加载器。自定义类加载器实现思路:
如果使用双亲委派机制就是重写findClass()方法(类加载器具体去加载类的方法),代码传送门。
如果要打破双亲委派机制,在重写findClass()方法基础上,还需要重新loadClass()方法,这里我们可以改写逻辑,先让该类加载器去加载类,加载不到再让父加载器去进行加载,代码传送门。
程序计数器线程私有的,它的生命周期与线程相同,它是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程当前正在执行的方法是本地方法,这个计数器值则应为空。字节码解释器的工作就是通过这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成。这个区域是唯一不会抛出OutOfMemoryError异常的区域。
虚拟机栈是线程私有的,它的生命周期与线程相同。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用和returnAddress类型。
以下异常条件与Java虚拟机栈相关:
如果线程中请求的栈深度大于虚拟机所允许的深度,Java虚拟机将会抛出StackOverflowError异常。
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,Java虚拟机将会抛出OutOfMemoryError异常。
本地方法栈是线程私有的,生命周期与当前线程一致。与虚拟机栈的作用是一样的,区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地方法服务。
以下异常条件与本地方法栈相关联(与虚拟机栈一样):
如果线程中请求的栈深度大于虚拟机所允许的深度,Java虚拟机将会抛出StackOverflowError异常。
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存,Java虚拟机将会抛出OutOfMemoryError异常。
堆是线程共享的,在虚拟机启动的时候创建,从中分配类实例(几乎所有的对象都存放在堆中,但是不是所有的)和数组的内存。对于大多数应用来说,堆是内存最大的一块区域;同时堆是内存模型中最重要的一个区域,也是JVM调优重点关注的区域。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会显式释放。
以下异常情况与堆相关联:
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
堆内存分为年轻代(Young Generation)和老年代(Old Generation)。
方法区是线程共享的,在虚拟机启动的时候创建,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
以下异常条件与方法区域相关联:
如果方法区无法满足新的内存分配需求时,Java虚拟机将会抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分。它包含多种常量,范围从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于常规编程语言的符号表,尽管它包含的数据范围比典型的符号表还大。每个运行时常量池都是从Java虚拟机的方法区分配的。当Java虚拟机创建类或接口时,将为该类或接口构造运行时常量池。
以下异常条件与类或接口的运行时常量池的构造相关联:
如果运行时常量池无法再申请到内存时,则Java虚拟机将抛出OutOfMemoryError异常
。
直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存有时候会使用,而且也可能导致OutOfMemoryError异常出现,所以这里简单提一下。直接内存的分配不会受到Java 堆大小的限制,既然是内存,肯定还是会受到本机总内存的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制从而导致动态扩展时出现OutOfMemoryError异常。
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
引用计数法:为每个对象创建一个引用计数器,有对象引用时计数器+1,引用被释放时计数器-1,当计数器为0时就可以被回收。它有一个缺点就是不能解决循环引用的问题。
分代收集理论
分代收集算法,顾名思义就是根据对象的存活周期将内存划分为几块。一般包括年轻代和老年代。
标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
标记—复制算法
按照容量划分为2个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用区域的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
标记无用对象,让所有存活对象都向一端移动,然后直接清除掉端边界以外的内存。
Serial 收集器(标记—复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
ParNew 收集器(标记—复制算法):新生代并行收集器,实际上是Serial收集器的多线程版本,在多核CPI环境下有着比Serial更好的表现。
Parallel Scavenge 收集器(标记—复制算法):新生代并行收集器,追求高吞吐量,高效利用CPU。吞吐量=用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景。
Serial Old 收集器(标记—整理算法):老年代单线程收集器,Serial收集器的老年代版本。
Parallel Old 收集器(标记—整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本。
CMS 收集器(标记—清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
Garbage First 收集器(标记—整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记—整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或者老年代。