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");
}
}
关于作者
后端程序员,五年开发经验,从事互联网金融方向。技术公众号「清泉白石」。如果您在阅读文章时有什么疑问或者发现文章的错误,欢迎在公众号里给我留言。