JVM理论:(三/7)关于类变量、成员变量、局部变量的案例总结

一、类变量、成员变量、局部变量的内存分布

  结合前文,对类变量、成员变量、局部变量三种变量的内存分布进行总结

1)类变量:方法区。静态变量随类加载到方法区中。方法区中存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。线程共享。

2)成员变量:堆。从父类继承下来或在子类中定义的各种类型的字段内容都存在对象的实例数据里,成员变量会在对象实例化时随着对象一起分配在Java堆中。线程共享。

3)局部变量:在栈帧里。一个方法对应一个栈帧,局部变量表占多大空间,在编译期存到Code属性里,局部变量需要赋初始值。结合操作数栈,操作数栈相当于是局部变量进行运算的场所。线程私有。

4)数组:堆。数组也是一个对象。https://www.cnblogs.com/duanxz/p/6102583.html

5)字符串常量 :运行时常量池。运行时常量池是方法区的一部分。常量池是Class文件里的结构,会在类加载后进入方法区的运行时常量池中存放。

二、类初始化顺序

回顾一下前面的知识:

对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

1)使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法的时候。(new、getstatic、putstatic、invokestatic)

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

扫描二维码关注公众号,回复: 2442623 查看本文章

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

类初始化的顺序如下:

  父类——静态块、初始化静态变量(静态块和静态变量根据代码先后顺序,只执行一次
    子类——静态块、初始化静态变量(静态块和静态变量根据代码先后顺序,只执行一次
  父类——非静态块、初始化非静态变量(也根据代码先后顺序)
  父类——构造方法
    子类——非静态块、初始化非静态变量(也根据代码先后顺序)
    子类——构造方法

//案例1 —— 针对初始化的时机
public class StaticTest {  
    public static int k = 0;  
    public static StaticTest t1 = new StaticTest("t1");  
    public static StaticTest t2 = new StaticTest("t2");  
    public static int i = print("i");  
    public static int n = 99;  
    public int j = print("j");        
    {  
        print("构造块");  
    }    
    static{  
        print("静态块");  
    }  
    public StaticTest(String str) {  
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);  
        ++n;  
        ++i;  
    }        
    public static int print(String str) {  
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);  
        ++i;  
        return ++n;  
    }     
    public static void main(String[] args) {  
        StaticTest t = new StaticTest("init");  
    }  
}

输出结果:
//要执行main方法前,会先初始化main方法所在的类StaticTest,对于Java来说,一旦开始初始化静态部分,无论是否完成,后续都不会再重新触发静态初始化流程了,类的静态初始化只会进行一次。下面一条一条对输出结果进行分析。

1:j i=0 n=0     
2:构造块 i=1 n=1
3:t1 i=2 n=2
//前3句一起分析,要执行main方法前,会先初始化main方法所在的类StaticTest,按顺序从上到下开始初始化静态变量,但是当执行到 public static StaticTest t1 = new StaticTest("t1"); 时发现需要先创建一个对象,
//这里虽然StaticTest类的初始化还未完成,但也不会再重新触发一次,不会再次初始化静态变量,所以直接开始初始化成员变量,直接执行 public int j = print("j"); ,所以第一句打印的是与 j 相关的,继续初始化对象,
//接着执行构造块和构造方法。值得注意的是,这时StaticTest类的静态变量i,n都还没执行到赋值语句,所以这时都还是系统的零值,为0。

4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
//4-6句与第一部分类似,创建另一个对象t2,这时也还没有执行到静态变量i,n的赋值语句。

7:i i=6 n=6
//开始执行静态变量i的赋值语句 public static int i = print("i"); ,会把n的值7返回给i,值得注意的是这里给i赋值后,前面不管i的值是什么都会变成新值。

8:静态块 i=7 n=99
//public static int n = 99; ,不管n之前的值为多少,这里初始化后的n为99,此时已经完成对类StaticTest静态变量的初始化,这句话是执行静态代码块时打印的。

9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102
//以上已经完成在执行main前对main方法所在类StaticTest的静态初始化,此时开始执行main方法里的语句StaticTest t = new StaticTest("init");,同样以成员变量,构造块,构造函数的顺序执行。
//案例2 —— 针对构造方法的选择
public class SonClass extends ParentClass{
  ParentClass parentClass;
  public SonClass(){
    System.out.println("1");
  }
  public SonClass(String name){
    System.out.println("2");
    this.name = name;
    parentClass = new ParentClass("FEI");
  }
  public static void main(String[] args) {
    System.out.println("------ main start ------ ");
    new SonClass("fei");
    System.out.println("------ main end ------ ");
  }
}
 
public class ParentClass{
  String name ;
  public ParentClass(){
    System.out.println("3");
  }
  public ParentClass(String name){
    System.out.println("4");
    this.name = name ;
  }
}

//如果子类没有显示地调用父类构造函数,不管子类是否存在带参数的构造方法都默认调用父类无参的构造函数,若父类没有无参的构造方法则编译出错。
输出结果:
------ main start ------ 
3        //调用子类构造方法SonClass(String name)前先调父类的构造,且没有显示调用父类构造方法,所以调的是ParentClass()
2        //继续执行子类构造方法SonClass(String name)
4        //执行到parentClass = new ParentClass("FEI");,会调用父类中带参的构造方法ParentClass(String name)
------ main end ------ 

 三、静态方法不能直接调用实例方法及成员变量

  实例方法内部可以直接使用静态方法,但是静态方法中则无法直接调用实例方法,也无法直接访问成员变量,编译会无法通过。

  通过Class文件中方法表里标识access_flags能知道这个方法是静态方法还是实例方法。在类加载的时候,就为静态方法分配了入口地址,而实例方法,只有在对象创建后才被分配入口地址。

  当我们创建第一个对象时,实例方法就分配了入口地址,当再创建对象时,不再分配入口地址,也就是说,方法的入口地址被所有的对象共享,当所有的对象都不存在时,实例方法的入口地址才被取消。

public class TestClass {
   private static void testMethod(){
        System.out.println("testMethod");
   }
   public static void main(String[] args) {
        ((TestClass)null).testMethod();
   }
}

输出结果:    
testMethod

//1)此处是类对方法的调用,不是对象对方法的调用。
//2)null可以被强制类型转换成任意类型(不是任意类型对象),于是可以通过它来执行静态方法。
//3)若将testMethod()方法前的static去掉,变成实例方法,必须依赖对象被创建后才能使用,会报空指针异常 。

补充一下关于null的知识:
  1)null可以被强制转换为任何引用类型。
  2)任何含有null值的包装类在自动拆箱成基本数据类型时都会抛出一个空指针异常。
  3)不能用一个值为null的引用类型变量来调用非静态方法,这样会抛出空指针异常,但是静态方法可以被一个值为null的引用类型变量调用而不会抛出空指针异常。

*注意static关键字所导致的内存泄漏问题

 https://blog.csdn.net/itm_hadf/article/details/7519598

 https://www.cnblogs.com/jichen/p/8522205.html

https://blog.csdn.net/weixin_35756522/article/details/77684031

 https://www.jb51.net/article/115613.htm

 https://blog.csdn.net/u013182381/article/details/74574278

https://blog.csdn.net/u013256816/article/details/50837863

http://www.importnew.com/18566.html

https://blog.csdn.net/lvsongsng91/article/details/52511724

 https://www.cnblogs.com/duanxz/p/6102583.html

猜你喜欢

转载自www.cnblogs.com/zjxiang/p/9296676.html