热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JVM类加载过程原理/实例

简介本文介绍Java的类加载过程,有实例。基础加载过程加载链接(验证准备解析)初始化使用卸载加载通过一个类的全限定名获取定义

简介
        本文介绍Java的类加载过程,有实例。

基础
加载过程

加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载

加载
通过一个类的全限定名获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存(不一定在堆,对于HotSpot在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
链接
验证:检查载入的class文件数据的正确性
准备:给类变量(静态变量)分配内存(方法区)并设置为零值(0、false、null等)。
           例外:static final类型。static final int a = 12; 在准备阶段就将a赋值为12。
解析(可选):将常量池内的符号引用替换成直接引用
初始化:初始化类变量(静态变量)、执行静态语句块
执行类变量(静态变量)的赋值动作和静态语句块(赋值与语句块是按定义的顺序进行的)。优先级:静态、父类、子类)
注意:初始化是操作类变量(也就是Class的变量),不是对象的变量。
使用:以new一个对象为例
若是第一次创建 Dog 对象(对象所属的类没有加载到内存中)则先执行上面的初始化操作。
在堆上为 Dog 对象(包括实例变量)分配空间,所有属性都设成默认值(数字为 0,字符为 null,布尔为 false,引用被设成 null)
初始化实例:给实例变量赋值、执行初始化语句块
执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数
执行构造函数。
虚拟机规范严格规定: 有且只有 5 种情况 必须立即对类进行初始化(加载、验证、准备自然需要在之前执行):

遇到 new 、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,若没有对类进行初始化,则要先触发其初始化。
这4个指令含义是:使用 new 新建一个 Java 对象,访问或者设置一个类的静态字段,访问一个类的静态方法。大数据培训
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
当虚拟机启动的时候,用户需要指定一个需要执行的主类(包含 main 方法的那个类),虚拟机会先初始化这个类。
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
验证类加载

package org.example.a;

class Tester1 {
//如果静态变量的赋值放到静态代码块之后,则会报错:非法向前引用
static int i = 2;

static{
System.out.println("初始化类:i = " + i);
    }

Tester1(){
System.out.println("构造方法");
    }

public void test(){
System.out.println("test 方法");
    }
}

public class Demo {

public static void main(String[] args) throws ClassNotFoundException {
ClassLoader cl = ClassLoader.getSystemClassLoader();
//装载类
cl.loadClass("org.example.a.Tester1");
System.out.println("装载类");
//初始化类
Class.forName("org.example.a.Tester1");
System.out.println("初始化结束");
Tester1 test = new Tester1();
test.test();
    }
}

运行结果

装载类
初始化类:i = 2
初始化结束
构造方法
test 方法

问答
为什么静态方法不能调用非静态方法和变量?

       静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接去访问。非静态成员(变量和方法)属于类的对象,只有该对象实例化之后才存在,然后通过类的对象去访问。

       加载阶段的第二步((2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构)会将所有static修饰的内容(包括静态属性、静态代码块、静态方法等)转为方法区的运行时数据结构,但此时非静态的方法和变量根本没经过初始化(没有内存),所以会失败。

实例验证
类初始化顺序
package org.example.a;

class Test{
int a = instanceMethod();
static int b = staticMethod();

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

    {
System.out.println("instance block");
    }

public Test(){
System.out.println("constructor");
    }

public int instanceMethod(){
System.out.println("instance method");
return 2;
    }

public static int staticMethod(){
System.out.println("static method");
return 2;
    }
}

public class Demo {
public static void main(String[] args) {
Test child = new Test();
    }
}

运行结果

static method
static block
instance method
instance block
constructor

手动装载/初始化
package org.example.a;

class Test{
int a = instanceMethod();
static int b = staticMethod();

static {
System.out.println("static block");
    }
    {
System.out.println("instance block");
    }
public Test(){
System.out.println("constructor");
    }

public int instanceMethod(){
System.out.println("instance method");
return 2;
    }

public static int staticMethod(){
System.out.println("static method");
return 2;
    }
}

public class Demo {
public static void main(String[] args) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
//装载类
cl.loadClass("org.example.a.Test");
System.out.println("装载类结束");
//初始化类
Class.forName("org.example.a.Test");
System.out.println("初始化结束");
Test test = new Test();
test.instanceMethod();
        } catch (ClassNotFoundException e) {
e.printStackTrace();
        }
    }
}

