java整个编译以及运行的过程相当繁琐,我就举一个简单的例子说明:
Java程序从源文件创建到程序运行要经过两大步骤:
1、源文件由编译器编译成字节码(ByteCode);
2、字节码由java虚拟机解释运行。因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言
( "semi-interpreted" language)
public class Main {
public static void main(String[] args) {
Animal animal = new Animal("super_yc");
animal.printName();
}
}
class Animal{
private String name;
public Animal(String name) {
super();
this.name = name;
}
public void printName(){
System.out.println("Animal = " + this.name);
}
}
第一步(编译):创建完源文件之后,程序先要被JVM中的java编译器进行编译为.class文件。java编译一个类时,如果这个类所依赖的类还没有被编译,
编译器会自动的先编译这个所依赖的类,然后引用。如果java编译器在指定的目录下找不到该类所依赖的类的 .class文件或者 .java源文件,就会报
"Cant found sysbol"的异常错误。
编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、
方法引用,成员变量引用等);方法字节码中放的是各个方法的字节码。
第二步(运行):java类运行的过程大概分为两个步骤:(1)类的加载 (2)类的执行。需要说明的一点的是:JVM主要在程序第一次运行时主动使用类的
时候,才会立即去加载。换言之,JVM并不是在运行时就会把所有使用到的类都加载到内存中,而是用到,不得不加载的时候,才加载进来,而且只加载一次!
根据上面的程序,详解该程序运行的详细步骤:
(1)在类路径下找到编译好的 java 程序中得到 Test.class 字节码文件后,在命令行上敲 java Test,系统就会启动一个 JVM 进程,JVM进程从classpath
路径下找到一个名为Test.class的二进制文件,将Test.class文件中的类信息加载到运行时数据区的方法区中,这一过程叫做类的加载。(只有类信息在方法区
中,才能创建对象,使用类中的成员变量)
(2)JVM 找到main方法的主函数入口, 持有一个指向当前类(Test)常量池的指针,而常量池中的第一项是发现是一个对Animal对象的符号引用,并且
main方法中第一条指令是Animal animal = new Animal("super_yc"),就是让JVM创建一个Animal对象,但是方法区中还没有Animal类的类信息,于是
JVM就要马上的加载Animal类,将Animal类信息放入到方法区中,于是JVM 以一个直接指向方法区 Animal类的指针替换了常量池中第一项的符号引用。
(3)加载完Animal类的信息以后,JVM虚拟机就会在堆内存中为一个Animal类实例分配内存,然后调用其构造函数初始化Animal实例,这个实例持有指向
方法区的Animal类的类型信息(其中包含有方发表,java动态绑定的底层实现)的引用。(animal指向了Animal对象的引用会自动的放在栈中,字符串常量
"super_yc"会自动的放在方法区的常量池中,对象会自动的放入堆区)
(4)当使用 animal.pringName()的时候,JVM根据栈中animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型
信息方法表,获得pringName()函数的字节码地址,然后开始运行函数。
java中类加载时机
java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令最常见的java代码场景是:
1)使用new关键字实例化对象
2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
4)调用一个类的静态方法
验证:
1)当类被初始化时,其静态代码块会执行。
class ClassLoadTime{
static{
System.out.println("ClassLoadTime类初始化时就会被执行!");
}
public ClassLoadTime(){
System.out.println("ClassLoadTime构造函数!");
}
}
class ClassLoadDemo{
public static void main(String[] args){
ClassLoadTime clt = new ClassLoadTime();
}
}
输出结果:
ClassLoadTime类初始化时就会被执行!
ClassLoadTime构造函数!
2) 读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
class ClassLoadTime{
static{
System.out.println("ClassLoadTime类初始化时就会被执行!");
}
public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)
public ClassLoadTime(){
System.out.println("ClassLoadTime构造函数!");
}
}
class ClassLoadDemo{
public static void main(String[] args){
int value = ClassLoadTime.max;
System.out.println(value);
}
}
输出:
ClassLoadTime类初始化时就会被执行!
200
3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
class ClassLoadTime{
static{
System.out.println("ClassLoadTime类初始化时就会被执行!");
}
public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)
public ClassLoadTime(){
System.out.println("ClassLoadTime构造函数!");
}
}
class ClassLoadDemo{
public static void main(String[] args){
ClassLoadTime.max = 100;
}
}
输出:
ClassLoadTime类初始化时就会被执行!
4)调用一个类的静态方法
class ClassLoadTime{
static{
System.out.println("ClassLoadTime类初始化时就会被执行!");
}
public static int max = 200; (防止测试类和此类不在一个包,使用public修饰符)
public ClassLoadTime(){
System.out.println("ClassLoadTime构造函数!");
}
public static void method(){
System.out.println("静态方法的调用!");
}
}
class ClassLoadDemo{
public static void main(String[] args){
ClassLoadTime.method();
}
}
输出:
ClassLoadTime类初始化时就会被执行!
静态方法的调用!
被final修饰静态字段在操作使用时,不会使类进行初始化,因为在编译期已经将此常量放在常量池。
测试:
class ClassLoadTime{
static{
System.out.println("ClassLoadTime类初始化时就会被执行!");
}
public static final int MIN = 10; (防止测试类和此类不在一个包,使用public修饰符)
}
class ClassLoadDemo{
public static void main(String[] args){
System.out.println(ClassLoadTime.MIN);
}
}
输出:
10
子类调用或者设置父类的静态字段或者调用父类的静态方法时仅仅初始化父类,而不初始化子类。同样读取final修饰的常量不会进行类的初始化。
class Fu{
public static int value = 20;
static{
System.out.println("父类进行了类的初始化!");
}
}
class Zi{
static{
System.out.println("子类进行了类的初始化!");
}
}
class LoadDemo{
public static void main(String[] args){
System.out.println(Zi.value);
}
}
输出:
父类进行了类的初始化!
20
java类中各种成员的初始化时机,此处不一一测试:
类变量(静态变量)、实例变量(非静态变量)、静态代码块、非静态代码块 的初始化时机:
* 由 static 关键字修饰的(如:类变量[静态变量]、静态代码块)将在类被初始化创建实例对象之前被初始化,而且是按顺序从上到下依次被执行;
public static int value =34;
static{
System.out.println("静态代码块!");
}
public 类名(){
System.out.println("构造函数!");
}
一旦这样写,在类被初始化创建实例对象之前会先初始化静态字段value,然后执行静态代码块,当实例化对象时会执行构造方法中的代码
* 没有 static 关键字修饰的(如:实例变量[非静态变量]、非静态代码块)初始化实际上是会被提取到类的构造器中被执行的,但是会比类构造器中的
代码块优先执行到,其也是按顺序从上到下依次被执行。
public int value =34;
{
System.out.println("非静态代码块!");
}
public 类名(){
System.out.println("构造函数!");
}
在使用构造函数实例化一个对象时,会先初始化value,然后执行非静态代码块,最后执行构造方法里面的代码。
*在存在父类的时候,调用子类的构造时,会先调用父类的默认构造(空参构造),进行父类的初始化。