JVM简介
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设施的标准,它是一个虚构进去的计算机,是通过在理论的计算机上仿真模仿各种计算机性能来实现的。Java虚拟机包含一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储办法域。 JVM屏蔽了与具体操作系统平台相干的信息,使Java程序只需生成在Java虚拟机上运行的指标代码(字节码),就能够在多种平台上不加批改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
Java语言的一个十分重要的特点就是与平台的无关性。而应用Java虚拟机是实现这一特点的要害。个别的高级语言如果要在不同的平台上运行,至多须要编译成不同的指标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不须要从新编译。Java语言应用Java虚拟机屏蔽了与具体平台相干的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的指标代码(字节码),就能够在多种平台上不加批改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的可能“一次编译,到处运行”的起因。
内存构造概述
类加载子系统(Class Loader)
类加载器分为:自定义类加载器 < 零碎类加载器 < 扩大类加载器 < 疏导类加载器
类加载过程分为:加载、链接、验证、初始化。
程序计数器(Program Counter Register)
是一块较小的内存空间,能够看作是以后线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。
虚拟机栈 (Stack Area)
栈是线程公有,栈帧是栈的元素。每个办法在执行时都会创立一个栈帧。栈帧中存储了局部变量表、操作数栈、动静连贯和办法进口等信息。每个办法从调用到运行完结的过程,就对应着一个栈帧在栈中压栈到出栈的过程。
本地办法栈 (Native Method Area)
JVM 中的栈包含 Java 虚拟机栈和本地办法栈,两者的区别就是,Java 虚拟机栈为 JVM 执行 Java 办法服务,本地办法栈则为 JVM 应用到的 Native 办法服务。
堆 (Heap Area)
堆是Java虚拟机所治理的内存中最大的一块存储区域。堆内存被所有线程共享。次要寄存应用new关键字创立的对象。所有对象实例以及数组都要在堆上调配。垃圾收集器就是依据GC算法,收集堆上对象所占用的内存空间。
Java堆分为年老代(Young Generation)和老年代(Old Generation);年老代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。
办法区(Method Area)
办法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码。更具体的说,动态变量+常量+类信息(版本、办法、字段等)+运行时常量池存在办法区中。常量池是办法区的一部分。
JDK 8 应用元空间 MeteSpace 代替办法区,元空间并不在JVM中,而是在本地内存中
类加载过程概述
类加载器子系统负责从文件系统或者网络中在家Class文件,class文件在文件结尾又特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否能够运行,则由ExecutionEngine
决定。
加载类的信息寄存于一块被称为办法区的内存空间。除了类的信息外,办法区中还会寄存运行时常量池信息,可能还包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池局部的内存映射)
类加载器ClassLoader角色
- class文件存在本地硬盘上,在执行时加载到JVM中,依据这个文件能够实例化出n个截然不同的实例。
- class文件加载到JVM中,被称为DNA元数据模板,放在办法区中。
- 在.class文件 -> JVM -> 最终成为元数据模板的过程中,ClassLoader就表演一个快递员的角色。
类的加载过程
类的加载过程大抵分为三个阶段:加载,链接,初始化。
类的加载过程一:加载(Loading)
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为办法区这个类的各种数据的拜访入口
类的加载过程二:链接(Linking)
验证(Verify)
- 目标在于确保Class文件的字节流中蕴含信息合乎以后虚拟机要求,保障被家再来的正确性,不会危害到虚拟机的平安。
筹备(Prepare)
筹备阶段是进行内存调配。为类变量也就是类中由static润饰的变量分配内存,并且设置初始值,这里要留神,初始值是默认初始值0、null、0.0、false等
,而不是代码中设置的具体值,代码中设置的值是在初始化阶段
实现的。另外这里也不蕴含用final润饰的动态变量,因为final在编译的时候就会调配了。这里不会为实例变量调配初始化,类变量会调配在办法区中,而实例对象会随着对象一起调配到Java堆中。
public class HelloApp { private static int a = 1; // 筹备阶段为0,而不是1 public static void main(String[] args) { System.out.println(a); }}
解析(Resolve)
解析次要是解析字段、接口、办法。次要是将常量池中的符号援用替换为间接援用的过程。间接援用就是间接指向指标的指针、绝对偏移量等。
类的加载过程三:初始化(initialization)
- 初始化阶段就是执行类结构器办法
<clinit>()
的过程 - 此办法不须要定义,是javac编译期主动收集类中所有类变量的赋值动作和动态代码块中的语句合并而来
- 结构器办法中指令按语句在源文件中呈现的程序执行。
<clinit>()
不同于类的结构器(结构器是虚拟机视角下的<init>()
)- 若该类具备父类,JVM会保障子类的
<clinit>()
执行前,父类的<clinit>()
曾经执行结束 - 虚拟机必须保障一个类的
<clinit>()
办法在多线程下被同步加锁
须要留神,如果没有定义动态变量或动态代码块的话则没有<clinit>()
案例如下:
public class HelloApp { static { code = 20; } private static int code = 10; //第一步:在筹备阶段初始化了code默认值为0。 //第二步:依据类执行程序先执行动态代码块,赋值为20. //第三步:最初赋值为10,输入后果为10. public static void main(String[] args) { System.out.println(code); // 10 }}
通过字节码文件能够很分明的看到后果:
0 bipush 20 2 putstatic #3 <com/jvm/HelloApp.code> 5 bipush 10 7 putstatic #3 <com/jvm/HelloApp.code>10 return
先被赋值为20,而后改为10。
类加载器概述
JVM反对两种类型的类加载器,别离为疏导类加载器(Bootstrap ClassLoader)
和 自定义类加载器(User-Defined ClassLoader)
从概念上讲,自定义类加载器个别指的是程序中由开发人员自定义的一类类加载器,然而Java虚拟机是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
无论怎么划分,在程序中最常见的类加载器始终只有三个:
零碎类加载器(System Class Loader) < 扩大类加载器(Extension Class Loader) < 疏导类加载器(Bootstrap Class Loader)
它们之间的关系不是继承关系,而是level关系。
零碎类加载器和扩大类加载器间接或间接继承ClassLoader。划线分为两大类。
public class HelloApp { public static void main(String[] args) { //获取零碎类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //获取其下层:扩大类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@60e53b93 //获取其下层:获取不到疏导类加载器 ClassLoader bootStrapLoader = extClassLoader.getParent(); System.out.println(bootStrapLoader);//null //咱们本人定义的类是由什么类加载器加载的:应用零碎类加载器 ClassLoader classLoader = HelloApp.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //看看String是由什么类加载器加载的:应用疏导类加载器 ClassLoader classLoaderString = String.class.getClassLoader(); System.out.println(classLoaderString);//null }}
疏导类加载器(Bootstrap ClassLoader)
- 这个类加载应用c/c++语言实现,嵌套在JVM外部。
- 他用来加载Java的外围库,(JAVA_HOME/jre/lib/rt.jar、resources.jar、或sun.boot.class.path门路下的内容),用于提供JVM本身须要的类。
- 并不继承自java.lang.ClassLoader ,没有父加载器。
- 加载扩大类和应用程序类加载器,并指定为他们的父类加载器。
- 出于平安思考,Bootstrap启动类加载器只加载包名为java、javax、sun等结尾的类。
扩大类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类。
- 上一层类加载器为启动类加载器。
- 从java.ext.dirs零碎属性所指定的指标中加载类库,或从JDK的装置目录jre/lib/ext子目录(扩大目录)下加载类库。
如果用户创立的Jar放在此目录下,也会主动由扩大类加载器加载。
零碎类加载器(System ClassLoader)
- Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
- 派生于ClassLoader类。
- 上一层类加载器为扩大类加载器。
- 它负责加载环境变量classpath或零碎属性 java.class.path指定下的类库。
该类加载是程序中默认的类加载器
,一般来说,Java利用的类都有由它来实现加载。- 通过ClassLoader.getSystemClassLoader()办法能够获取该类加载器。
为什么须要用户自定义类加载器?
- 隔离加载类
- 批改类加载的形式
- 扩大加载源
- 避免源码泄露
用户自定义类加载器实现步骤
- 通过集成抽象类java.lang.ClassLoader类的形式,实现本人的类加载器。
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()办法,从而实现自定义的类加载器,然而在JDK1.2之后不再倡议用户去笼罩loadClass()办法,而是倡议把自定义类加载逻辑写在findClass()办法中。
- 在编写自定义类加载器时,如果没有太过于简单的需要,能够间接继承URLClassLoader类,这样就能够防止本人编写findClass()办法其获取字节码流的形式,使得自定义加载器编写更为简洁。
对于ClassLoader
ClassLoader是一个抽象类,零碎类加载器和扩大类加载器间接或间接继承ClassLoader。
罕用办法如下:
办法名称 | 形容 |
---|---|
getParent() | 返回该类加载器的上一级类加载器 |
loadClass(String name) | 加载名称为name的类,返回后果为java.lang.Class的实例 |
findClass(String name) | 查找名称为name的类,返回后果为java.lang.Class的实例 |
findLoadedClass(String name) | 查找名称为name的曾经被加载过的类,返回后果为java.lang.Class类的实例 |
defineClass(String name,byte[] b,int off,int len) | 把字节数组b中的内容转换为一个java类,返回后果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连贯指定的一个java类 |
双亲委派机制
Java虚拟机对class文件采纳的是按需加载
的形式,也就是说须要应用该类的时候才会将它的class文件加载到内存生成class对象。
当某个类加载器须要加载某个.class文件时,它首先把这个工作委托给他的下级类加载器,递归这个操作,如果下级的类加载器没有加载,本人才会去加载这个类。
工作原理
- 如果一个类加载器收到了类加载申请,他并不会本人先去加载,而是把这个申请向上委托给上一级类加载器去执行。
- 如果上一级类加载器还存在上一级,则进一步向上委托,顺次递归,申请最终会达到疏导类加载器。
- 如果疏导类加载器能够实现类加载工作,就胜利返回。如果无奈实现类加载工作,会顺次往下再去执行加载工作。这就是双亲委派机制。
比方咱们当初在本人我的项目中创立一个包名java.lang下创立一个String类。
package java.lang;public class String { static { System.out.println("我是本人创立的String"); }}
public class HelloApp { public static void main(String[] args) { String s = new String(); }}
执行之后并不会输入咱们的语句,因为咱们的String类加载器一开始由零碎类加载器一级一级往上委托,最终交给疏导类加载器,疏导类加载器一看是java.lang包下的,ok,我来执行,最终执行的并不是咱们本人创立String类,保障了外围api无奈被纂改,防止类的反复加载。
package java.lang;public class String { static { System.out.println("我是本人创立的String"); } public static void main(String[] args) { System.out.println("Hello World !!!"); }}
如果咱们想运行如上代码,咱们会失去如下谬误:
谬误: 在类 java.lang.String 中找不到 main 办法, 请将 main 办法定义为: public static void main(String[] args)否则 JavaFX 应用程序类必须扩大javafx.application.Application
因为咱们晓得在外围api String类中是没有main办法的,所以咱们能够确定加载的并不是咱们本人创立的String类。
在JVM中示意两个Class对象是否为同一个类存在的必要条件:
- 类的残缺类名必须统一,包含包名。
- 加载这个类的ClassLoader也必须雷同。
顺便说一句,咱们包名如果为java.lang则会报错。