Java虚拟机学习

时间:2022-04-20 10:44:37

JDK:用于支持程序开发的最小环境,Java程序设计语言,Java虚拟机以及JavaAPI类库的统称。

JRE:支持Java程序运行的标准环境,JavaSE标准子集和Java虚拟机统称。

Java虚拟机

Java虚拟机学习

程序计数器(Program Counter Register)

一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。每条线程都有一个独立的程序计数器,各线程间互不影响,独立存储。

无异常情况。

Java虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期与线程相同。

描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接和方法出口等信息。每一个方法从执行到完成都对应着一个栈帧在Java虚拟机栈中从入栈到出栈的过程。其中局部变量表存放了编译器已知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。

异常情况-----*Error:线程请求栈深度大于虚拟机所允许的深度

              -----OutofMemoryError:虚拟机栈动态扩展时无法申请到足够的内存

本地方法栈(Native Method Stack)

与虚拟机栈基本相同,只是作用在Native方法上。

异常情况:-----*Error:线程请求栈深度大于虚拟机所允许的深度

                  -----OutofMemoryError:虚拟机栈动态扩展时无法申请到足够的内存

Java堆(Java Heap)

虚拟机启动时创建,被所有线程共享。

Java虚拟机管理内存中最大的一块,目的是存放对象实例,几乎所有的对象实例存在于此。

Java堆是垃圾收集的主要区域,也被称作GC堆。

垃圾收集角度可分为:新生代(Eden空间、From Survivor空间和To Survivor空间)和老年代

异常情况:内存不足并且无法扩展时,抛出OutOfMemoryError异常

方法区(Method Area)

线程共享的内存区域

用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

异常情况:当方法区无法满足内存分配时,抛出OutOfMemoryError异常

 运行时常量池(Runtime Constant Pool)

方法区的一部分,用于存放编译器生成的各种字面量和符号引用这部分内容将在类加载后进入方法区的运行时常量池中存放。

异常情况:常量池无法申请到内存时会出现OutOfMemoryError异常

 

 垃圾收集(Garbage Collection,GC)

对象存活判断

1.引用技术算法(Reference Counting)

给对象添加一个引用计数器,当有地方引用它时就加1;引用失效时减1。当计数器为0时,表示对象不再被引用。

缺点:无法解决对象之间相互循环引用的问题。

2.可达性分析算法(Reachability Analysis)

以“GC Roots”对象作为起点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC roots没有任何引用链相连时,则证明此对象不可用。

Java虚拟机学习

GC Roots对象:

  虚拟机栈(栈帧中的本地变量表)中引用的对象

  方法区中类静态属性引用的对象

  方法区中常量引用的对象

  本地方法栈中JNI(即Native方法)引用的对象

谈引用

JDK1.2之后,Java将引用分为四种:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

强引用:普遍存在,代码中new出来的对象,类似Object obj = new Object()。只要强引用存在,GC永远不会回收掉被引用的对象。

软引用:用来描述一些还有用但非必需的对象,SoftReference类来实现。对于软引用关联的对象,会在将要发生内存溢出之前进行回收,如果还没有足够内存,才会抛内存溢出异常。

弱引用:描述非必需对象,强度比软引用更弱,WeakReference类来实现。对于弱引用关联的对象,GC工作时,无论当前内存是否足够,都会被回收掉。

虚引用:最弱的一种引用关系,PhantomReference类来实现。无法通过虚引用来获取对象实例,设置虚引用关联的唯一目的是能在这个对象被回收时收到一个系统通知。

Live or die?

可达性分析算法中不可达的对象,并非一定会被回收。

宣告对象死亡,至少要经历两次标记过程:可达性分析后没有与GC Roots相连的引用链,会被第一次标记并且进行一次筛选(此对象是否有必要执行finalize方法),如果对象没有覆盖finalize()方法,或者该方法已被虚拟机调用过,虚拟机就视为没有必要执行。

finalize()方法是对象逃脱死亡的最后机会,对象可以重新与引用链上的对象建立关联,下次标记时就会被移除出即将回收的集合。

任何一个对象的finalize()方法只会被系统自动调用一次。

 

 虚拟机类加载机制

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

类的生命周期

Java虚拟机学习

整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

其中加载、验证、准备、初始化和卸载五个阶段的顺序确定,解析阶段在某些情况下可能会在初始化阶段之后执行,比如动态绑定或者晚期绑定。

类加载的过程

加载、验证、准备、解析、初始化

加载(loading)

通过一个类的全限定名来获取定义此类的二进制流。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的class对象,作为方法区这个类的各种数据的访问入口。

验证(verification)

 本阶段为了确保class文件中包含的字节流信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。

包含文件格式验证、元数据验证、字节码验证和符合引用验证。

文件格式验证

验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理。

元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

字节码验证

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证

对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

准备(preparation)

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

此时进行内存分配的仅包括static修饰的类变量,初始值也只是零值。

但是对于Constant Value属性(final修饰的类变量),则直接赋值为设定的值。

例:public static final int value = 123,则赋值为123。

解析(resolution)

虚拟机将常量池的符号引用转换为直接引用的过程。

符号引用

以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。

直接引用

可以是直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化(initialization)

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序设置的计划去初始化类变量和其他资源。

类加载器-双亲委派模型(Parents Delegaion Model)

从Java虚拟机角度,只存在两种不同的类加载器

启动类加载器(Bootstrap ClassLoader)--虚拟机自身的一部分。

其他类加载器--独立于虚拟机外部,并且继承自抽象类java.lang.ClassLoader。

启动类加载器

负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(例如rt.jar)类库加载到虚拟机内存中。

扩展类加载器

由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的类库。

应用程序类加载器

由sun.misc.Launcher$AppClassLoader实现,由于是ClassLoader中的getSystemClassLoader()方法的返回值,一般也称为系统类加载器。

负责加载用户类路径(classpath)上所指定的类库。

自定义类加载器

 Java虚拟机学习

上图中的层次关系即为类加载器的双亲委派模型,要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

双亲委派模型工作过程

如果一个类加载器收到了类加载的请求,会把这个请求委托给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈无法完成该加载请求时,子加载器才会尝试去加载。

代码实现如下:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException异常
// 说明父类加载器无法完成加载请求

}

if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法进行类加载

long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

 OSGI模块化热部署

每一个程序模块(OSGI中的Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

OSGI类搜索顺序如下:

1.将以java.*开头的类委派给父类加载器加载

2.将委派列表名单内的类委派给父类加载器加载

3.将Import列表中的类委派给Export这个类的Bundle的类加载器加载

4.查找当前Bundle的classpath,使用自己的类加载器加载

5.查找类是否在自己的Fragment Bundle中,如果在,则委托给Fragment Bundle的类加载器加载

6.查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载

7.否则,类查找失败