java文件是如何运转的?
引言
在学习java之前,要先了解java文件是如何运转的,就得了解一下JVM和java的内存结构。
本文先简单介绍一下的java内存结构、程序运行时保存到什么地方。
I 前置知识
栈(stack)是一种后进先出的数据结构,在内存中,变量会被分配在堆栈上来进行操作。
1.1 栈帧
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame )。
-
栈帧是 一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
每一个方法执行时,都有一个独立的内存空间,内存空间在方法执行完毕之后,随之被回收。
-
栈帧内部结构
1.2 堆
存放的是当前java程序执行时共享数据。在堆中,如果一个对象没有被变量指向,该变量就符合垃圾回收机制的条件。
1.3 程序运行时保存到什么地方?
程序运行时,有六个地方都可以保存数据:
-
寄存器: 这是最快的保存区域,因为它位于处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。 -
堆栈: 驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。
创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里(特别是对象句柄),但Java对象并不放到其中。
-
堆 : 一种常规用途的内存池(也在RAM区域),其中保存了Java对象。
和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。
要求创建一个对象时,只需用new命令编写相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!
-
静态存储; 这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。
可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。
-
常数存储: 常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
-
非RAM存储: 若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。
其中两个最主要的例子便是
“流式对象”
和“固定对象”
。对于流式对象,对象会变成字节流,通常会发给另一台机器。
而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对Lightweight persistence的支持。未来的版本甚至可能提供更完整的方案。
1.4 JVM的内存划分
JVM主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。
Java虚拟机在执行的过程中管理的内存划分为若干个数据区域,如下图:
-
Java栈: 是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。
StackOverflowError:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。
-
堆: Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。
JVM堆一般又可以分为以下三部分
-
Perm: 主要保存class,method,filed对象,这部门的空间一般不会溢出,除非一次性加载了很多的类。
热部署的应用服务器有时候会遇到j
ava.lang.OutOfMemoryError : PermGen space
的错误,很大原因是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
-
Tenured: Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
-
Young: Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
II 类在JVM中的工作原理
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载
七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化
五个阶段。
-
JVM栈由一个个的栈帧组成, 每个栈帧都是一个方法的调用状态. 每个栈帧主要由三部分组成: 局部变量表(又叫本地变量表), 操作数栈和其他一些信息. 主要是局部变量表和操作数栈. -
堆栈的意义:1)栈是保证方法在运算时的数据安全;减少内存空间。2)堆是以空间换时间来提高效率。
2.1 java内存结构图
-
方法区:用来存储代码。将.class文件加载到内存中,并存储在方法区
-
栈:用来存储局部变量,形参,方法的返回值,中间运算结果
-
堆:成员变量,数组对象,方法的引用
-
本地方法区:存储链接本地方法相关的代码
执行过程:
-
执行了java命令之后,classloader将.class文件,加载到内存中并存储在方法区。
-
JVM调用main方法,顺次执行代码。
-
将局部变量存储在栈区中,将引用变量是指向内容存储在堆区中。
-
引用变量所指向的空间,用来存储hashcode码,顺次执行到mian方法完毕。
-
再通过classLoader将.class文件内容在JVM所占用的空间全部卸载。
2.2 类的生命周期
虚拟机规范并没有规定在什么时候要加载类,但是规定了在遇到 new、反射、父类、Main的时候需要初始化完成。
在类加载完成之后就可以开始执行了,和线程运转相关的东西都放在栈帧中,其结构如下:
属性 | 作用/含义 |
---|---|
局部变量表 | 方法参数及方法内部定义的局部变量 |
操作数栈 | 用来被指令操作 |
动态连接 | 指向运行时常量池中该栈帧所属方法的引用 |
方法返回地址 | 上层方法调用本方法的位置 |
附加信息 | 调试信息等 |
一个类的生命周期取决于它Class对象的生命周期:
-
当一个类被加载、连接、初始化后,它的生命周期就开始了。 -
当代表该类的Class对象不再被引用、即已经不可触及的时候,Class对象的生命周期结束。那么该类的方法区内的数据也会被卸载,从而结束该类的生命周期。
由Java虚拟机自带的默认加载器(根加载器、扩展加载器、系统加载器)所加载的类在JVM生命周期中始终不被卸载。所以这些类的Class对象(实例的模板对象)始终能被触及!而由用户自定义的类加载器所加载的类会被卸载掉!
要想使用一个Java类为自己工作,必须经过以下几个过程:
-
类加载:从字节码.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的方法区内,之后在堆区建立一个java.lang.Class对象,用来封装方法区的数据结构。类加载的最终产物就是堆中的一个java.lang.Class对象。
Classloader的作用,就是将编译后的class装载、加载到机器内存中,为了以后的程序的执行提供前提条件。
-
链接:连接又分为以下小步骤
a. 验证:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。
b. 准备:将类的静态变量进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情)
c. 解析:把类的符号引用转为直接引用(保留)
-
类的初始化:将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。
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
Joo.run(参数类表);
分析:
-
检查Joo是否存在代码区
-
若存在代码区,就不加载Joo代码。若不存在,就将Joo的代码加载到方法区中。
将被static修饰的属性、方法和静态块放置到静态域中,将普通方法放置在普通方法区中。
静态域:存放静态属性、静态方法和静态块。
-
当Joo的代码加载完毕之后,立即执行static块;
-
分析示例2
Joo J=new Joo(参数类表);
-
检查J00在代码区是否存在
-
若存在,就直接在堆区中开辟空间,并将非静态的属性以及非静态的方法引用存在堆区中。
-
分析示例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。
具体分析:
-
主动调用Singleton类
Singleton mysingleton = Singleton.GetInstence();//根据内部类的静态方法要一个Singleton实例,这个时候就属于主动调用Singleton类了。
-
开始加载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变量尽量没什么可变因素在里面
,否则性能会有所下降。