深入理解Java虚拟机_03虚拟机执行子系统

来源:互联网 发布:java action定义 编辑:程序博客网 时间:2024/05/03 16:59

0、问题:

1、代码如下:

public class Test05{    public static void main(String[] args)    {        Child1 c1 = new Test05.Child1();        Child2 c2 = new Test05.Child2();        c1.test();        c2.test();    }    public static class Parent    {        public static int   a   = 1;    }    public static class Child1 extends Parent    {        public void test()        {            a = 2;        }    }    public static class Child2 extends Parent    {        public void test()        {            System.out.println("a:" + a);        }    }}为什么Child1修改a变量, Child2变量会发生变化;

遇到new、getstatic、putstatic或invokestatic这4条指令时, 如果类没有进行过初始化, 则需要先触发其初始化.

当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化. 既去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载, 解析和初始化过. 如果没有, 那必须先执行相应的类加载过程.

对于静态字段, 只有直接定义这个字段的类才会被初始化, 因此通过其子类来引用父类中定义的静态字段, 只会触发父类的初始化而不会触发子类的初始化.

1、编译阶段的常量传播优化 :

public class ConstClass{    static {            System.out.println("ConstClass init");    }    public static final String HELLOWORLD = "hello world";}public class Test {    public static void main(){        System.out.println(ConstClass.HELLOWORLD);    }}

打印结果为: “hello world”, 并没有打印ConstClass init.
因为在编译阶段通过常量传播优化, 已经将此常量的值”hello world”存储到了Test类的常量池中, 以后Test对常量ConstClass.HELLOWORLD的引用实际都被转化为Test对自身常量池的引用了. 也就是说,实际上Test的Class文件之中并没有ConstClass类的符号引用入口, 这两个类在编译成Class之后就不存在任何联系了.

2.必须立即对类进行初始化的场景:

有且只有5种情况必须立即对类进行”初始化”
1. 遇到new, getstatic, putstatic或invokestatic这4条字节码指令时, 如果类没有进行过初始化, 则需要先触发其初始化.
2. 使用java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行初始化, 则需要先触发其初始化.
3. 当初始化一个类的时候, 如果发现其父类还没有进行初始化, 则需要先触发其父类的初始化.
4. 当虚拟机启动时, 用户需要指定一个要执行的主类(包含main()方法的那个类), 虚拟机会首先初始化这个主类.

3.Class对象:

Class对象比较特殊, 它虽然是对象, 但是存放在方法区里面;

4.方法区:

方法区与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即使编译器编译后的代码等数据.

5.类加载过程:

  1. 加载
  2. 验证 :
  3. 准备 :

    1、准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
    2、为变量分配的内存都属于方法区的内存
    3、这阶段进行内存分配的仅包括类变量(被static修饰的变量)

  4. 解析

  5. 初始化
  6. 使用
  7. 卸载

6.初始化:

类初始化阶段是类加载过程的最后一步, 具体讲就是执行类构造器< clinit>()方法的过程.
< clinit>()方法执行过程中一些可能会影响程序运行行为的特点和细节:

1、< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序所决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在他之后的变量, 在签名的静态语句块可以赋值, 但是不嫩访问.

public class Test{    static{        i = 0;                  //可以编译通过        System.out.println(i);  //非法向前引用(illegal forward reference)    }    static int i = 1;}

2、< clinit>()方法与类的构造函数不同, 它不需要显示的调用父类构造器, 虚拟机会保证在子类的< clinit>()方法执行之前, 父类的构造器方法已经执行完毕.
3、由于父类的< clinit>()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作.

static class Parent{    public static int A = 1;    static{        A = 2;    }}static class Sub extends Parent{    public static int B = A;}public static void main(String[] args){    System.out.println(Sub.B);}

7.类加载器:

1、比较两个类是否”相等”, 只有在这两个类是由同一个类加载器加载的前提下才有意义, 否则, 即使这两个类来源于同一个Class文件, 被同一个虚拟机加载, 只要加载他们的类加载器不同, 那么这两个类就必定不相等.

public static void main(String[] args) {    ClassLoader classLoader = new ClassLoader() {        @Override        public Class<?> loadClass(String name) throws ClassNotFoundException {            try {                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";                InputStream is = getClass().getResourceAsStream(fileName);                if (is == null) {                    return super.loadClass(name);                }                byte[] b = new byte[is.available()];                is.read(b);                return defineClass(name, b, 0, b.length);            } catch (IOException e) {                e.printStackTrace();            }            return name.getClass();         }    };    try {        Object obj = classLoader.loadClass("com.example.classload.Test1");        System.out.println("obj:" + obj);        System.out.println("" + (obj instanceof com.example.classload.Test1));    } catch (ClassNotFoundException e) {        e.printStackTrace();    }}

打印结果为false, 类Object(有classLoader加载)和Test1(系统类加载器加载)是由两个类加载器加载

0 0
原创粉丝点击