Java如何运行 深入了解Java程序java的运行机制是怎样的?

大家都知道,Java中JVM的重要性,学习了JVM你对Java的运行机制、编译过程和如何对Java程序进行调优相信都会有一个很好的认知。废话不多说,直接带大家来初步认识一下JVM。什么是JVM?JVM(Java Virtual Machine)是一个抽象的计算机,和实际的计算机一样,它具有指令集并使用不同的存储区域,它负责执行指令,还要管理数据、内存和寄存器。看到这里,可能不懂JVM的人,已经蒙圈了。没关系,下面让我详细为大家介绍JVM的体系架构图,或许你会明白些。简单来说,JVM就是一个虚拟计算机。我们都知道Java语言其中的一个特性就是跨平台的,而JVM就是Java程序实现跨平台的关键部分。Java编译器编译Java程序时,生成的是与平台无关的字节码(也就是*.class文件),所谓的平台无关是指编译生成的字节码无论是在Window、Linux、Mac系统都是可执行。也就是说Java编译生成的*.class文件不是面向平台的,而是面向JVM的。不同平台上的JVM都是不同的,但是他们都是提供了相同的接口。图一为Java的大致运行步骤:图一引用一个《疯狂Java讲义》中提到例子来帮助大家理解JVM的作用:JVM的作用就像有两只不同的铅笔,但需要把同一个笔帽套在两支不同的笔上,只有为这两支笔分别提供一个转换器,这个转换器向上的接口相同,用于适应同一个笔帽;向下的接口不同,用于适应两支不同的笔。在这个类比中,可以近似地理解两支不同的笔就是不同的操作系统,而同一个笔帽就是Java字节码程序,转换器角色则对应JVM。类似地,也可以认为JVM分为向上和向下两个部分,所有平台的JVM向上提供给Java字节码程序的接口完全相同,但向下适应的不同平台的接口则互不相同。JVM体系结构概览上面我们是初步介绍了JVM的作用,那么要深入去了解JVM我们就需要了解JVM的体系结构,请看图二:图二图二是JVM的体系架构图,接下让我们一起来聊一聊每一个部分都是什么意思。1.类装载器子系统(ClassLoader)负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。Java编译生成的*.class文件就是通过ClassLoader进行加载的,那么这里就会有几个问题:ClassLoader如何知道*.class文件就是需要加载的文件?如果我手动将一个普通文件的扩展名称改为class后缀,ClassLoader会加载这个文件吗?实际上,class文件在文件的开头是有特定的文件标识的,随便编写一个Java程序,编译生成一个class文件,打开后你都能看到如下内容:cafe babe就是class文件的一个标识,ClassLoader负责加载有cafe babe的class文件,它将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时的数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定,请看图三:图三Car.class文件通过ClassLoader进行加载到内存中,Car Class在内存中就相当一个模板,我们可以通过这个模板可以实例化成不同的实例car1、car2、car3。不知大家会不会有一个疑问,ClassLoader加载Car.class在Java中是用什么类型的加载器加载的呢?在解答这个问题前我们先写个简单的代码看看://new一个Car对象
Car car = new Car();
//得到ClassLoader
ClassLoader classLoader = car.getClass().getClassLoader();
//打印结果
System.out.println(classLoader);结果为:我们再来看看另外一组代码://new两个不同的对象
Car car = new Car();
String string = new String();
//得到ClassLoader
ClassLoader classLoader1 = car.getClass().getClassLoader();
ClassLoader classLoader2 = string.getClass().getClassLoader();
//打印结果
System.out.println(classLoader1);
System.out.println(classLoader2);结果为:从上面我们可以知道,ClassLoader的打印结果一个是“sun.misc.Launcher$AppClassLoader@18b4aac2”,一个则是“null”,这是怎么回事呢,细心的朋友就可以发现这两个不同的对象中,其中car对象是我们自己写的一个类,string对象是系统自带的一个类。简单来说就是ClassLoader会根据不同的类选择不同的类加载器去进行加载。这里就牵扯到了ClassLoader的分类ClassLoader的类别:启动类加载器(BootStrap)扩展类加载器(Extension)应用程序类加载器(AppClassLoader)用户自定义加载器一般我们自己所写的类用的类加载器都是AppClassLoader,就是上图所示的“sun.misc.Launcher$AppClassLoader@18b4aac2”,而为什么string这个对象是”null“呢?实际上,这个“null”指的就是使用BootStrap这个加载器。那可能有人有疑问,自己定义的类用AppClassLoader,能理解,因为car这个对象输出的类加载器名字中有AppClassLoader这个字样,但是为什么string这个对象是”null“,从哪里可用体现是用BootStrap这个加载器呢?是这样的,BootStrap类加载器相当于扩展类加载器、应用程序类加载器的祖宗,若是用了BootStrap,由于BootStrap上一级已经没有了,所以就用“null”来表示其实我们可以找一下String这个类在JDK的位置:$JAVA_HOME/jre/lib/rt.jar/java/lang所有在这个路径$JAVA_HOME/jre/lib/rt.jar这个jar包下的类都是用BootStrap来加载的。下面请看图4:图四这张图就可以很清晰得看到:1.所有在$Java_Home/jre/lib/rt.jar是通过BootStrap加载的2.所有在$Java_Home/jre/lib/ext/*.jar是通过Extension加载的3.所有在$CLASSPATH是通过SYSTEM加载的(应用程序类加载器也叫系统类加载器,加载当前应用的classpath的所有类)接下来我们再来看一个例子:如果创建一个java.lang包,然后创建String类,打印一句话执行会怎么样呢?package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("Hello World");
}
}效果如下:可以看到程序报错了,说是找不到main方法,可是明明就有main方法为什么没有执行呢?这里就涉及了双亲委派机制双亲委派机制:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。所以它实际的运行过程是这样的:ClassLoader收到String类的加载请求。先去Bootstrap查找是否有这个类,没有则反馈无法完成这个请求,但是恰好,在rt.jar中找到了java.lang.Stirng这个类执行这个类,这个类是没有定义main方法的报错,类中没有定义main方法所以上面的例子,他会找到jdk中java.lang.String这个类,这个类确实是没有定义main方法,简单来说它执行的类是JDK中java.lang.String这个类,而不是我们自己定义的类。那用双亲委派机制有什么好处呢:采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。2.执行引擎(Execution Engine)执行引擎负责解释命令,提交给操作系统执行,这里对执行引擎就不做过多的解释了,只要知道他是负责解释命令的即可。3.本地方法接口(Native Interface)和本地方法栈(Native Method Stack)本地接口:本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。   目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用    Socket通信,也可以使用Web Service等等,不多做介绍。如果在程序中有见到native关键字,就代表不是Java能完成的事情了,需要加载本地方法库才能完成本地方法栈:它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。说白了就是本地方法由本地方法栈来登记,Java中的方法由Java栈来登记。4.PC寄存器(Program Counter Register)每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果执行的是一个Native方法,那这个计数器是空的。PC寄存器用来完成分支、循环、跳转、异常处理、线程恢复等基础功能。由于使用的内存较小,所以不会发生内存溢出(OutOfMemory)错误。}
在 Java 中,类加载机制是 Java 虚拟机(JVM)的一个重要组成部分,它负责在运行时将 Java 类加载到内存中,并转换为可执行代码。理解类加载机制对于深入理解 Java 的运行机制和开发高质量的Java应用程序至关重要。本文将深入探讨 Java 的类加载过程以及双亲委派模型。首先,我将详细介绍类加载过程的五个阶段。接下来,将重点介绍双亲委派模型以及它解决的问题。1.1 加载(Loading)加载是类加载的第一个阶段。在这个阶段,JVM 会根据类的全限定名(Fully Qualified Name)找到对应的字节码文件,并将其加载到内存中。加载阶段不仅仅包括从文件系统中读取字节码,还可能包括从网络、数据库等地方加载类的字节码。详细说来,类的加载阶段需要完成三个任务: 获取字节流:Java虚拟机根据类的全限定名(Fully Qualified Name)来获取定义该类的二进制字节流。这个过程可以通过从本地文件系统、网络、JAR包等位置读取类的字节码文件,并将其存储在内存中。
转化为运行时数据结构:在获取类的字节流后,Java虚拟机将这个字节流所代表的静态存储结构(如类、字段、方法、常量池等)转化为方法区的运行时数据结构。这个过程包括解析字节码中的各种信息,并生成对应的运行时数据结构,用于在运行时执行类的各种操作。
生成java.lang.Class对象:在内存中生成一个代表这个类的java.lang.Class对象。这个Class对象是在JVM中表示类的元数据信息的对象,通过这个对象可以访问类的方法、字段、构造函数等信息,以及执行类的各种操作。这个Class对象也是Java程序中获取类的入口,通过它可以访问类的各种静态和动态信息。
注意:“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 (Class Loading) 是不同的,一个是加载 (Loading)另一个是类加载 (Class Loading),不要把二者混淆。
1.2 验证(Verification)验证是类加载过程的第二个阶段。在验证阶段,JVM 会对加载的字节码进行验证,确保字节码的结构是合法的、符合规范的,不包含安全漏洞和不符合 JVM 规范的内容。这个阶段是确保类加载过程的安全性和正确性的重要步骤。下图是 Java 虚拟机规范中的 Class 文件的结构定义,同时也是验证阶段所需要验证的:1.3 准备(Preparation)准备是类加载过程的第三个阶段。在准备阶段,JVM会为类的静态变量分配内存,并设置初始值(通常是零值)。这里并不包括对静态变量赋值的操作,赋值的操作将在初始化阶段进行。例如有这样一段代码:public static int value = 100;
在准备阶段,JVM 会给静态变量 value 分配内存,但是设置的初值是 0,而不是 100,因为赋值操作是在初始化阶段完成的。1.4 解析(Resolution)解析是类加载过程的第四个阶段。在解析阶段,JVM 会将符号引用替换为直接引用。符号引用是一种在编译期产生的,用于描述类、字段、方法等的引用。直接引用是指直接指向内存中的地址的引用。解析阶段的目的是将符号引用解析成直接引用,以便在之后的程序执行中更快速地定位到所引用的目标。1.5 初始化(Initialization)初始化是类加载过程的最后一个阶段。在初始化阶段,JVM会执行类的初始化代码,包括对静态变量赋值和执行静态初始化块。类的初始化是在首次使用类的时候触发的,只有在初始化完成后,类才算是被真正加载和准备好了。2.1 类加载器一提到 JVM 的类加载机制,不由自主的就会想到 双亲委派模型,而要理解这个模型,首先就需要了解类加载器。在 JVM 中,默认有三种类加载器: 启动类加载器(Bootstrap Class Loader)
这是JVM内部实现的特殊类加载器,由 C++ 编写,而不是 Java 类;它负责加载 JVM 自身需要的类,包括核心类库(如java.lang包中的类)等;启动类加载器是类加载器层次结构的最顶层,没有父加载器;由于其是C++编写的,因此在Java代码中无法直接引用它。 扩展类加载器(Extension Class Loader) 扩展类加载器是 Java 类,由sun.misc.Launcher$ExtClassLoader实现;它负责加载 JRE(Java Runtime Environment)的扩展目录 lib/ext中的类;扩展类加载器是启动类加载器的子加载器;同时也是类加载器层次结构中的中间层。应用程序类加载器(Application Class Loader)应用程序类加载器也是 Java 类,由sun.misc.Launcher$AppClassLoader实现;它负责加载应用程序 classpath下的类,即我们自己编写的 Java 类;应用程序类加载器是扩展类加载器的子加载器;同时处于类加载器层次结构中的最底层。以上三种类加载器构造了 JVM 的类加载器层次结构,即双亲委派模型。除了启动类加载器是由 C++ 语言实现外,其他的所有加载器均由 Java 实现,并且都继承了java.lang.ClassLoader。下图展示了 JVM 的类加载器的层次结构:当然,除了默认的这三个类加载器外,开发人员还可以根据自己的需求实现自定义的类加载器。自定义类加载器需要继承自java.lang.ClassLoader,通过重写findClass方法来实现特定的类加载逻辑。自定义的类加载器常用于实现插件机制,热部署等功能。2.2 什么是双亲委派模型双亲委派模型是 Java 类加载器的一种类加载机制,简单来说,核心思想就是:一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。双亲委派模型的类加载过程大致如下图所示:当一个类加载器收到加载请求的时候(子加载器),它首先会把这个加载请求委派给其父加载器,父加载器可能会继续委托给其父加载器,依次递归。到达类加载器层次结构的最顶层(启动类加载器)后,再尝试进行类加载。如果当前加载器无法加载时,就会将加载任务交给其子加载器,由子加载器尝试进行类加载。2.3 双亲委派模型的解决的问题双亲委派模型主要解决了两个问题:类的隔离和防止冲突在复杂的 Java 应用程序中,可能会涉及许多不同的类库和模块。这些类库和模块可能引用了相同的类名,如果不加以限制,可能就会导致类名冲突。双亲委派模型通过层级结构的类加载器,确保每个类加载器都将加载任务优先委派给父加载器进行加载,这样同名的类只会被加载一次,并且加载过程是有序的,避免了类的冲突和混淆。
举个例子:
假设应用程序的类加载器需要加载 java.lang.String 类。它首先会委派给扩展类加载器,扩展类加载器也找不到,再委派给启动类加载器。因为启动类加载器能够找到并加载 java.lang.String 类,所以它会将该类返回给应用程序类加载器。由于类加载器的委派顺序,即便应用程序中有自定义的 java.lang.String 类,也不会被加载,从而保证了类的隔离和防止冲突。安全性和可信任代码的执行Java中的核心类库位于JVM内部,它们提供了Java编程语言的基本功能。这些核心类库在 JVM 启动时由启动类加载器加载。通过双亲委派模型,可以确保核心类库只会被启动类加载器加载,而不会被应用程序类加载器或其他自定义类加载器加载。这种安排提高了Java程序的安全性,因为核心类库的来源可信,不会被恶意类替代。如果允许应用程序类加载器直接加载核心类库,那么恶意类可能会替代核心类库中的某些类,从而导致安全漏洞。双亲委派模型通过限制核心类库的加载,确保了可信任代码的执行,并防止恶意类的篡改。2.4 破坏双亲委派模型尽管双亲委派模型在大多数情况下是有益的,但是有些特定的场景下就需要破坏它,其中 JDBC 就是一个典型的例子: 在 JDBC 中,数据库厂商提供的自己的 JDBC 驱动,这些驱动实现了 JDBC 标准接口的类库。由于 JDBC 驱动需要和特定的数据库交互,因此它们通常由数据库厂商提供,而不是 Java 标准的一部分。而数据库厂商非常多,因此 JDBC 驱动的种类也就非常多了。
考虑到这种情况,JDBC 驱动就需要被应用程序自己加载,而不是委托给父类。如果此时还是按照双亲委派模型的规则进行类加载,那么加载的就是 Java 提供的 JDBC 标准接口的类库中的类了,而不是特定数据库的类。
为了解决这个问题,JDBC 驱动的加载通常是通过反射来实现的,应用程序类加载器可以直接加载驱动的类,而不通过双亲委派模型。这样应用程序可以加载自己所需的特定驱动类,而不受父类加载器的限制。 }
在学习java之前,我们得先了解一下java程序的运行机制:java是一门可以跨平台操作的语言,而之所以能跨平台,是因为其中有两个工具:编译器和解释器;下面举一个例子来解释这两者的区别:假如一位中文学家编写了一本书,这时有一位美国学者想要读这本书,但是他又不懂中文,这时,他就有两种方法来解决这个问题:第一种:通过阅读这本书的英文翻译版,但是却有个缺点,就是当中文书更新或者做了一些修改后,这位美国学者读的英文翻译版却不能实时更新;这也就是编译器的功能。第二种:通过雇佣一个翻译家,在自己身边实时翻译,可想而知,这种方式代价较高,但是能实时的更新内容;这也就是解释器的功能。通过上面的例子我们就明白了什么是编译器和解释器了当java程序运行时,我们写的源程序( .java)文件就会通过java编译器编译成为字节码(.class)文件,这时的字节码才能被我们的java虚拟机(JVM)所读取,具体通过类加载器→字节码校验器→解释器,最后到操作系统平台,此时,我们写的java程序就被成功运行了!下图即为整个的流程图:}

我要回帖

更多关于 java的运行机制是怎样的 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信