一、JVM的定义

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

二、类加载子系统

包含三个部分:加载—>链接—>初始化

2.2作用

类加载子系统负责从文件系统或者网络中加载Class文件(Class文件在开头有特定标识)。

类加载器(Class Loader)只负责class文件的加载,至于是否可以运行,由执行引擎(Execution Engine)决定。

加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

2.3类加载过程

2.3.1加载过程

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

2.3.2链接过程

验证(Verify)

目的:在于确保Class文件的字节流中包含信息符合当前JVM规范要求,保证被加载类的正确性,不会危害虚拟机自身安全。

主要包括四种验证:

文件格式验证、源数据验证、字节码验证、符号引用验证

这里就不展开讲了相关信息可以自行 百度

准备(Prepare)

为类变量(static)分配内存并且设置初始值。

这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;

不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中。

解析(Resolve)

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

事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行

2.3.3初始化

初始化阶段就是执行类构造器方法clInit()的过程。 clInit是ClassInit缩写。此方法并不是程序员定义的构造方法。是javac编译器自动收集类中的所有类变量(Static)的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在源文件中出现的顺序执行若该类具有父类,jvm会保证子类的clinit()执行前,父类的clinit()已经执行完毕

2.4类加载器的分类

启动类加载器

负责加载JAVA_HOME/lib目录下的可以被虚拟机识别(通过文件名称,比如rt.jar``tools.jar)的字节码文件。与之对应的是java.lang.ClassLoader类

扩展类加载器

负责加载JAVA_HOME/lib/ext目录下的的字节码文件。对应sun.misc.Launcher类 此类继承于启动类加载器ClassLoader

应用程序类加载器

负责加载ClassPath路径下的字节码 也就是用户自己写的类。对应于sun.misc.Launcher.AppClassLoader类 此类继承于扩展类加载器Launcher

用户自定义加载器

需要继承系统类加载器ClassLoader,并重写findClass方法。负责加载指定位置的字节码文件。通过类中的path变量指定。

2.5双亲委派机制

原理:

如果一个类加载器收到了类加载的请求,它并不会自己加载,而是先把请求委托给父类的加载器执行

如果父类加载器还有父类,则进一步向上委托,依次递归,请求到达最顶层的引导类加载器。

如果顶层类的加载器加载成功,则成功返回。如果失败,则子加载器会尝试加载。直到加载成功。

双亲委派机制优势

避免类的重复加载

当自己程序中定义了一个和Java.lang包同名的类,此时,由于使用的是双亲委派机制,会由启动类加载器去加载JAVA_HOME/lib中的类,而不是加载用户自定义的类。此时,程序可以正常编译,但是自己定义的类无法被加载运行。

保护程序安全,防止核心API被随意篡改

双亲委派模型的破坏者-线程上下文类加载器

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。

2.6沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

  

组成沙箱的基本组件

字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用

它防止恶意代码去干涉善意的代码;

它守护了被信任的类库边界;

它将代码归入保护域,确定了代码可以进行哪些操作。