Java虚拟机(三) —— 类加载

概述

类加载:将类从class文件加载到内存中,并且对字节码进行校验、准备、解析和初始化,最终转换为可以被JVM直接使用的Java类型。

类加载的过程

加载——验证——准备——解析——初始化——使用——卸载

准备

虚拟机在准备阶段为类变量(static修饰)分配内存,并设置类变量的初始值。这些内存是在哪里分配的?堆里?答案是方法区。
而实例变量则是在初始化阶段随对象一起分配在堆内存。而且这里要注意private static int a = 1;在准备阶段,a为0而不为1,在初始化阶段,a才被赋为1

解析

解析阶段是将 常量池中的符号引用替换为直接引用

初始化

遇到下列4种情况,必须对类进行初始化
(1)有new、getstatic、putstatic、invokestatic
(2)反射调用时,如果类没有初始化,就必须首先对类初始化
(3)初始化一个类时,如果父类没有被初始化,就首先对父类进行初始化。
(4)虚拟机启动时需要指定一个主类(main方法所在的类),虚拟机必须首先对其初始化

监控类的加载

-XX:+TraceClassLoading

被动引用不会触发类的初始化

(1)子类引用父类的静态字段,不会引起子类的初始化


class Father{

    public static int a = 1;

    static{
        System.out.println("father");
    }

}

class Son extends Father{

    public static int b = 2;

    static {
        System.out.println("son");
    }

}


public class ClassLoaderDemo {

    public static void main(String[] args) {
        System.out.println(Son.a);
    }

}

原因是,该静态变量虽然是子类引用,但底层还是属于父类的。

(2)通过数组定义引用类,不会触发类的初始化


public class ClassLoaderDemo {

    public static void main(String[] args) {
        Father[] fathers = new Father[10];
    }

}

只加载,并未初始化。

(3)常量在编译的阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量类的初始化


class Father{

    public static final int a = 1;

    static{
        System.out.println("father");
    }

}

public class ClassLoaderDemo {

    public static void main(String[] args) {
        System.out.println(Father.a);
    }

}

对象占用空间计算

dir

类加载器

运行期间动态加载

JVM对class文件并不是全部加载,而是按需加载,需要的时候才会加载进来。

启动时加参数-verbose:class,打印类加载顺序。


class A{
    static {
        System.out.println("A");
    }
}
public class ClassLoaderDemo2 {

    public static void main(String[] args) {
        System.out.println(String.valueOf("test"));
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        A a = new A();
    }

}

运行结果,会发现只有当执行到new A()是,A类才会被加载进来。

双亲委派机制

先委托父类加载器寻找目标类,在找不到的情况下,在自己的路径中查找目标类。

是否可以创建一个全限定名相同的String类

不可以。当AppClassLoader加载该String时,判断java.lang.String已经加载,便不会再次加载。所以执行的依旧是jdk中的String,但是系统的java.lang.String中没有main()方法,所以会报错。这是一种安全机制。


package java.lang;

public class String {

    public static void main(String[] args) {
        System.out.println("test");
    }

}

异常信息如下

那么String是由哪个加载器加载的?

打印为null,说明是启动类加载器加载的,由于该加载器是C++实现,所以返回null。


public class ClassLoaderDemo2 {

    @Test
    public void testStringClassLoader(){
        // 打印为null,说明是启动类加载器加载的,由于该加载器是C++实现,所以返回null
        System.out.println(String.class.getClass().getClassLoader());
    }

}

启动类加载器(Bootstrap CLassLoader)

无法被Java程序直接引用。它属于JVM内部使用的类加载器,既没有上级类加载器,也没有下级类加载器,因此不遵守ClassLoader的上级委托机制,只能算是JVM的一个类加载工具。

扩展类加载器(Extension CLassLoader)

应用程序类加载器(Application CLassLoader)

1、为什么Java可以跨平台
因为有java虚拟机,跨平台是因为字节码即class文件具有平台无关性,java代码会经过java虚拟机转换为字节码

2、class文件的结构
class文件主要是以8位字节码为基本单位的二进制文件,主要的存储方式是以类似于c语言的结构体来存储,其中的两种基本类型为无符号数和表,其中表中又会有其他的表和一些无符号数。就好比结构体中有一些变量,还有一些其他的结构体
class文件的不同位置是固定的,第一个字节码是魔数,紧接着是次版本、主版本、常量池变量数,常量池变量表.......


<dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>4.0.0</version>
</dependency>


//计算指定对象及其引用树上的所有对象的综合大小,单位字节
long RamUsageEstimator.sizeOf(Object obj)

//计算指定对象本身在堆空间的大小,单位字节
long RamUsageEstimator.shallowSizeOf(Object obj)

//计算指定对象及其引用树上的所有对象的综合大小,返回可读的结果,如:2KB
String RamUsageEstimator.humanSizeOf(Object obj)


public class A {


    static{
        System.out.println("A");
    }


}

public class B extends A{

    static{
        System.out.println("B");
    }

}



posted @ 2016-07-18 23:43  清泉白石  阅读(276)  评论(0编辑  收藏  举报