vlambda博客
学习文章列表

java文件是如何运转的?

引言

在学习java之前,要先了解java文件是如何运转的,就得了解一下JVM和java的内存结构。

本文先简单介绍一下的java内存结构、程序运行时保存到什么地方。

I 前置知识

栈(stack)是一种后进先出的数据结构,在内存中,变量会被分配在堆栈上来进行操作。

1.1 栈帧

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame )。

  1. 栈帧是 一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

每一个方法执行时,都有一个独立的内存空间,内存空间在方法执行完毕之后,随之被回收。

  1. 栈帧内部结构
在这里插入图片描述


1.2 堆

存放的是当前java程序执行时共享数据。在堆中,如果一个对象没有被变量指向,该变量就符合垃圾回收机制的条件。

1.3 程序运行时保存到什么地方?

程序运行时,有六个地方都可以保存数据:

  1. 寄存器: 这是最快的保存区域,因为它位于处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。
  2. 堆栈: 驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。

创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里(特别是对象句柄),但Java对象并不放到其中。

  1. 堆 : 一种常规用途的内存池(也在RAM区域),其中保存了Java对象。

和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。

要求创建一个对象时,只需用new命令编写相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!

  1. 静态存储; 这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。

可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。

  1. 常数存储: 常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。

  2. 非RAM存储: 若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。

其中两个最主要的例子便是“流式对象”“固定对象”

对于流式对象,对象会变成字节流,通常会发给另一台机器。

而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对Lightweight  persistence的支持。未来的版本甚至可能提供更完整的方案。

1.4 JVM的内存划分

JVM主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。

Java虚拟机在执行的过程中管理的内存划分为若干个数据区域,如下图:

java文件是如何运转的?
在这里插入图片描述
  1. Java栈: 是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。

StackOverflowError:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。

  1. 堆: Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。
java文件是如何运转的?
在这里插入图片描述

JVM堆一般又可以分为以下三部分

  1. Perm: 主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类。

热部署的应用服务器有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,很大原因是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。

  1. Tenured: Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

  2. Young: Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

II 类在JVM中的工作原理

类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。

  1. JVM栈由一个个的栈帧组成, 每个栈帧都是一个方法的调用状态. 每个栈帧主要由三部分组成: 局部变量表(又叫本地变量表), 操作数栈和其他一些信息. 主要是局部变量表和操作数栈.
  2. 堆栈的意义:1)栈是保证方法在运算时的数据安全;减少内存空间。2)堆是以空间换时间来提高效率。

2.1  java内存结构图

java文件是如何运转的?
在这里插入图片描述
  1. 方法区:用来存储代码。将.class文件加载到内存中,并存储在方法区

  2. 栈:用来存储局部变量,形参,方法的返回值,中间运算结果

  3. 堆:成员变量,数组对象,方法的引用

  4. 本地方法区:存储链接本地方法相关的代码

执行过程:

  1. 执行了java命令之后,classloader将.class文件,加载到内存中并存储在方法区。

  2. JVM调用main方法,顺次执行代码。

  3. 将局部变量存储在栈区中,将引用变量是指向内容存储在堆区中。

  4. 引用变量所指向的空间,用来存储hashcode码,顺次执行到mian方法完毕。

  5. 再通过classLoader将.class文件内容在JVM所占用的空间全部卸载。

2.2  类的生命周期

虚拟机规范并没有规定在什么时候要加载类,但是规定了在遇到 new、反射、父类、Main的时候需要初始化完成。

在类加载完成之后就可以开始执行了,和线程运转相关的东西都放在栈帧中,其结构如下:

属性 作用/含义
局部变量表 方法参数及方法内部定义的局部变量
操作数栈 用来被指令操作
动态连接 指向运行时常量池中该栈帧所属方法的引用
方法返回地址 上层方法调用本方法的位置
附加信息 调试信息等

一个类的生命周期取决于它Class对象的生命周期:

  1. 当一个类被加载、连接、初始化后,它的生命周期就开始了。
  2. 当代表该类的Class对象不再被引用、即已经不可触及的时候,Class对象的生命周期结束。那么该类的方法区内的数据也会被卸载,从而结束该类的生命周期。

由Java虚拟机自带的默认加载器(根加载器、扩展加载器、系统加载器)所加载的类在JVM生命周期中始终不被卸载。所以这些类的Class对象(实例的模板对象)始终能被触及!而由用户自定义的类加载器所加载的类会被卸载掉!

要想使用一个Java类为自己工作,必须经过以下几个过程:

  1. 类加载:从字节码.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的方法区内,之后在堆区建立一个java.lang.Class对象,用来封装方法区的数据结构。类加载的最终产物就是堆中的一个java.lang.Class对象。

Classloader的作用,就是将编译后的class装载、加载到机器内存中,为了以后的程序的执行提供前提条件。

  1. 链接:连接又分为以下小步骤

a. 验证:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。

b. 准备:将类的静态变量进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情)

