目录
一、JVM内存结构
二、JVM类加载过程
1.加载
1. 类加载的来源
2.类加载时机
2.连接
1.验证
2.准备
3.解析
3.初始化
1.定义
2.类构造器
3.类初始化的时机
三、类的实例化过程
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
1.程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。
2.虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError。
3.本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法。
4.堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作。
5.方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中。
程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接(验证、准备、解析)、初始化三个步骤对类进行加载也叫初始化。这里注意,解析这个过程不一定是顺序进行的,也可能在初始化阶段之后,这个是为了支持Java的运行时绑定。
下图为类的生命周期:
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了。在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
来源主要有:本地文件系统、JAR包、网络、动态编译Java源文件后加载。
1.使用前预先加载
2.首次使用时加载
3.类加载的原则:延迟加载,能不加载就不加载。
类的连接,即把类的.class文件中的内容合并到JRE中,具体可分为三个阶段:
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证选项主要有:文件格式验证、字节码验证、符号引用验证...
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段(final修饰的基本类型的静态变量会在这里赋值)。
例如:
public static int value = 123;
在这个阶段,他的初始值设为0,而不是123。
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
首先关于常量池:
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(ConstantPool Table),用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。所以我们可以了解:运行时常量池里面存放的是从Class文件中的常量池表中加载到的数据。
常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据。常量池中主要存放两大数据:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
关于符号引用:
由于Java代码在进行Javac编译的时候,并不像C/C++那样有“连接”这一步骤,而是在虚拟机加载Clsss文件的时候进行动态连接,因此,在我们将Java代码编译成Class文件后,Class文件并不会保存方法、字段等在内存中的布局。为了解决这个问题,Class文件会在常量池内保存方法、字段等的符号引用。所谓符号引用,我们可以简单的理解为真正内存布局的占位符,在类加载过程的解析阶段,符号引用会被替换为真正的直接引用。
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
关于直接引用:
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。
对静态变量显示初始化和静态代码块执行初始化。
注意:初始化的顺序为代码排列的顺序。如静态代码块在静态变量声明前,静态变量a在静态代码块和静态变量中分别做了赋值,则a先被静态代码块赋值,后被静态变量赋值语句赋值。并且静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不可以访问。
初始化阶段是执行类构造器
注意:
1.类构造器(
2.虚拟机会保证在子类类构造器
1.创建类的实例
2.调用某个类的静态变量或者静态方法
3.初始化某个类的子类
4.使用反射机制强制创建某个类或接口对应的java.lang.Class对象
5.直接使用java.exe命令来运行某个主类
注意:对于常量(final)静态(static)类变量,在程序编译时期就可以确定该值(是个常量),所以不会加载对应的类。
实例化的实际:在使用new或反射的时候。
1.首先确保*.class文件加载完成
2.系统赋初始值:在堆中为这个类及其父类中的分静态成员变量分配内存,并且赋默认值(0,null...)
(此时为对象分配内存空间,内存空间的初始化为0,所以成员变量的初始值就是0)
3.父类成员变量的显示初始化和构造代码块的初始化:执行顺序就是代码的排列顺序。和静态代码块、静态变量的一样。
4.父类构造函数执行:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有类的构造函数的第一条语句必须是超类的构造函数的调用语句,以保证所创建实例的完整性。
5.当前类的成员变量初始化和构造代码块初始化
6.当前类的构造函数的初始化
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。