• 从内存分配分析程序初始化和存储


    从内存分配分析程序初始化和存储

    一.类中各成员的执行顺序

    属性、方法、构造方法和自由块都是类中的成员,在创建类的对象时,类中各成员的执行顺序:
    1.父类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
    2.子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
    3. 父类的实例成员普通类变量和实例初始化块,按在代码中出现的顺序依次执行。
    4.执行父类的构造方法。
    5.子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
    6.执行子类的构造方法。

    注意:程序启动需要触发main方法的时候,虚拟机会先触发这个类的初始化

    使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、JIT时放入常量池的静态字段除外)、调用一个类的静态方法,会触发初始化

    当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化

     

    public class InitialOrderTest {
        /**
         * Description
         * 
         * @param args
         */
        public static void main(String[] args) {
            System.out.println("Parent.str4:"+Parent.str4);
            Son s = new Son();
            System.out.println("s.str5:"+s.str5);
    
        }
    }
    
    class Parent {
    
        {
            String str1 = "parent构造块中的变量";//
            System.out.println("parent中的构造块");
        }
        String str2 = "parent类变量";//
    
        static {
            String str3 = "parent构造块中的变量";//
            System.out.println("parent中static初始化块");
        }
        static String str4 = "parent类静态变量";//
    
        public Parent() {
            System.out.println("parent构造方法");
        }
    }
    
    class Son extends Parent {
        {
            System.out.println("son中的初始化块");
        }
        String str5 = "son类变量";//
        static String str6 = "son类静态变量";//
        static {
            String str7 = "son类变量";//
            System.out.println("son中的static初始化块");
        }
        public Son() {
            System.out.println("son构造方法");
        }
    }

    输出:

    parent中static初始化块
    Parent.str4:parent类静态变量
    son中的static初始化块
    parent中的构造块
    parent构造方法
    son中的初始化块
    son构造方法
    s.str5:son类变量


    二.类初始化顺序的JVM解释

    类初始化顺序受到JVM类加载机制的控制,类加载机制包括加载、验证、准备、解析、初始化等步骤。不管是在继承还是非继承关系中,类的初始化顺序主要受到JVM类加载时机、解析和clinit()初始化规则的影响。

    加载时机

    加载是类加载机制的第一个阶段,只有在5种主动引用的情况下,才会触发类的加载,而在其他被动引用的情况下并不会触发类的加载。关于类加载时机和5中主动引用和被动引用详见【深入理解JVM】:类加载机制。其中3种主动引用的形式为:

    程序启动需要触发main方法的时候,虚拟机会先触发这个类的初始化

    使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、JIT时放入常量池的静态字段除外)、调用一个类的静态方法,会触发初始化

    当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化

    代码1中触发main()方法前,需要触发主类InitialOrderWithoutExtend的初始化,主类初始化触发后,对静态代码区和静态成员进行初始化后,打印”第1个主类对象:”,之后遇到newInitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();,再进行其他普通变量的初始化。

    代码2是继承关系,在子类初始化前,必须先触发父类的初始化。

    类解析在继承关系中的自下而上递归

    类加载机制的解析阶段将常量池中的符号引用替换为直接引用,主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。关于类的解析过程详见【深入理解JVM】:类加载机制

    而在字段解析、类方法解析、方法类型解析中,均遵循继承关系中自下而上递归搜索解析的规则,由于递归的特性(即数据结构中栈的“后进先出”),初始化的过程则是由上而下、从父类到子类的初始化顺序。

    初始化clinit()方法

    初始化阶段是执行类构造器方法clinit() 的过程。clinit() 是编译器自动收集类中所有类变量(静态变量)的赋值动作和静态语句块合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。JVM会保证在子类的clinit() 方法执行之前,父类的clinit() 方法已经执行完毕。

    因此所有的初始化过程中clinit()方法,保证了静态变量和静态语句块总是最先初始化的,并且一定是先执行父类clinit(),在执行子类的clinit()。

    代码顺序与对象内存布局

    在前面的分析中我们看到,类的初始化具有相对固定的顺序:静态代码区和静态变量先于非静态代码区和普通成员,先于构造函数。在相同级别的初始化过程中,初始化顺序与变量定义在程序的中顺序是一致的。

    而代码顺序在对象内存布局中同样有影响。(关于JVM对象内存布局详见【深入理解JVM】:Java对象的创建、内存布局、访问定位。)

    HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。而实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

    无论是从父类继承还是子类定义的,都需要记录下来,这部分的存储顺序JVM参数和字段在程序源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。满足这个条件的前提下,父类中定义的变量会出现在子类之前。不过,如果启用了JVM参数CompactFields(默认为true,启用),那么子类中较窄的变量也可能会插入到父类变量的空隙中。

    静态代码声明的变量存储在哪里?为什么不能声明静态变量?

    构造代码块声明的变量存储在哪里?为什么不能声明静态变量?

    Str1与str2的区别

    Str3与str4的区别

    执行顺序:(优先级从高到低)静态代码块>main方法>构造代码块>构造方法。

    普通代码块:在方法或语句中出现的{}就称为普通代码块。普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出现先执行”

    构造块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块。构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。

     

    静态代码块:在java中使用static关键字声明的代码块。静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。//如果类中包含多个静态代码块,那么将按照"先定义的代码先执行,后定义的代码后执行"。

    注意:1 静态代码块不能存在于任何方法体内。

    2 静态代码块不能直接访问静态实例变量和实例方法,需要通过类的实例对象来访问。

    JVM将内存划分为6个部分:PC寄存器(也叫程序计数器)、虚拟机栈、堆、方法区、运行时常量池、本地方法栈

    PC寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。

    java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有的。java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表、操作数栈、动态链接、方法出口等信息。这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常。

    局部变量表:方法的局部变量列表,在编译时就写入了class文件

    操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量x

    java 堆java堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出OutOfMemoryError异常。

    方法区:方发区被各个线程共享,用于存储静态变量、运行时常量池等信息。

    本地方法栈:本地方法栈的主要作用就是支持native方法,比如在java中调用C/C++

    栈中主要存放局部变量。

    堆中存放new出来的东西。

    static 的变量或者字符串常量 则存在在 data segment(数据区)中;

    那么类中方法的话,是存在在 code segment(代码区)中了。

  • 相关阅读:
    架构设计:系统存储(10)——MySQL简单主从方案及暴露的问题
    西安大唐提车游记——感受古都容颜
    架构设计:系统存储(9)——MySQL数据库性能优化(5)
    架构设计:系统存储(8)——MySQL数据库性能优化(4)
    架构设计:系统存储(7)——MySQL数据库性能优化(3)
    架构设计:系统存储(6)——MySQL数据库性能优化(2)
    全班成绩录入系统
    直接选择排序
    冒泡排序
    直接插入排序
  • 原文地址:https://www.cnblogs.com/interfaceone/p/7514329.html
Copyright © 2020-2023  润新知