执行结果

装载类结束
static method
static block
初始化结束
instance method
instance block
constructor
instance method

静态域的访问
访问类或接口的静态域时,只有真正声明这个域的类或接口才会被初始化

package org.example.a;

class B {
static int value = 100;
static {
System.out.println("Class B is initialized."); //输出
    }
}
class A extends B {
static {
System.out.println("Class A is initialized."); //不会输出
    }
}

public class Demo {
public static void main(String[] args) {
System.out.println(A.value); //输出100
    }
}

执行结果

Class B is initialized.
100

线程安全
简介

Java类的加载和初始化过程都是线程安全的。具体原因如下:

加载
类加载整个过程是线程安全的,因为loadClass方法内有synchronized。
初始化
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
总结

        类的静态资源都是线程安全的,而且是单例的。因此,在写单例模式时,经常会使用static标记实例变量。比如:

public class Singleton {
private static Singleton instance = new Singleton();

private Singleton() {
    }

public static Singleton getInstance() {
return instance;
    }
}

加载源码

见java.lang.ClassLoader#loadClass:

下边方法前边有个注释:

Unless overridden, this method synchronizes on the result of method during the entire class loading process.

//除非被重写,否则整个装载过程中是线程安全的。注意:本处装载是装载、链接、初始化的装载,而不是整个加载流程

protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
        } else {
c = findBootstrapClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
      }

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
      }
    }
if (resolve) {
resolveClass(c);
    }
return c;
  }
}

Class对象
基础
其他网址

《深入理解Java虚拟机 JVM高级特性与最佳实践 第2版》=> 第7章 虚拟机类加载机制

代码实例

        一旦类被加载了到了内存中,不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址(对于HotSpot是方法区)上的Class引用。

package org.test.a;

class Cat{
static {
System.out.println("static cat");
    }
}

public class Demo {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Cat.class;
Class c2 = new Cat().getClass();
Class c3 = new Cat().getClass();
Class c4 = Class.forName("org.test.a.Cat");
System.out.println(c1 == c2);
System.out.println(c2 == c3);
System.out.println(c3 == c4);
    }
}

输出结果

static cat
true
true
true

分析

        内存的字节码块就是完整的把整个类装到了内存。使用的主要步骤如下:

当一个ClassLoder启动的时候,ClassLoader生存在jvm中的堆。
ClassLoder去主机硬盘上将A.class装载到jvm的方法区。
new A()时:虚拟机使用方法区中的字节文件在堆内存生成了一个A字节码的对象
然后A字节码这个内存文件有两个引用:一个指向A的class对象,一个指向加载自己的classLoader


静态内部类的加载时机
说明

        只有当我们有对类的引用的时候,才会将类初始化。比如以下情况

new一个非静态类的对象
访问静态类的成员(包括方法和属性)
        new一个外部类的时候,加载阶段会扫描static修饰的东西,初始化阶段只会执行static修饰的属性的赋值以及static代码块。利用这个特性,可以做基于静态内部类的单例模式:​​​​

测试

登录后复制
package org.example.a;

class OuterClass {
static {
System.out.println("OuterClass static");
    }
public static class InnerClass {
static int a = 1;
static{
System.out.println("InnerClass static");
        }

static void innerMethod(){
System.out.println("innerMethod");
        }
    }
}

public class Demo {
public static void main(String[] args) {
//OuterClass outerClass = new OuterClass();
//OuterClass.InnerClass.a = 2;
//OuterClass.InnerClass.innerMethod();
    }
}

只放开第一个测试的执行结果

OuterClass static

只放开第二个测试的执行结果

InnerClass static 

只放开第三个测试的执行结果

InnerClass static
innerMethod
 


推荐阅读
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
author-avatar
无声胜有剩
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有