java 虚拟机内存划分,类加载过程以及对象的初始化

时间:2024-01-01 08:29:09
涉及关键词:
虚拟机运行时内存 java内存划分 类加载顺序  类加载时机  类加载步骤  对象初始化顺序  构造代码块顺序 构造方法 顺序 内存区域   java内存图  堆 方法区 虚拟机栈 本地方法栈 程序计数器  局部变量表   栈帧  java堆 运行时常量池   直接内存
 本文从三个部分理解java的初始化

1).java虚拟机运行时的内存区域

2).类的加载过程

3).初始化过程

java虚拟机运行时内存空间区域分配

java 虚拟机内存划分,类加载过程以及对象的初始化
虚拟机栈中每个方法执行都会创建栈帧,每个栈帧中有局部变量表
方法区中有运行时常量

线程私有的,也就是每个线程都需要程序计数器 
java 虚拟机内存划分,类加载过程以及对象的初始化
 

java虚拟机栈 也是线程私有的
虚拟机栈描述的是java方法执行的内存模型,每个方法执行的同时都会创建栈帧
用于存储局部变量表/操作数栈/动态链接/方法出口等信息
一般所说的栈就是指的这里

本地方法栈跟虚拟机栈类似
只不过是运行的本地方法,虚拟机实现中有的直接把方法合二为一

可以右键新标签页面打开看大图
java 虚拟机内存划分,类加载过程以及对象的初始化

java堆是java虚拟机管理的最大一块内存,所有线程共享
启动时创建
唯一目的就是存放对象实例
几乎所有的对象实例都是在这里分配内存
垃圾回收的主要管理区域

java 虚拟机内存划分,类加载过程以及对象的初始化

方法区是与堆一样的线程共享的
存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据

java 虚拟机内存划分,类加载过程以及对象的初始化

 

运行时常量池是方法区的一部分
Class文件中有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用
这部分内容类加载之后进入方法区的运行时常量池中存放

java 虚拟机内存划分,类加载过程以及对象的初始化
 

 不是虚拟机运行的内存区域
java 虚拟机内存划分,类加载过程以及对象的初始化

类的加载

java代码编译成class文件之后,就形成了类的信息-类的二进制字节流
想要使用,肯定要加载

java 虚拟机内存划分,类加载过程以及对象的初始化

生命周期

java 虚拟机内存划分,类加载过程以及对象的初始化

加载/验证/准备/初始化/卸载 5个阶段顺序是确定的
解析不确定,可能在初始化阶段之后,为了支持java的运行时绑定

加载时机

1) new关键字实例化对象/读取或者配置类的静态字段/调用类的静态方法
2) java.lang.reflect 包的方法对类进行反射调用 如果没有初始化 触发
3) 初始化类的时候,发现父类没有初始化 触发父类初始化
4)虚拟机启动需要指定一个main方法的主类 先初始化这个类

加载

java 虚拟机内存划分,类加载过程以及对象的初始化

而且,对于非数组类的加载阶段,准确的说是加载阶段中获取类的二进制字节流的动作行为
是多样性的
可以使用系统提供的引导类加载器
也可以用户自定义的类加载器
开发人员可以通过定义自己的类加载器去控制字节流的获取方式(重写类加载器的loadCalss()方法)

数组类不同
数组类本身不通过类加载器创建 由java虚拟机直接创建
但是数组的元素类型 最终是靠类加载器去创建的

验证
确保Class文件的字节流中包含的信息符合当前虚拟机要求
并且不会危害虚拟机
因为字节码文件可以随便编写,由其他语言编译出来,或者直接十六进制编辑器直接书写
所以需要校验

文件格式校验
是否符合Class文件格式的规范
魔数/主次版本号/编码/文件完整性....相当于是格式上的硬校验

元数据校验
字节码描述的信息进行语义分析,确保其描述的信息符合语言规范要求
比如是否有父类 是否继承了不允许被继承的类等等
针对字节码描述的信息

字节码验证
通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的
第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析
保证被校验类的方法在运行时不会做出危害虚拟机安全的事情

符号引用验证
虚拟机将符号引用转化为直接引用的时候 解析过程中发生
符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)

准备

正式为类变量分配内存并设置类变量初始值的阶段
这些变量所使用的内存都将在方法区中进行分配
注意不包括实例变量 实例变量将会在对象实例化时随着对象一起分配在java堆中
基本数据类型的初始化

java 虚拟机内存划分,类加载过程以及对象的初始化

通常情况下是这样,如果是常量
public static final int value = 123; 准备阶段就会设置

解析

虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:
一组符号来描述所引用的目标,说白了是逻辑意义上的
符号可以是任何形式的字面量,只要能无歧义的定位到目标即可
符号引用与虚拟机实现的内存布局无关,引用的目标不一定加载到内存中
虽然各种虚拟机实现不同
但是能够接受的符号引用必须是一致的

直接引用:
可以是直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄
也就是物理上的,能够真的定位到指定的内存区域
跟虚拟机的实现的内存布局相关的
同一个符号引用不同虚拟机可能有不同的直接引用,而且一般是不同的

java 虚拟机内存划分,类加载过程以及对象的初始化

CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_InterfaceMethodref_info
CONSTANT_MethodType_info
CONSTANT_MethodHandle_info
CONSTANT_InvokeDynamic_info

初始化

在接下来是初始化,初始化也属于类加载的一步,不过这一步骤是程序员最关心的,单独拿出来说

类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)

初始化阶段是执行类构造器 <clinit>()方法的过程

java 虚拟机内存划分,类加载过程以及对象的初始化

所有的-->  类变量静态语句块

<clinit>() 对于类或者接口并不是必须的,如果一个类没有静态语句块
也没有对变量的赋值操作
编译器可以不为这个类生成<clinit>()方法

