1 概念
JVM 是 .class文件和硬件系统之间的接口,即JVM屏蔽了与具体平台相关的信息,这就是java程序一次编译到处执行的根本原因。
2JVM的构成
JVM = ClassLoader + execution engine + runtime data area
java虚拟机 = 类加载器 + 执行引擎 + 运行时数据区
2.1ClassLoader
ClassLoader的核心作用就是把class文件加载到JVM中。其本质是将Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
JVM 运行并不是一次性加载所需要的全部类的,而是在运行过程中按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。类加载时机有以下几种:
1)使用new关键字实例化对象
2)访问类的静态变量(不是常量)
3)访问类的静态方法
4)反射如( Class.forName,这里只加载类不创建对象实例 )
5)当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化
6)虚拟机启动时,定义了main()方法的那个类先初始化
2.1.1ClassLoader的种类
-
1.启动类加载器(Bootstrap ClassLoader)
这个类加载使用C++语言实现的,是虚拟机自身的一部分。负责将$JAVA_HOME/lib
或者-Xbootclasspath
参数指定路径下面的文件(按照文件名识别,如 rt.jar) 加载到虚拟机内存中.启动类加载器无法直接被 java 代码引用。 -
2.扩展类加载器(Extension ClassLoader)
扩展类加载器是指sun.misc.Launcher\$ExtClassLoader
类,是Launcher
的静态内部类,负责加载$JAVA_HOME/lib/ext
目录中的文件,或者java.ext.dirs
系统变量所指定的路径的类库。 -
3.应用程序类加载器(Application ClassLoader)
应用程序类加载器是指sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath
路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
2.1.2类加载顺序
类的即在顺序遵循双亲委派机制(Parent delegation),即:在需要加载一个类的时候,我们首先判断该类是否已被加载,如果没有就判断是否已被父加载器加载,如果还没有再调用自己的findClass()
方法尝试加载。这个加载顺序定义在java.lang.ClassLoader#loadClass()
方法里。我们在自定义ClassLoader时不建议重写loadClass()
方法,而是 重写findClass()
方法。双亲委派机制的好处是提高了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛java.lang.ClassCaseException
。
需要注意的是,图中各个类加载器之间不是继承关系,而是关联关系,具体是
java.lang.ClassLoader
类定义了一个parent属性。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
……
2.1.3自定义类加载器
ClassLoader 里面有三个重要的方法 loadClass()
、findClass()
和 defineClass()
。
loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass()
让自定义加载器自己来加载目标类。ClassLoader 的 findClass()
方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass()
方法将字节码转换成 Class 对象。
适用场景:
1.加载加密的.class文件。
将正常的.class文件用代码加密以后,自定义ClassLoader 类,重写findClass()
方法,用同样的加密算法对读出文件的字节流进行解密。参考:自定义ClassLoader对Class加密并解密
2.解决同名class不同版本共存问题(钻石引用)
参考:老大难的 Java ClassLoader,到了该彻底理解它的时候了
2.2runtime data area
JVM在运行时将数据划分为了5个区域来存储:heap, java stack, native method stack, PC register, method area。
1、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以是看作当前线程所执行的字节码的行号指示器。说简单一点就是一个计数器,当字节码解释器工作是能够通过改变这个计数器的值来选取下一条需要执行的字节码指令。在说明一点,各条线程之间计数器互不影响,独立存储,程序计数器器内存区域为 线程私有的。
2.本地方法栈
本地方法栈和虚拟机栈所发挥的作用是很相似的,它们之间的区别不过是 虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。Sun HotSpot 直接就把本地方法栈和虚拟机栈合二为一。本地方法栈也会抛出
StackOverflowError
和OutOfMemoryError
异常。3、Java虚拟机栈
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也是常说的栈。Java栈是Java方法执行的内存模型。Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。栈也是线程私有的。
4、Java堆
堆是jvm内存管理的最大的一块区域,此内存区域的唯一目的就是存放对象的实例,所有对象实例与数组都要在堆上分配内存。它也是垃圾收集器的主要管理区域。java对可以处于物理上不连续的空间,只要逻辑上是连续的即可。线程共享的区域。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出OutOfMemoryError异常。
1).Young (新生代)
新生代 分为三部分。Eden区(new 的对象)和两个大小相同的Survivior区(某一时刻,只有一个被使用),另外一个,当Eden区满了,GC就会将存活的对象移动到空闲的Survivor区,根据JVM的策略,在经过几次垃圾收集后,依然存活在Survivor区的对象,将移动到Tenured区(老年代)
2).Tenured(老年代)
老年代 主要保存生命周期长的对象。(new 的大对象,会直接进入老年代)
5、方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。方法区是堆的一个逻辑部分,为了区分Java堆,它还有一个别名Non-Heap(非堆)。相对而言,GC对于这个区域的收集是很少出现的。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。在Java 7及之前版本,我们也习惯称方法区它为“永久代”(Permanent Generation),更确切来说,应该是“HotSpot使用永久代实现了方法区”!
6、运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
7、直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域。但这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即直接堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。
详情参考:Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)
3 GC
GC动作发生之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。
1、引用计数法
在对象上添加一个引用计数器,每当有一个对象引用它时,计数器加1,当使用完该对象时,计数器减1,计数器值为0的对象表示不可能再被使用。引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。
2、可达性分析法
通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链”,以下对象可作为GC Roots:
- 本地变量表中引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
-
Native方法引用的对象
当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。
在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程:
1)如果对象objA到 GC Roots没有引用链,则进行第一次标记。
2)如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA会被移出“即将回收”集合。