java编译和类加载详述 博客分类: JAVA java
程序员文章站
2024-03-24 09:37:40
...
来源于https://blog.csdn.net/HelloJave/article/details/83145719
java编译和类加载详述
Java程序运行时,必须经过编译和运行两个步骤。首先将后缀名为.java的源文件进行编译,最终生成后缀名为.class的字节码文件。然后Java虚拟机将编译好的字节码文件加载到内存(这个过程被称为类加载,是由加载器完成的),然后虚拟机针对加载到内存的java类进行解释执行,显示结果。
Java的运行原理
在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(ByteCode),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。
Java代码编译整个过程
Java代码编译是由Java源码编译器来完成,流程图如下所示:
词法分析器:
词法分析器一般以函数的形式存在,供语法分析器调用。
这里的单词是一个字符串,是构成源代码的最小单位。从输入字符流中生成单词的过程叫作单词化(Tokenization),在这个过程中,词法分析器还会对单词进行分类。
词法分析器通常不会关心单词之间的关系(属于语法分析的范畴),举例来说:词法分析器能够将括号识别为单词,但并不保证括号是否匹配。
词法分析(lexical analysis)或扫描(scanning)是编译器的第一个步骤。词法分析器读入组成源程序的字符流,并且将它们组织成有意义的词素(lexeme)的序列,并对每个词素产生词法单元(token)作为输出。
简单的来说,词法分析就是将源程序(可以认为是一个很长的字符串)读进来,并且“切”成小段(每一段就是一个词法单元 token),每个单元都是有具体的意义的,例如表示某个特定的关键词,或者代表一个数字。而这个词法单元在源程序中对应的文本,就叫做“词素”。词法分析注重的是每个单词是否合法,以及这个单词属于语言中的哪些部分
token流:词法分析器的结果,就是把程序的语句进行分词得到的的一个个“单词”!
语法分析器:
是对token流进行语法检查、并构建由输入的单词组成的数据结构(语法树/抽象语法树)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”;语法分析的上下文无关文法注重的是一个一个的推导式,是将词法分析中得到的单词按照语法规则进行组合
语法树/抽象语法树:
是源代码的抽象语法结构的树状表现形式,这里特指java的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if(;;){ //当符合条件执行的任务}或者 while(true){//当符合条件执行的任务}这样的条件跳转语句,可以使用带有两个分支的节点来表示。
语义分析器:语义分析就是要了解各个推导式之间的关系是否合法,主要体现在推导式中使用的终结符和非终结符之间的关系,也就是它们的类型。
注解抽象语法树:经过 语义分析器将语法树/抽象语法树转化为注解抽象语法树
字节码生成:
目的:将注解语法树转化成字节码,并将字节码写入*.class文件。
流程:
将java的代码块转化为符合JVM语法的命令形式,这就是字节码
按照JVM的文件组织格式将字节码输出到*.class文件中
类加载详解:
在Java 中分为主动引用和被动引用 主动引用都会触发类的加载!比如:访问这个类的静态变量,方法,和 通过new ,jvm标记加载的类(存在main方法的类),反射等,父类在子类加载的时候也会被加载
被动引用:比如访问静态常量或者创建数组内部对象
类加载主要是由jvm虚拟机负责的,过程非常复杂,类加载分三步 加载 》 连接 》初始化
加载
程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用,用到的就是类加载器classLoader ,java程序的运行并不是直接依 靠底层的操作系统,而是基于jvm虚拟机。
类加载器:负责读取字节码,并转换成java.Long.Class类的一个对象存在于方法区
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始
注意:要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
java中加载器的种类大致可以分为四种:
1.Bootstrap ClassLoader(由C++语言写成)*加载器,%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
2.系统加载器(也就是内部类AppClassLoader) 加载classpath下的路径下的class
3.ExtClassLoader,加载jre\lib\ext\classes文件下的class
4.java.net.UrlClassLoader. 加载指定的url下的class
当我们运行一个程序时,首先是找到JDK安装目下的jvm.dll来启动JAVA虚拟机。
而后Bootstrap ClassLoader产生。
接下来就是Bootstrap ClassLoader来加载ExtClassLoader,并且指定ExtClassLoader的父加载器为Bootstrap ClassLoader,但是因为Bootstrap ClassLoader用C++语言写的,所以用JAVA的观点来看,这个加载器的实例是不存在的所以ExtClassLoader的父加载器被设置为了null。
然后就是Bootstrap ClassLoader将AppClassLoader装载,并指定其父加载器为ExtClassLoader。
双亲委派模型:是通过组合实现的!并不是继承关系!
JAVA是按照加载器的委派模型来实现的。这种模型是JAVA安全性机制的保证。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,可以防止核心API库被随意篡改
连接:
连接是很重要的一步,过程比较复杂,分为三步 验证 》准备 》解析
验证:确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编译规则,这一步就是要过滤掉这部分不合法文件
准备:为类的静态变量分配内存,将其初始化为默认值 。我们都知道静态变量是可以不用我们手动赋值的,它自然会有一个初始值 比如int 类型的初始值就是0 ;boolean类型初始值为false,引用类型的初始值为null 。 这里注意,只是为静态变量分配内存,此时是没有对象实例的
解析:将class 内的 符号引用,加载到 运行时常量池 内转化成为 直接引用 的过程解释一下符号引用和直接引用。比如在方法A中使用方法B,A(){B();},这里的B()就是符号引用,它对于方法的调用没有太多的实际意义。但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用。
初始化:
为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值
类加载完成!!!
备注:
当 JVM 遇到 new 指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过,如果没有就先执行类加载。如果类已经被加载过,则会为新生对象分配内存(所需内存大小在类加载后就可以确定),分配对象内存采取的方式是“指针碰撞”或“空闲列表”,前者是在内存比较规整的情况下,后者是在空闲内存和已使用内存相互交错的情况下,而内存是否规整这又取决于垃圾回收器。
问:我们通过 Java 栈中对象的引用去访问这个对象,访问对象的主流方式有 那些
答 :使用句柄和直接指针。
使用句柄访问:在 Java 堆中会划分出一块内存作为句柄池,引用中储存的内容就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针访问:在对象的内存布局中就要放置访问类型数据的指针。
这两种方式各有优势,使用句柄的好处是引用中存储的是稳定的句柄,对象被移动时(垃圾回收时对象被移动)只需改变句柄中的实例数据的指针,不需要改动引用本身。而使用直接指针的好处是速度更快,它节省了一次指针定位的开销。HotSpot 使用的是第二种方式进行对象的访问。
问:怎解决内存分配的线程安全问题?
答:jvm提供了2种解决内存分配的线程安全问题,1.使用cas无锁机制失败重试来保证操作的原子性 2.jvm 会给每条线程分配本地线程分配缓冲TLAB 可以通过配置来决定分配的大小,只有当分配内存用完后才会去锁进行同步操作
---------------------
作者:HelloJave
来源:CSDN
原文:https://blog.csdn.net/HelloJave/article/details/83145719
版权声明:本文为博主原创文章,转载请附上博文链接!
java编译和类加载详述
Java程序运行时,必须经过编译和运行两个步骤。首先将后缀名为.java的源文件进行编译,最终生成后缀名为.class的字节码文件。然后Java虚拟机将编译好的字节码文件加载到内存(这个过程被称为类加载,是由加载器完成的),然后虚拟机针对加载到内存的java类进行解释执行,显示结果。
Java的运行原理
在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(ByteCode),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。
Java代码编译整个过程
Java代码编译是由Java源码编译器来完成,流程图如下所示:
词法分析器:
词法分析器一般以函数的形式存在,供语法分析器调用。
这里的单词是一个字符串,是构成源代码的最小单位。从输入字符流中生成单词的过程叫作单词化(Tokenization),在这个过程中,词法分析器还会对单词进行分类。
词法分析器通常不会关心单词之间的关系(属于语法分析的范畴),举例来说:词法分析器能够将括号识别为单词,但并不保证括号是否匹配。
词法分析(lexical analysis)或扫描(scanning)是编译器的第一个步骤。词法分析器读入组成源程序的字符流,并且将它们组织成有意义的词素(lexeme)的序列,并对每个词素产生词法单元(token)作为输出。
简单的来说,词法分析就是将源程序(可以认为是一个很长的字符串)读进来,并且“切”成小段(每一段就是一个词法单元 token),每个单元都是有具体的意义的,例如表示某个特定的关键词,或者代表一个数字。而这个词法单元在源程序中对应的文本,就叫做“词素”。词法分析注重的是每个单词是否合法,以及这个单词属于语言中的哪些部分
token流:词法分析器的结果,就是把程序的语句进行分词得到的的一个个“单词”!
语法分析器:
是对token流进行语法检查、并构建由输入的单词组成的数据结构(语法树/抽象语法树)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”;语法分析的上下文无关文法注重的是一个一个的推导式,是将词法分析中得到的单词按照语法规则进行组合
语法树/抽象语法树:
是源代码的抽象语法结构的树状表现形式,这里特指java的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if(;;){ //当符合条件执行的任务}或者 while(true){//当符合条件执行的任务}这样的条件跳转语句,可以使用带有两个分支的节点来表示。
语义分析器:语义分析就是要了解各个推导式之间的关系是否合法,主要体现在推导式中使用的终结符和非终结符之间的关系,也就是它们的类型。
注解抽象语法树:经过 语义分析器将语法树/抽象语法树转化为注解抽象语法树
字节码生成:
目的:将注解语法树转化成字节码,并将字节码写入*.class文件。
流程:
将java的代码块转化为符合JVM语法的命令形式,这就是字节码
按照JVM的文件组织格式将字节码输出到*.class文件中
类加载详解:
在Java 中分为主动引用和被动引用 主动引用都会触发类的加载!比如:访问这个类的静态变量,方法,和 通过new ,jvm标记加载的类(存在main方法的类),反射等,父类在子类加载的时候也会被加载
被动引用:比如访问静态常量或者创建数组内部对象
类加载主要是由jvm虚拟机负责的,过程非常复杂,类加载分三步 加载 》 连接 》初始化
加载
程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用,用到的就是类加载器classLoader ,java程序的运行并不是直接依 靠底层的操作系统,而是基于jvm虚拟机。
类加载器:负责读取字节码,并转换成java.Long.Class类的一个对象存在于方法区
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始
注意:要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
java中加载器的种类大致可以分为四种:
1.Bootstrap ClassLoader(由C++语言写成)*加载器,%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
2.系统加载器(也就是内部类AppClassLoader) 加载classpath下的路径下的class
3.ExtClassLoader,加载jre\lib\ext\classes文件下的class
4.java.net.UrlClassLoader. 加载指定的url下的class
当我们运行一个程序时,首先是找到JDK安装目下的jvm.dll来启动JAVA虚拟机。
而后Bootstrap ClassLoader产生。
接下来就是Bootstrap ClassLoader来加载ExtClassLoader,并且指定ExtClassLoader的父加载器为Bootstrap ClassLoader,但是因为Bootstrap ClassLoader用C++语言写的,所以用JAVA的观点来看,这个加载器的实例是不存在的所以ExtClassLoader的父加载器被设置为了null。
然后就是Bootstrap ClassLoader将AppClassLoader装载,并指定其父加载器为ExtClassLoader。
双亲委派模型:是通过组合实现的!并不是继承关系!
JAVA是按照加载器的委派模型来实现的。这种模型是JAVA安全性机制的保证。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,可以防止核心API库被随意篡改
连接:
连接是很重要的一步,过程比较复杂,分为三步 验证 》准备 》解析
验证:确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编译规则,这一步就是要过滤掉这部分不合法文件
准备:为类的静态变量分配内存,将其初始化为默认值 。我们都知道静态变量是可以不用我们手动赋值的,它自然会有一个初始值 比如int 类型的初始值就是0 ;boolean类型初始值为false,引用类型的初始值为null 。 这里注意,只是为静态变量分配内存,此时是没有对象实例的
解析:将class 内的 符号引用,加载到 运行时常量池 内转化成为 直接引用 的过程解释一下符号引用和直接引用。比如在方法A中使用方法B,A(){B();},这里的B()就是符号引用,它对于方法的调用没有太多的实际意义。但是B方法实际调用时是通过一个指针指向B方法的内存地址,这个指针才是真正负责方法调用,他就是直接引用。
初始化:
为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值
类加载完成!!!
备注:
当 JVM 遇到 new 指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过,如果没有就先执行类加载。如果类已经被加载过,则会为新生对象分配内存(所需内存大小在类加载后就可以确定),分配对象内存采取的方式是“指针碰撞”或“空闲列表”,前者是在内存比较规整的情况下,后者是在空闲内存和已使用内存相互交错的情况下,而内存是否规整这又取决于垃圾回收器。
问:我们通过 Java 栈中对象的引用去访问这个对象,访问对象的主流方式有 那些
答 :使用句柄和直接指针。
使用句柄访问:在 Java 堆中会划分出一块内存作为句柄池,引用中储存的内容就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针访问:在对象的内存布局中就要放置访问类型数据的指针。
这两种方式各有优势,使用句柄的好处是引用中存储的是稳定的句柄,对象被移动时(垃圾回收时对象被移动)只需改变句柄中的实例数据的指针,不需要改动引用本身。而使用直接指针的好处是速度更快,它节省了一次指针定位的开销。HotSpot 使用的是第二种方式进行对象的访问。
问:怎解决内存分配的线程安全问题?
答:jvm提供了2种解决内存分配的线程安全问题,1.使用cas无锁机制失败重试来保证操作的原子性 2.jvm 会给每条线程分配本地线程分配缓冲TLAB 可以通过配置来决定分配的大小,只有当分配内存用完后才会去锁进行同步操作
---------------------
作者:HelloJave
来源:CSDN
原文:https://blog.csdn.net/HelloJave/article/details/83145719
版权声明:本文为博主原创文章,转载请附上博文链接!
推荐阅读
-
java 求最大公约数和最小公倍数 博客分类: JAVA java最大公约数最小公倍数
-
java编译和类加载详述 博客分类: JAVA java
-
java日期时间转换工具类 博客分类: java常用类 java日期转换流水号时间格式格式日期
-
selenium不打开浏览器和打开浏览器-很方便(java) 博客分类: Java_aboutTesting_自动化测试
-
xml文件和java对象之间转换 博客分类: Java EE xmljava
-
ssh java类的连接与远程命令执行(包括密钥连接) 博客分类: Java EE sshjava
-
Socket的长连接和短连接-Java实现-关键点 博客分类: Java_about
-
Java文件操作类FileManager 博客分类: java java
-
Java中replace和replaceAll的区别 博客分类: java java正则表达式
-
Java中replace和replaceAll的区别 博客分类: java java正则表达式