一、类加载过程,双亲委派模型
1.Java中类加载分为3个步骤:加载、链接、初始化。
- 加载。加载是将字节码数据从不同的数据源读取到JVM内存,并映射为JVM认可的数据结构,也就是Class对象的过程。数据源可以是Jar文件、Class文件等等。如果数据的格式并不是ClassFile的结构,则会报ClassFormatError。
- 链接。链接是类加载的核心部分,这一步分为3个步骤:验证、准备、解析。
a. 验证。验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。
b.准备。这一步会创建静态变量,并为静态变量开辟内存空间。
c.解析。这一步会将符号引用替换为直接引用。
- 初始化。初始化会为静态变量赋值,并执行静态代码块中的逻辑。
2.双亲委派模型。
类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。
启动类加载器主要加载 jre/lib下的jar文件。
扩展类加载器主要加载 jre/lib/ext 下的jar文件。
应用程序类加载器主要加载 classpath下的文件。
所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。
3.Java程序执行过程
.java ——编译——> .class
类加载器负责加载各个字节码文件(.class)
加载完.class后,由执行引擎执行,在执行过程中,需要运行时数据区提供数据
二.内存空间划分
JMVS规定了,Java虚拟机运行时数据区的划分,应当分为如下几块:
程序计数器、Java堆、Java虚拟机栈、本地方法栈、方法区
程序计数器(PC寄存器):线程私有。生命周期与线程相同,每创建一个线程就会创建出一个程序计数器。线程销毁,计数器就销毁。
每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。
- 一块较小的的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空。
- 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError情况的区域。
通俗的说,可以把它看作是一个指针,他指向当前程序正在运行的一行代码。需要注意的是,他指向的是字节码,不是你写的代码!
java 虚拟机栈:线程私有,在创建线程时创建的,用来存储栈帧。是Java方法执行的内存模型。
- 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用类型和returnAddress类型,它所需的内存空间在编译期间完成分配。
- 一般把Java内存区分为堆内存(Heap)和栈内存(Stack),其中『栈』指的是虚拟机栈,『堆』指的是Java堆。
- 在Java虚拟机规范中,对这个区域规定了两种异常状况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可动态扩展且扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。
本地方法栈:跟JVM虚拟机栈比较类似,只不过它支持的是Native方法。
- 在虚拟机规范中,对这个区域无强制规定,由具体的虚拟机自由实现。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
java 堆:用于存放几乎所有的对象实例和数组。
- 被所有线程共享的一块内存区域,在虚拟机启动时创建。
在Java堆中,可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),但无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
- 是垃圾收集器管理的主要区域,也被称做“GC堆”。
- 是Java虚拟机所管理的内存中最大的一块。
- 在Java虚拟机规范中,如果在堆中没有内存完成实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。与Java堆一样,是各个线程共享的内存区域。
- JVMS中起的别名叫non-heap(为了与Java堆区分开来)
- 作用是存储 Java 类的结构信息(代码中:常量(static)、静态变量(static)、方法、JIT编译后的部分数据)
- JVMS 不要求该区域实现自动内存管理,但是商用 Java 虚拟机都能够自动管理该区域的内存,主要是常量池回收和类类型卸载,但是方法区的GC条件是相当苛刻的。
- 可能出现 OutOfMemoryError 异常
那么方法区具体存储了哪些东西呢?
(1)类型信息 :
-- 这个类型的完整有效名
-- 这个类的直接父类的完整有效名(除Object例外)
-- 这个类型的修饰符(public absctract final)
-- 这个类型的直接接口的游戏列表
解释下什么叫完整有效名:代码中是 包名+ "." + 类名,但是类文件是"/"代替点: com/gaiay/mebilecard/GroupDetail。
(2)运行时常量池
--全局共享
--是方法区的一部分
--作用是存储 Java 类文件常量池中的符号信息
--可能出现 OutOfMemoryError 异常
在class文件中,有部分很重要的信息就是常量池,用于存放在编译期就生成好的各种字面量和符号引用。这部分信息在类加载后被装入方法区的运行时常量池。Java虚拟机对class文件结构是有非常严格的规定的,calss文件中的 常量池 以及其他信息,必须保证每个字节存储哪种数据都必须与JVMS一致,才能被Java虚拟机认可、装载和执行,但是对于 运行时常量池,JVMS并没有细节上的约束,不同JVM厂商可以按照自己的需要去实现自己运行时常量池,一般来说 运行时常量池除了保存class文件中描述的符号引用外也会把符号引用翻译出来的直接引用一起存储在运行时常量池中。 那么这里所说的直接引用和间接引用指的是什么呢?好比当前类为A.java , 里面引用了一个String str , 和一个 你自己写的类 B b, 那么a就是个直接引用, b就是间接引用 。在编译期,str在class文件中的描述可能就是String类似这样的,而b就是类似 com.person.B.java (只是举个栗子,不要当真)。 然后在类加载时,会根据com.person.B.java 在方法区符号表中解析出实际的B.java的真实地址。 相比于class文件的常量池,既然叫做运行时常量池,那么必然是有动态性的。Java语言并不要求常量只能在编译期间才能产生,在运行时也可以通过代码产生新的常量,并将它们放入到运行时常量池当中。这种特性被开发者利用最大的就是 String#intern() 。
(3)Field信息
-- 域名
-- 域类型
-- 域修饰符(public, private, protected,static,final volatile, transient的某个子集)
(4)方法信息
-- 方法名
-- 方法的返回类型(或 void)
-- 方法参数的数量和类型(有序的)
-- 方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集)
-- 除了abstract和native方法外,其他方法还有保存方法的字节码(bytecodes)操作数栈和方法栈帧的局部变量区的大小、异常表
(5)static 变量(也叫类变量)
(6)final 常量
常量也会被保存在方法区中,同时也会复制一份到常量池中。保存在方法区中的常量是保存在使用他的类信息中, 而static 变量是保存在 声明 他的类信息中的。
(7)方法表
JVMS并没有声明方法表一定要存在,他是Java虚拟机设计者为了提高方法的访问效率而实现的一种数据信息结构,他是实现(1)中用来存储类型信息的一部分,jvm对每个加载的非虚拟类(个人理解是非匿名类)的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法)。jvm可以通过方法表快速激活实例方法。