一、java文件运行的全部过程是怎么样的呢?
当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader
java虚拟机的生命周期:声明周期起点是当一个java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。
java的虚拟机种有两种线程,一种叫叫守护线程,一种叫非守护线程(也叫普通线程),main函数就是个非守护线程,虚拟机的gc就是一个守护线程。java的虚拟机中,只要有任何非守护线程还没有结束,java虚拟机的实例都不会退出,所以即使main函数这个非守护线程退出,但是由于在main函数中启动的匿名线程也是非守护线程,它还没有结束,所以jvm没办法退出
java源文件到最后运行起来,主要会经过两个阶段:
1、.java文件通过java编译器(javac.exe)编译成.class文件,这个就是所说的字节码文件。
2、.class文件加载到jvm进行运行
首先,当一个程序启动之前,它的class会被类装载器装入方法区(不好听,其实这个区我喜欢叫做Permanent区),执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统APIi等等。
jvm内部执行运行流程图:
JVM运行简易过程:
上图左半部分其实不是在JVM中,程序员在eclipse上写的是.java文件,经过编译成.class文件(比如maven工程需要maven install,打成jar报,jar包里面都是.calss文件);这些步骤都是在eclipse上进行的。然后类加载器(classloader)一直到解释器是属于JVM的
二、JVM是如何来处理.class文件的
首先来分析下JVM到底是个怎么样的东西
JVM被分为三个主要的子系统
(1)类加载器子系统(2)运行时数据区(3)执行引擎
1. 类加载器子系统
加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
•加载
jvm系列(一):java类的加载机制 - 纯洁的微笑 - 博客园www.cnblogs.com
加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
① 通过一个类的全限定名来获取其定义的二进制字节流。
② 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
③ 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
•连接
1)验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2)准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
① 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
② 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行
3)解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 初始化
类初始化阶段是类加载的最后一步,前面的类加载过程中,除了在加载阶段用户用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说字节码)。这里所有的静态变量会被赋初始值, 并且静态块将被执行。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻初始化:
① 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类;
② 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化;
③ 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化;
④ 虚拟机启动时,用户会先初始化要执行的主类(含有main);
⑤ jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化;
2、运行时数据
运行时数据区域被划分为5个主要组件:
① 方法区 (线程共享) 常量 静态变量 JIT(即时编译器)编译后代码也在方法区存放
② 堆内存(线程共享) 垃圾回收的主要场地
③ 程序计数器 当前线程执行的字节码的位置指示器
④ Java虚拟机栈(栈内存) :保存局部变量,基本数据类型以及堆内存中对象的引用变量
⑤ 本地方法栈 (C栈):为JVM提供使用native方法的服务
3、执行引擎
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
① 解释器: 解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。
② 即时(Just-In-Time)编译器:JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。
简单理解JIT就是当代码中某些方法复用次数比较高的,并超过一个特定的值就成为了“热点代码”。那么这个这些热点代码就会被编译成本地代码(其实可以理解成缓存)加快访问速度。