类加载机制,类的加载过程;静态成员和非静态成员;静态代码块和非静态代码块。

Java类加载机制详解
本文详细介绍了Java类加载机制,包括类的加载过程、类加载器的工作原理及双亲委派机制。此外,还对比了静态成员与非静态成员的区别。

类加载机制

  1. 什么是类的加载
      类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
      类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
  2. JVM的类加载机制主要有如下3种
  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

PS:双亲委派机制
在这里插入图片描述

  双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
  双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

类加载的时机

  • 隐式加载 new 创建类的实例,
  • 显式加载:loaderClass,forName等
  • 访问类的静态变量,或者为静态变量赋值
  • 调用类的静态方法
  • 使用反射方式创建某个类或者接口对象的Class对象。
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行某个主类

类的加载过程

在这里插入图片描述

加载(Loading)

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 将内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种访问数据的入口。

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载,典型场景:Web Applet
  • 从zip,jar等归档文件中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多是:动态代理技术
  • 由其它文件生成,典型场景JSP应用
  • 从专有数据库中提取.class文件,比较少见
  • 将Java源文件动态编译为.class文件

链接(Linking)

  1. 验证
    目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
  2. 准备
    为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中
  3. 解析
    主要是把常量池中的符号引用替换成直接引用

初始化(Initialization)

  • 初始化阶段就是执行类构造器方法< clinit >()的过程。
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • < clinit >()不同于类的构造器。(关联:构造器是虚拟机视角下的< init >())
  • 若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()己经执行完毕。
  • 虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁。

类加载器

类加载器基本概念

  顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

  类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

静态成员和非静态成员

  1. 概念
      这里的普通方法和成员变量是指,非静态方法和非静态成员变量
    首先static是静态的意思,是修饰符,可以被用来修饰变量或者方法。
静态成员非静态成员
static成员变量有全局变量的作用非static成员变量则没有全局变量的作用 ,局部变量是类的方法里面的变量
static变量是静态变量,当加载类时即已加载非static变量实例对象时加载
static变量是静态变量当改变其中的值,其它实例中的该static变量也会改变非static变量只会在具体所在的那个对象中改变,不会影响其它实例

例子: static int i=1; 这里的i就是一个静态变量。具体特性如下
静态成员变量与普通成员变量

  • 内存分配

  静态静态,就是当类被加载时,这个内存就静静的待在这里,就已经存在,而普通成员变量,只有类被实例化时才分配内存。

  • 调用方法

  成员变量需要对象才能被调用;而静态成员变量可以直接通过类来调用,当然也可以直接调用:

public class demo {
	static int i=10;
	public static void main(String[] args){
		int k=i;//直接调用
		int j=demo.i;//通过类来调用
		System.out.println("直接调用k:"+k);
		System.out.println("通过类来调用j:"+j);
	}
}
  • 生命周期

  静态成员变量从类被加载开始到类被卸载,一直存在;普通成员变量只要在类被实例化也就是创建对象是才开始存在,一直到对象结束,生命也告终。

  • 共享方式

  静态成员变量是全类共享的,即使不是对象,也可以调用,并且内存不变;普通成员变量是每个对象单独享用的,就是说你这个对象有一个int i=1,我再给该类创建一个新的对象,他也有一个int i,但他可以不等于1,可以等于5,也可以等于10,但无论等于多少,都不会影响之前对象的i的值,而静态成员变量不是这样。

静态方法与普通方法

  我们一直用的main方法就是静态方法,在此我只讨论一下调用的问题

  • 静态方法中不可直接调用非静态方法,但是非静态方法中,可以直接调用静态的。
  • 静态方法可以调用静态成员变量,调用普通成员变量时需创建对象;非静态方法可以随意调用静态成员变量或普通成员变量。
  • 静态方法和普通方法调用特性同上,静态调用普通方法时需创建对象,而普通方法随意。

静态方法的使用情况
  是在类中执行该方法时,该方法并不对成员变量有任何操作,也就是说该静态方法的操作不针对对象,只针对该类时,使用。那么使用该类也就不需要创建对象,而可以直接通过类名.方法名实现。

java中的类修饰符、成员变量修饰符、方法修饰符
  default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

类修饰符:

类修饰符描述
public(访问控制符)将一个类声明为公共类,他可以被任何对象访问,一个程序的主类必须是公共类。
abstract将一个类声明为抽象类,没有实现的方法,需要子类提供方法实现。
final将一个类生命为最终(即非继承类),表示他不能被其他类继承。
friendly默认的修饰符,只有在相同包中的对象才能使用这样的类

成员变量修饰符:

成员变量修饰符描述
public(公共访问控制符)指定该变量为公共的,他可以被任何对象的方法访问。
private(私有访问控制符)指定该变量为公共的,他可以被任何对象的方法访问。
private(私有访问控制符)指定该变量只允许自己的类的方法访问,其他任何类(包括子类)中的方法均不能访问。
protected(保护访问控制符)指定该变量可以别被自己的类和子类访问。在子类中可以覆盖此变量。
friendly在同一个包中的类可以访问,其他包中的类不能访问。
final最终修饰符,指定此变量的值不能变。
static(静态修饰符)指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类。
transient(过度修饰符)指定该变量是系统保留,暂无特别作用的临时性变量。
volatile(易失修饰符)指定该变量可以同时被几个线程控制和修改。

方法修饰符:

方法修饰符描述
public(公共控制符)指定此方法为公共的,他可以被任何类访问
private(私有控制符)指定此方法只能有自己类等方法访问,其他的类不能访问(包括子类)
private(私有访问控制符)指定该变量只允许自己的类的方法访问,其他任何类(包括子类)中的方法均不能访问(包括子类)。
protected(保护访问控制符)指定该方法可以被它的类和子类进行访问。
final指定该方法不能被重载。
static指定不需要实例化就可以激活的一个方法。
synchronize同步修饰符,在多个线程中,该修饰符用于在运行前,对他所属的方法加锁,以防止其他线程的访问,运行结束后解锁。
native本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。

静态代码块和非静态代码块

  静态代码块是以static修饰的代码块,反之没有用static修饰的代码块为非静态代码块,如下所示:

静态代码块:

static{
	若干代码
}

非静态代码块:

{
	若干代码
}

它们之间的区别主要如下:

  • 静态代码块在虚拟机加载类的时候就执行(只执行一次),而非静态代码块每一次new的时候都会执行一次。
  • 由于静态代码块在虚拟机加载类的时候就执行,因此在非静态代码块在静态代码块后执行。

验证静态代码块比非静态代码块先执行:

public class Test {
 
	public static void main(String[] args) {
		{
			System.out.println("代码块被执行");
		}
	}
	static {
		System.out.println("静态代码块被执行");
	}
}

结果:

静态代码块被执行
代码块被执行

PS:静态代码块不能放在main方法中,不然会编译错误。(Syntax error on token “static”, delete this token)
所以是先执行静态代码块而不是main。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值