c. 解析:把类的符号引用转为直接引用(保留)

  1. 类的初始化:将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。

III 类的加载

ClassLoader的loadClass方法加载一个类不属于主动调用,不会导致类的初始化。

ClassLoader classLoader = ClassLoader.getSystemClassLoader();

Class<?> clazz = classLoader.loadClass("test01.ClassDemo");


并不会让类加载器初始化test01.ClassDemo,因为这不属于主动调用此类。

3.1 ClassLoader的关系(父委托加载机制)

根加载器——>扩展类加载器——>应用类加载器——>用户自定义类加载器

加载类的过程是首先从根加载器开始加载、根加载器加载不了的,由扩展类加载器加载,再加载不了的有应用加载器加载,应用加载器如果还加载不了就由自定义的加载器(一定继承自java.lang. ClassLoader)加载、如果自定义的加载器还加载不了。而且下面已经没有再特殊的类加载器了,就会抛出ClassNotFoundException,表面上异常是类找不到,实际上是class加载失败,更不能创建该类的Class对象。

在这里插入图片描述

3.2 类的加载

类的加载方式:

1):本地编译好的class中直接加载

2):网络加载:java.net.URLClassLoader可以加载url指定的类

3):从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类

4):从java源代码文件动态编译成为class文件

JVM自带的默认加载器:

1):根类加载器:bootstrap,由C++编写,所有Java程序无法获得。

2):扩展类加载器:由Java编写。

3):系统类、应用类加载器:由Java编写。

用户自定义的类加载器:java.lang.ClassLoader的子类,用户可以定制类的加载方式。

每一个类都包含了加载他的ClassLoader的一个引用

getClass().getClassLoader()。如果返回的是null,证明加载他的ClassLoader是根加载器bootstrap。

像jre的rt.jar下面的java.lang.*都是默认的根类加载器去加载这些运行时的类。

    publicstaticvoid main(String[] args)throws ClassNotFoundException {

       Class clazz = Class.forName("java.lang.String");

       System.out.println(clazz.getClassLoader());

    }
//结果是null,证明java.lang.String是根类加载器去加载的。

    publicstaticvoid main(String[] args) {

       Singleton mysingleton = Singleton.GetInstence();

    System.out.println(mysingleton.getClass().getClassLoader());

    }
//结果是sun.misc.Launcher$AppClassLoader@19821f,证明是AppClassLoader(系统类、应用类加载器)去加载的。

3.3 运行时包

由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。

假设用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于 java.lang.Spy 和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包。所以javalang.Spy不能访问核心类库javalang包中的包可见成员。

ClassLoader加载类的原代码如下

    protected synchronized Class<?>loadClass(String name, boolean resolve)

    throws ClassNotFoundException

    {

    // First, check if the class has already been loaded

    Class c = findLoadedClass(name);

    if (c ==null) {

        try {

       if (parent !=null) {

           c = parent.loadClass(name,false);

       } else {

           c = findBootstrapClassOrNull(name);

       }

        } catch (ClassNotFoundException e) {

                // ClassNotFoundException thrown if class not found

                // from the non-null parent class loader

            }

            if (c ==null) {

            // If still not found, then invoke findClass in order

            // to find the class.

            c = findClass(name);

        }

    }

    if (resolve) {

        resolveClass(c);

    }

    return c;

    }

初始化系统ClassLoader代码如下

    private static synchronized void initSystemClassLoader() {

    if (!sclSet) {

        if (scl !=null)

       throw new IllegalStateException("recursive invocation");

            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

        if (l !=null) {

       Throwable oops = null;

       scl = l.getClassLoader();

            try {

           PrivilegedExceptionAction a;

           a = new SystemClassLoaderAction(scl);

                    scl = (ClassLoader) AccessController.doPrivileged(a);

            } catch (PrivilegedActionException pae) {

           oops = pae.getCause();

                if (oops instanceof InvocationTargetException) {

               oops = oops.getCause();

           }

            }

       if (oops !=null) {

           if (oop sinstanceof Error) {

           throw (Error) oops;

           } else {

               // wrap the exception

               throw new Error(oops);

           }

       }

        }

        sclSet = true;

    }

    }

它里面调用了很多native的方法,也就是通过JNI调用底层C++的代码。

IV 类的链接

4.1 链接阶段的准备

    publics taticint a;

    public staticint b = 10;

在这个阶段,加载器会按照结构化似的,从上到下流程将静态变量int类型分配4个字节的空间,并且为其赋予默认值0,而像b = 10这段代码在此阶段是不起作用的,b仍然是默认值0。

4.2  链接阶段的解析

在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。

例 : 在Worker类的gotoWork0方法中会引用Car类的run方法。

public void gotowork(){
car.run();//这段代码在worker类的二进制数据中表示为符号引用
}

在Worker类的二进制数据中包含了一个对Car类的run0方法的符号引用,它由run方法的全名和相关描述符组成。

