How do java files work?

Get into the habit of writing together! This is the 11th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

introduction

Before learning java, to understand how java files work, you must understand the memory structure of JVM and java.

This article first briefly introduces the java memory structure and where the program is saved when it is running.

I Prerequisites

A stack is a last-in, first-out data structure. In memory, variables are allocated on the stack for operation.

The heap is a memory area used to allocate space for type instances (objects). When an object is created on the heap, the address of the object is passed to the variable on the stack (in turn, the variable points to this object, or the variable refers to this object). object).

1.1 stack frame

Each thread has its own stack, and the data in the stack exists in the format of Stack Frame. Each method being executed on this thread corresponds to a stack frame (Stack Frame).

  1. stack frame is一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

For the execution engine, in the active thread, only the stack frame at the top of the stack is valid, which is called the current stack frame, and the method associated with this stack frame is called the current method. All bytecode instructions run by the execution engine operate only on the current stack frame.

When each method is executed, there is an independent memory space, and the memory space is reclaimed after the method is executed.

  1. Internal structure of stack frame

insert image description here

1.2 Heap

Stored is the shared data when the current java program is executed. In the heap, if an object is not pointed to by a variable, the variable is eligible for garbage collection.

1.3 Where is the program saved when it is running?

When the program is running, there are six places where data can be saved:

  1. Register: This is the fastest save area because it is located inside the processor. However, the number of registers is very limited, so registers are allocated by the compiler as needed.
  2. Stack: It resides in the regular RAM (Random Access Memory) area, but has direct support for processing through its "Stack Pointer". Moving the stack pointer down creates new memory; moving it up frees that memory. This is a particularly fast and efficient way to save data, second only to registers.

创建程序时,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虚拟机在执行的过程中管理的内存划分为若干个数据区域,如下图:

insert image description here

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

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

  1. 堆: Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。

insert image description here

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内存结构图

insert image description here

  1. 方法区:用来存储代码。将.class文件加载到内存中,并存储在方法区

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

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

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

执行过程:

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

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

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

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

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

2.2 类的生命周期

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

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

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

一个类的生命周期取决于它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对象。

insert image description here

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的地址在静态域中找到run方法,并且执行run方法。

分析示例2

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

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

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

  3. 在栈区开辟空间,记录堆区对象地址。

分析示例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): The static blocks in the class are executed sequentially from top to bottom


    public static int a;

    public static int b = 0;


复制代码

a is not assigned a value, and it remains the same as a=1. b is assigned, the original 1 value of b is overwritten, b=0. So the result is like this.

The code slightly modifies the declaration and initialization order of static variables, as follows:

    public static int a;

    public static intb = 0;

    public static Singleton singleton =new Singleton();

复制代码

Running result: a=1,b=1

6.2 Compile processing of static final variables

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);

    }

}

复制代码

The result is that only 8 is printed, proving that the class is not initialized. Decompile the source code and find that the content in the class ispublic static final int A = 8;

That is to say, the compiler is very intelligent and can figure out that 4+4 is 8 when compiling, which is a fixed number. There are no unknown factors in it.

Change the code a little bit

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

复制代码

At this time, the static block is executed, proving that the class is initialized. When the static final variable is uncertain at compile time, if the client program accesses the static variable of the class at this time, the class will be initialized, so 静态final变量尽量没什么可变因素在里面otherwise the performance will be degraded.

Guess you like

Origin juejin.im/post/7085158291415760910