JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇
JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇
作为一个使用Java语言开发的程序员,我们都知道,要想运行Java程序至少需要安装JRE(安装JDK也没问题)。我们也知道我们Java程序员编写的程序代码文件是*.java的,而JRE运行的是*.class的文件。所以,我们需要将java文件编译成class文件然后才可以。那么,你有没有想过,一个java文件是怎么运行起来的呢?中间都经历了哪些环节呢?我们都知道JVM是Java虚拟机,那么,有没有思考过JVM的内存模型是什么呢?我们new出来的对象,声明不同类型的变量又是存放在JVM哪个位置呢?
本文是凯哥(凯哥Java:kaigejava)学习JVM系列教程第一篇。欢迎大家一起学习
本文目标:
通过本文学习后,希望大家对JVM类加载过程有个了解。
上面程序很简单。那么,有没有想过上面代码怎么运行的呢?
选中main方法,然后ruan as...,编译后,运行输出。这个流程我想大家都很熟悉的。那么对应的流程应该是什么样的呢?如下图:
在Run的时候,先将.java文件编译成.class文件。然后,在通过类加载器,将class文件加载到JVM中,然后在运行。输出结果。
那么为什么编译好的AppTest.class可以加载到JVM中呢?可以被JVM识别呢?
一个java类的一生都会经历哪些步骤呢?
如下图:
在我们run的时候,AppTest.java类先经过编译后,编译成了AppTest.class文件。JVM把class文件加载到内存后需要经历:加载-验证-准备-解析-初始化-使用-卸载这七个阶段。
第一个问题:JVM在什么时候会加载一个类呢?起始也就是在什么时候会加载.class字节码文件到JVM的内存中去呢?上面我们写的,当我们run的时候,才执行的。所以答案就很明确了,就是在你代码中需要使用到这个类的时候,就去加载的。
具体每一步:
加载
加载阶段是将class文件从磁盘或者jar等读到JVM内存中,并为其创建一个Class对象。任何一个类被使用时候系统都会为其创建一个Class对象的。
加载的同时将加载的这些数据转换成方法区中运行时数据(运行时候数据区:静态变量、静态代码块、常量池等),作为方法区数据的访问入口
这个很好理解的。我要想使用你,需要先得到你,是不是。结合上面我们自己写的AppTest类。在此阶段应该是:
扩展:
在类加载阶段JVM都做了什么?获取class文件方式都有哪些?
1.1:在类加载的时候JVM完成了以下:
- 根据类的全路径(全限定名)来获取到该类的二进制字节流
(我们知道,在电脑的世界中,什么都是二进制形式存在的)
- 将加载的字节流中所代表的静态存储结构转换成方法区运行时数据结构
(这个话具体怎么理解,有哪位能留言教教凯哥)
- 将加载的对象在内存中生成一个代表了该类的jvaa.lang.Class对象。这个Class对象作为加载进来对象在方法区各种数据的访问入口。
(要想在内存中访问AppTest这个字节码类中的属性或者方法的时候,可以在内存中方法区找到对应的Class对象。这个Class就是入口)
关于方法区在后面文章中,凯哥会详细讲讲。
1.2:获取class文件的方式
- 可以直接从本地的磁盘文件获取
- 可以从忘了下载class文件
- 可以从ZIP或者jar等文件中
- Java源文件动态编译的class文件
在一个类运行生命周期内,类加载(加载获取类的二进制字节流)阶段,是可控性最强的阶段。因为在这个阶段,我们程序员可以使用系统提供的类加载去来加载完成,也可以使用自己自定义的类加载来完成.(类加载器在后面文章详细讲讲)
1.3:类加载的具体时机,在文章最后,凯哥会列出来。
验证
将上一步加载到内存中的Class对象进行校验。确保加载的类的信息符合JVM的规范。确保没有安全方面的问题。
这个很好理解了,我要使用你,得到你好,我要检查你是不是符合标准的。如果不合法,就没法使用。
在此阶段如下图:
扩展:验证都验证哪些方面?
- 文件给是验证:验证加载的字节流是否 符合Class文件格式的规范。
例如:是否已咖啡babe开头(0xCAFEBABE),主次版八号是否在当前JVM的处理范围内等等
比如你在JDK1.8下编译的class文件,放到JDK1.6版本的JVM中,有可能就运行不了的
- 元数据验证:对字节码描述的信息进行语义分析。保证描述信息符合Java语言规范。
例如:这个类如果有父类,是否实现了父类的抽象方法等.
- 字节码验证
- 符号引用验证:确保解析动作是正确的。
例如:通过符号引用能找到对应点的类和方法。比如com.kaigejava.Person.getAge()
在比如:符号引用中类、属性、方法的访问性是否能被当前类访问等等。
准备
准备阶段,就是给加载进来且验证通过的Class类分配空间的。这里是给类里面的变量(也就是static修饰的变量)分配空间的,同时给变量一个默认的初始值。
如下图:
在准备阶段时候static int m 被分配了4个字节的空间,且分配了默认初始值为0(注意默认初始值是0).
PS:int类型占用4个字节。int的默认值是0.如果是对象的话。默认为null
在此阶段AppTest.class如下图:
该阶段需要注意:
- 在此阶段值只对static修饰的静态变量进行内存分配,赋默认值的(比如0、0L、0D、null、false等);
- 对于final修饰的静态字面值常量直接赋初始值(注意:这里的初始值并不是默认值。如果不是字面值静态常量,那么会和静态变量一样赋默认值)
比如:final int x = 1;这个在此阶段就给赋值的就是1而不是0
解析
解析是将常量池中的符号引用替换为直接引用(内存地址)的过程。
在此阶段AppTest类如下图:
扩展:
符号引用:
就是一组符号来描述目标的。可以是任何字面量。这个属于编译原理方面的东西。
比如:可以是一个类的完整类名字(com.kaigejava.Person)、字段的名称和描述符、方法的名称和描述等。
直接引用:
就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。比如指向方法区中某一个类的一个指针。
例如:在AppTest这个类中,有个static的静态变量p。这个静态变量p又是一个自定义的类型(com.kaigejava.Person),那么在经过解析阶段后,这个静态的p变量将是一个指针(比如0xddff1),这个指针指向该类在方法区的内存地址值。具体见凯哥后续文章,将会详细讲解。
初始化
到了此阶段(初始化阶段),JVM才开始真正的执行类中定义的Java代码。
当进行到初始化阶段的时候,就是执行类的构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器自动收集类中的所有类变量赋值动作和静态语句。
- <clinit>()方法与类的构造器不同。此方法不需要显示的调用类的父构造器(如果类有父类的话),虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此JVM中第一个被执行的<clinit>()方法的类肯定是java.lang.Object(因为Java中所有类的父类是Object类)
- 因为父类的<clinit>()方法先执行,所以也就意味着父类中定义的static语句块要优先于子类的变量赋值操作
- 如果一个类中没有静态变量或者是静态的语句块的时候,编译器可以不为这个类创建<clinit>()方法的
- 虚拟机会保证一个类的的<clinit>()方法在多线程环境中被正确的加锁和同步。多线程访问,一个访问,其他在访问的话会被阻塞。
使用
类实例化也初始化成功之后,这个类就是一个正常的类了。我们可以正常使用了。
卸载
当遇到以下几种情况的时候,类会被卸载
- 执行了System.exi()方法的时候
- 程序正常执行结束
- 程序在执行过程中遇到了异常或者是错误而异常终止
- 由于操作系统出现错误导致Java虚拟机进程终止
今天问题:
现在我们知道了一个Java类是怎么运行起来的了。那么请看下面代码,运行后输出的顺序是什么?
public class JvmDemo { public static void main(String[] args) { Son son = new Son(); FatherInterface fatherInterface = new SonInterFace(); fatherInterface.say("凯哥Java"); } } class Father{ static String st1 = "父类Father中的静态变量"; String str2 ="父类Father中的非静态变量"; static { System.out.println("当前执行了父类Father的静态代码块中的方法"); } { System.out.println("执行了父类Father类中的非静态代码块"); } public Father(){ System.out.println("执行了父类Father中的构造方法了"); } } class Son{ static String str1 = "子类Son中的静态变量"; String str2 = "子类Son中的非静态变量"; static{ System.out.println("执行了子类son中的静态代码块"); } { System.out.println("执行了子类Son中的非静态代码块"); } public Son(){ System.out.println("执行了子类son中的构造器方法"); } } interface FatherInterface{ static String str1 = "接口父类FatherInterface中的静态变量"; void say(String say); } class SonInterFace implements FatherInterface{ static String str1 = "子类SonInterFace中的静态变量"; String str2 = "子类SonInterFace中的非静态变量"; static{ System.out.println("执行了子类SonInterFace中的静态代码块"); } { System.out.println("执行了子类SonInterFace中的非静态代码块"); } public SonInterFace(){ System.out.println("执行了子类SonInterFace中的构造器方法"); } @Override public void say(String say) { System.out.println(FatherInterface.str1+"--say:"+say); } } |
运行后答案将在下一篇文章中揭晓。
下一篇预告:
因为这是第一篇,所以只是大致讲解了下一个类怎么加载过程。在下一篇文章中,咱们来讲解在加载阶段使用到类加载器、父类委派机制等、类在什么时候会被初始化等?。欢迎继续学习。
JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇的更多相关文章
- JVM学习笔记(二)------Java代码编译和执行的整个过程【转】
转自:http://blog.csdn.net/cutesource/article/details/5904542 版权声明:本文为博主原创文章,未经博主允许不得转载. Java代码编译是由Java ...
- JVM学习笔记(二)------Java代码编译和执行的整个过程
Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码 ...
- 从.Net到Java学习第一篇——开篇
以前我常说,公司用什么技术我就学什么.可是对于java,我曾经一度以为“学java是不可能的,这辈子不可能学java的.”结果,一遇到公司转java,我就不得不跑路了,于是乎,回头一看N家公司交过社保 ...
- Java并发包下锁学习第一篇:介绍及学习安排
Java并发包下锁学习第一篇:介绍及学习安排 在Java并发编程中,实现锁的方式有两种,分别是:可以使用同步锁(synchronized关键字的锁),还有lock接口下的锁.从今天起,凯哥将带领大家一 ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- LINQ to XML LINQ学习第一篇
LINQ to XML LINQ学习第一篇 1.LINQ to XML类 以下的代码演示了如何使用LINQ to XML来快速创建一个xml: public static void CreateDoc ...
- 一个JAVA代码
public class HelloJava { public static void main(String[] args) { System.out.println("这"); ...
- 第一个Java代码的编写 :HelloWorld代码的编写
HelloWorld代码的编写 创建一个新的文件夹,通过Notepad++编写第一个Java程序 , 文件名为"Hello.java" 在文件中编写,如下代码: public cl ...
- 听说你还不知道Java代码是怎么运行的?
作为一名Java程序员,我们需要知道Java代码是怎么运行的.最近复习了深入理解Java虚拟机这本书,做了一下笔记,希望对大家有帮助,如果有不正确的地方,欢迎提出,感激不尽. java 代码运行主要流 ...
随机推荐
- “深度评测官”——记2020BUAA软工软件案例分析作业
项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 个人博客作业-软件案例分析 我在这个课程的目标是 完成一次完整的软件开发经历并以博客的方式记录开发 ...
- 使用 IPMI 远程为服务器安装操作系统教程
使用 IPMI 远程为服务器安装操作系统教程 shida_csdn 2019-01-09 11:30:10 9588 收藏 16展开一.什么是 IPMI? IPMI 是智能平台管理接口(Intelli ...
- lambda,filter,map,reduce
# lambda,filter,map,reduce from functools import reduce print('返回一个迭代器') print((x) for x in range(5) ...
- mysql基础之mariadb库管理和表管理语句
一.数据库管理语句 1.Syntax: CREATE {DATABASE | SCHEMA} [IF NOT EXISTS] db_name [create_specification] ... cr ...
- Linux进阶之Git分布式版本控制系统篇
一.Git介绍 Git(读音为/gɪt/.)是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常大的项目版本管理. Git 是 Linus Torvalds 为了帮助管理 Linux 内核 ...
- 3*060-A 日志记录
电路检修遇到的问题: 今天在检修一块3060-A电路板时 发现 3.3V烫 但是3.3V路上的电容并没有短路 于是拆单片机 拆RS232EN 拆 FM24V 最后发现 原来是 1117 ...
- 简单读读源码 - dubbo多提供者(provider)配置方法
简单读读源码 - dubbo多提供者(provider)配置方法 消费者端dubbo的yml配置 dubbo: consumer: timeout: 300000 protocol: name: du ...
- C语言编程 菜鸟练习100题(51-60)
[练习51]矩阵转置 0. 题目: 矩阵的转置 1. 分析: 练习使用 for 循环嵌套,多维数组的表达. 2. 程序: #include <stdio.h> int main() { i ...
- Ubuntu中的MySQL修改root密码的多种方法
查看.修改mysql的用户名和密码第一步:这时你需要进入/etc/mysql目录下,然后sudo vim/vi debian.cnf查看里面的用户名和密码,然后使用这个文件中的用户名和密码进入mysq ...
- 【python接口自动化】初识unittest框架
本文将介绍单元测试的基础版及使用unittest框架的单元测试. 完成以下需求的代码编写,并实现单元测试 账号正确,密码正确,返回{"msg":"账号密码正确,登录成功& ...