在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run0)方法在方法区内的内存位置,这个指针就是直接引用。

这里面的指针就是C++的指针

V  类的初始化

4.1 初始化的时机

所有的JVM实现(不同的厂商有不同的实现)在首次主动调用类和接口的时候才会初始化他们。

以下是视为主动使用一个类,其他情况均视为被动使用!

1):new一个类的实例对象

2):对类的静态变量进行读取、赋值操作的。

3):直接调用类的静态方法。

4):反射调用一个类的方法。

5):初始化一个类的子类的时候,父类也相当于被程序主动调用了

如果调用子类的静态变量是从父类继承过来并没有复写的,那么也就相当于只用到了父类的东东,和子类无关,所以这个时候子类不需要进行类初始化

6):直接运行一个main函数入口的类。

4.2 初始化的顺序

  1. 初始化变量:

对于静态变量要首先进行初始化,因为后面的方法可能会使用这个变量,或者构造函数中也可能用到。

而对于非静态变量而言,由于匿名块内、非静态方法和构造函数都可以进行操作(不仅仅是初始化),所以要提前进行加载和赋默认值。

  1. 初始化静态代码块: 多个静态代码块按顺序加载

这里需要注意:这个顺序是类内书写的顺序,也是类加载的顺序。

  1. 匿名代码块,这个要后初始化于静态代码块,因为其依然属于实例对象,而不属于类。在这里可以对非静态成员变量进行初始化工作。
  2. 构造函数 :先初始化父类,因为子类可能会继承父类的属性或方法,所以肯定要先初始化父类了,而初始化父类则必须要调用父类的构造函数。

至于方法不用考虑,因为方法不用初始化.

分析示例1

Joo.run(参数类表);

分析:

  1. 检查Joo是否存在代码区

  2. 若存在代码区,就不加载Joo代码。若不存在,就将Joo的代码加载到方法区中。

将被static修饰的属性、方法和静态块放置到静态域中,将普通方法放置在普通方法区中。

静态域:存放静态属性、静态方法和静态块。

  1. 当Joo的代码加载完毕之后,立即执行static块;

分析示例2

Joo J=new Joo(参数类表);

  1. 检查J00在代码区是否存在

  2. 若存在,就直接在堆区中开辟空间,并将非静态的属性以及非静态的方法引用存在堆区中。

分析示例3

Joo.print(参数类表);

VI   案例分析

6.1 值的变化和变量的声明顺序的关联

package test01;

class Singleton {

    public static Singleton singleton =new Singleton();

    public static int a;

    public static int b = 0;

    private Singleton() {

       super();

       a++;

       b++;

    }

    public static SingletonGetInstence() {

       return singleton;

    }

}

public class MyTest {

    /**

     * @param args

     */


    public static void main(String[] args) {

       Singleton mysingleton = Singleton.GetInstence();

       System.out.println(mysingleton.a);

       System.out.println(mysingleton.b);

    }

}

运行结果:a=1,b=0。

具体分析:

  1. 主动调用Singleton类
Singleton mysingleton = Singleton.GetInstence();//根据内部类的静态方法要一个Singleton实例,这个时候就属于主动调用Singleton类了。


  1. 开始加载Singleton类

1):对Singleton的所有的静态变量分配空间,赋默认的值,所以在这个时候,singleton=null、a=0、b=0。注意b的0是默认值,并不是咱们手工为其赋予的的那个0值。

2):之后对静态变量赋值,这个时候的赋值就是我们在程序里手工初始化的那个值了。此时singleton = new Singleton();调用了构造方法,构造方法里面a=1、b=1。之后接着顺序往下执行。

3):类中的静态块static块顺序地从上到下执行的


    public static int a;

    public static int b = 0;


a没有赋值,保持原状a=1。b被赋值了,b原先的1值被覆盖了,b=0。所以结果就是这么来的。

代码稍微修改一下静态变量的声明和初始化顺序,如下:

    public static int a;

    public static intb = 0;

    public static Singleton singleton =new Singleton();

运行结果:a=1,b=1

6.2 静态final变量的编译处理

package test01;

class FinalStatic {

    publics tatic final int A = 4 + 4;

    static {

       System.out.println("如果执行了,证明类初始化了……");

    }

}

publicclass MyTest03 {

    /**

     * @param args

     */


    public static void main(String[] args) {

       System.out.println(FinalStatic.A);

    }

}

结果是只打印出了8,证明类并没有初始化。反编译源码发现class里面的内容是public static final int A = 8;

也就是说编译器很智能的、在编译的时候自己就能算出4+4是8,是一个固定的数字。没有什么未知的因素在里面。

将代码稍微改一下

public static final int A = 4 + new Random().nextInt(10);

这个时候静态块就执行了,证明类初始化了。静态final变量在编译时不定的情况下,如果客户程序这个时候访问了该类的静态变量,那就会对类进行初始化,所以静态final变量尽量没什么可变因素在里面,否则性能会有所下降。