JVM内存模型
1、类的加载、连接和初始化
加载:查找并加载类的二进制数据(字节码文件)
连接:
验证:确保被加载的类的正确性(手工生成class文件,可
能不符合JVM标准规范)
准备:为类的静态变量分配内存,并将其初始化为默认值
(这个时候并不涉及new对象的操作)
解析:把类中的符号引用转换为直接引用(Child.run()转
换为pointer)
初始化:为类的静态变量赋予正确的初始值(正确:用户赋予的
值)
示例:MyTest
1.1类的使用方式:
所有的类或接口只有在java程序“首次主动使用”才会初始化
主动使用:
(1)创建类的实例new Test();
(2)访问某个类的静态变量,或者为某个类的静态变量赋值:a =
Test.b; Test.b = a;
(3)调用类的静态方法:Test.doSomething();
(4)初始化一个类的子类:
class Parent{};class Childextends Parent{
public static int a= 3};
Child.a =4;
(5) java虚拟机就启动时被标明为启动类的类(java TestTest
中有main方法)
(6)反射Class.forName(“Test”);
除此之外的使用都是被动使用,被动使用是不会触发初始化的。
1.2类的加载:
将.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,又 来封装在方法区内的数据结构,class对象是jvm在类加载时自动创建的。
1.3类加载器
Java虚拟机自带的类加载器:
根类加载器(Bootstrap)(c++ java代码中无法获取该类)
扩展类加载器(Extension)(java )
系统类加载器(System)(也叫应用加载器)
用户自定义的类加载器
层次关系:
根类加载器:用c++实现,并没有集成java.lang.ClassLoader类
扩展类记载器:父类加载器为根类加载器,加载目录jre\lib\ext,如果把用户创建的jar包放在这个目录下,则会有扩展类加载器 进行加载。
系统类加载器:父类加载器为扩展类加载器,从环境变量classpath或系统属性java.class.path所指定的目录加载类,是自定义 加载类的默认父类加载器。
(1)核心类库由bootstrap加载器进行加载,获取到为空,其他都可获取,例Test2
(2)调用一个类的静态变量,且这个静态变量在编译时期就可以确定,则不会进行类的初始化,当这个类在执行阶段才能确 定,则会进行初始化,例Test4,Test3
(3)加载顺序—启动类—父类—子类,例Test5,Test6
(4)调用loadClass方法加载一个类,并不是对类的主动使用,并不会初始化该类。例Test7
1.4类的加载机制—父委托机制
(1)除了java自带的根类加载器,其余类加载器都有且仅有一个父类加载器,只有当父类加载器不能进行加载的时候,才会由 子类加载器进行加载。
(2)父子加载器并非继承关系,子加载器并不一定继承父类加载器
(3)安全考虑,每个类都会有确定的层次中的一个类加载器进行加载,自定义除外。
(4)每个类都有自己的命名空间,命名空间由该类加载器及所有父类加载器所加载的类组成。同一个命名空间中,不会存在两 个完整名字相同的类,不同命名空间中,可能存在完整名字相同的类。
1.5 用户自定义类加载器
继承ClassLoader类,覆盖findClass方法,会由loadClass方法调用。
1.6 JVM虚拟机执行整体顺序
2、JVM的内存结构
方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
程序计数器:当前线程所执行字节码指令的行号指示器,线程私有,执行java方法,计数器记录的是java代码的行号,执行native方法则为null(undefined);
虚拟机栈:线程私有,表示java方法执行的内存模型,每个栈帧对应的是一个调用的方法,包括局部变量表、动态链接、操作数栈、指向当前方法所属的类的运行时常量池、方法返回地址和附加信息。
栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。
局部变量表:方法参数,方法内部定义的变量。
操作数栈:每一个方法的调用,都会建立一个操作数栈
动态链接:与静态解析对应,一部分的符号引用会在运行是转化为直接引用
方法返回地址:正常完成出口,异常完成出口
本地方法栈:线程私有,与虚拟机栈类似,为native方法服务
堆:线程共享,存储对象本身及数组
方法区:线程共享,类信息,静态变量,常量,编译后的代码等
运行时常量池:存放编译期生成的各种字面量和符号引用
2.1对象的创建—内存分配
(1)jvm遇到一个new指令,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个类代表的类是否已被加载、解析和初始化。如果没有,则进行类加载。
(2)类的加载—先加载父类,先加载静态块,顺序加载。
(3)JVM开始为对象分配内存,两种方式:
指针碰撞:java堆内存规整,GC带有压缩整理功能
空闲列表:java堆内存空间并不规整。
解决多线程分配内存的问题:
失败重试:保证更新操作的原子性
本地线程缓存取:TLAB,在启动线程时,就会在堆中给该线程预留一个内存空间
(4)内存分配完后,首先内存空间都初始化为零值。若使用TLAB,则在分配TLAB时即已初始化成功。
(5)设置对象头(Object Header):如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。
(6)执行方法,将对象初始化为指定的值。
3、GC垃圾回收
3.1判断对象失效
(1)引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1,当引用失效就减1,计数器为0的对象就是不可能再被引用的对象。
实现简单,但无法解决循环引用的问题
(2)可达性分析算法
通过一系列成为“GC ROOT”的对象作为起始点,向下搜索,走过的路径成为引用链,当一个对象到GC ROOT没有任何引用链相连时,则证明此对象是不可用的。
可作为GC ROOT的对象有:
虚拟机栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(native方法)引用的对象
3.2垃圾回收算法
(1)标记—清除算法
先对不用的对象进行标记,然后清除
(2)复制算法
将内存按容量分为几块,在一个时间段只使用一块内存,当这块内存用完,则将内存清理干净,然后将可用的对象复制到另一个区域。
(3)标记—整理算法
复制算法在对象存活率较高时要进行较多的复制操作,效率将变低,而且浪费空间。
标记—整理的标记过程与标记—清除一样,但后续步骤不是对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
(4)分代收集算法
根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代。
在新生代中每次垃圾收集时都发现有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为对象存活率搞,没有额外空间对它进行分配担保,就必须使用标记—清理或标记—整理算法来进行回收