接口中不能使用静态语句块 但是仍然有变量初始化,所以接口与类一样,也会生成这个方法
但是与类不同的是,不需要先执行父接口的<clinit>()方法 
只有当父接口定义的变量使用时,父接口才会初始化

虚拟机会保证一个类的<clinit>()方法在多线程环境中能够被正确的加锁同步

从加载到对象初始化全过程

一加载时机

1) new关键字实例化对象/读取或者配置类的静态字段/调用类的静态方法
2) java.lang.reflect 包的方法对类进行反射调用 如果没有初始化 触发
3) 初始化类的时候,发现父类没有初始化 触发父类初始化
4)虚拟机启动需要指定一个main方法的主类 先初始化这个类

也就是在上面这些情况的时候 会发生类的加载

满足加载时机之后,然后经过类加载的几个过程

ps:  对于下面的初始化      同级别也就是相同优先级的变量的顺序是按照代码书写顺序来的

二初始化静态(类变量)

然后就是准备阶段中:

类变量 也就是static变量分配内存 并且 初始化数据默认值  注意实例对象的变量此时没有操作

另外如果是final 修饰的常量,此时一并直接赋值

三 构造器方法  <clinit>()

此时所有类的构造器方法执行

而且父类早于子类,所以最早执行的肯定是Object的

此方法把所有类的静态变量也就是类变量的赋值动作执行结束,而且静态代码块也已经执行结束,而且顺序是父类早于子类

也就是说至此,所有的静态变量都已经分配内存空间,也都是已经设置好的值了,包括父类的所有静态变量

静态变量以及静态代码块的执行都是在这里,显然他们是早于构造方法的执行的

但是如果静态变量赋值或者代码块中赋值中使用到了其他的方法,那么这个方法将会提早执行

如果使用new 构建了对象,不仅仅是构造方法,实例化的步骤都会执行的

而且如果是构造方法,那么new对象实例化的时候还会再次执行

java 虚拟机内存划分,类加载过程以及对象的初始化

输出结果:

1).main方法所在的类会被加载,所以会加载In这个类,

2).然后会处理static类型的变量,以及static代码块

3).这个变量赋值使用了new 所以会调用构造方法,如果是调用其他方法,一样先执行

静态变量和静态代码块优先级相同,代码块在下面,所以先打印了  ""构造方法""   然后打印了""静态方法""

4).此时类加载结束了进入主函数,主函数中先打印了分隔符"-----------------"

5).然后new对象又调用了构造方法,看起来怪怪的,其实逻辑很清晰

java 虚拟机内存划分,类加载过程以及对象的初始化

四  对象实例化

只有需要产生对象的时候才会有对象实例化,仅仅是加载类的话,上面的前三步就结束了

而且虽然说是一般最后但是也不一定,比如上面提到的如果静态变量调用new 就会提前触发

1.在堆上分配对象足够的内存空间

2.然后就是空间擦除清零,也即是设置为默认值

3.然后按照实例字段定义的顺序,顺序执行赋值初始化   初始化代码块 和直接定义变量的初始化 优先级别一样 按照定义顺序进行先后

4.实例的构造方法调用

对象的实例化是一个整体的,调用了new 就会按部就班的执行这些步骤

补充说明:

  1. 静态的初始化仅仅是类加载的时候发生,仅仅发生一次,类的加载时机看上面的---加载时机
  2. 静态变量也就是类变量都有默认的初始化值的,局部变量都没有默认值的,想要使用必须赋值,否则报错
  3. static不能修饰局部变量 java 虚拟机内存划分,类加载过程以及对象的初始化
  4. 静态变量不能向前引用,比如先使用了值,接下来才定义
  5. 成员属性值的初始化方式:并不是只能定义的时候赋值的

    类内声明时直接赋值
    构造方法 -----如果构造方法中只是初始化了部分属性值的话,其他的值还是默认值的
    调用成员方法进行初始化(方法可以有参数,不过参数必须是已经初始化了的)
    初始化块---只要构造对象,初始化块就会执行的,而且早于构造方法

   6.每个类都有默认的构造方法,如果你不定义他永远有一个默认的,如果定义了,默认的就不存在了,当你还需要new 对象(  ) 这种形式的话就不行了,按需添加

  7.数组的初始化定义的是一个引用,需要显式的初始化,否则引用为null,数组类型和普通的类加载是不一样的

  8.相同优先级别的根据定义的顺序决定初始化顺序;不同的优先级别的,不管你怎么写,优先级别高的始终会早于优先级低的

    比如静态的你写到构造方法下面还是静态的先执行;(特殊情况是上面提到的static变量用new 对象赋值)

    初始化代码块总会早于构造方法的执行

  9.继承结构中除非有特殊情况,否则顺序一般都是下面这样子的

    先执行静态的初始化
    所有的静态初始化结束
    执行最*初始化块
    执行最*构造方法

    ......
    执行子类初始化块
    执行子类构造方法

  10,如果对象中有其他类的成员变量,这个变量的静态,初始化块,构造方法的顺序(他们三个是一起的不分割的),跟这个类本身的初始化块的优先级是一样的,按照定义的顺序

  

      java 虚拟机内存划分,类加载过程以及对象的初始化

      比如  Test 中有T1   T1的静态初始化块,初始化块,构造方法是一起的,然后他们和Test的初始化块的顺序是不固定的

      java 虚拟机内存划分,类加载过程以及对象的初始化

好了,把这些点都记住的话,基本上就可以彻底理清楚了

初始化的过程是很复杂的,所以要掌握好优先级和规则

否则 包含的变量又有很多父类 等 各个类里面调用各种方法初始化就会让人彻底懵逼了