Jvm(56),虚拟机类加载机制----类加载的过程----初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是

执行类构造器<clinit>()方法的过程。我们在下文会讲解<clinit>()方法是怎么生成的,在这里,我们先看一下<clinit>()方法执行过程中一些可能会影响程序运行行为的特点和细节,这部分相对更贴近于普通的程序开发人员。

上面的意思就是说在初始化的时候,也就是在编译的时候变量已经有初始值了,而不是运行的时候,这个时候我们也能更加亲切的看到常量都是在编译的时候已经有值了,所以它是不可以变更的。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如代码清单7-5中的例子所示。

public
class
Test {

static
{

i = 0?// 给变量赋值可以正常编译通过

System.out.print(i)?// 这句编译器会提示"非法向前引用"

}

static
int
i = 1?

}

<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()

方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

注意这里这里说的初始化的范围比构造方法的初始化的范围要宽泛好多,这里说的初始化主要有静态代码块,静态的变量,它发生在构造方法之前,原因就是我们前面所说的,当虚拟机在准备阶段的时候就把静态的数据放到方法区中(常量池)了。

由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。看下面的代码。

package
demo.jvm.test6?

public
class
Parent {

public
static
int
A = 1?//第一步

static
{

A = 2?//第二步,把2赋值给A了

}

static
class
Sub extends
Parent {

public
static
int
B = A?//现在B值就是2了

}

public
static
void
main(String[] args) {

System.out.println(Sub.B)?

}

}

结果是2

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<

clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

多线程的初始化;

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中

有耗时很长的操作,就可能造成多个进程阻塞[2],在实际应用中这种阻塞往往是很隐蔽的。

例如:


package
demo.jvm.test6?

import
java.util.concurrent.ExecutorService? import
java.util.concurrent.Executors?

class
DeadLoopClass {
static
class
Hello {

static
{

System.out.println(Thread.currentThread() + "我现在在类的初始化的过程")?

try
{

Thread.sleep(5000)?

} catch
(InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace()?

}

}

}

public
static
void
main(String[] args) {

ExecutorService threatpool = Executors.newFixedThreadPool(20)?

int
i = 0?

pool-1-thread-6---结束 pool-1-thread-9---结束 pool-1-thread-5---结束 pool-1-thread-4---结束 pool-1-thread-7---结束 pool-1-thread-1---结束 pool-1-thread-12---结束 pool-1-thread-2---结束 pool-1-thread-10---结束 pool-1-thread-14---结束 pool-1-thread-11---结束 pool-1-thread-13---结束 pool-1-thread-15---结束 pool-1-thread-16---结束 pool-1-thread-17---结束 pool-1-thread-19---结束 pool-1-thread-18---结束 pool-1-thread-20---结束 pool-1-thread-8---结束

从上面的例子我们可以看到在初始化的过程中是会加锁的,是线程安全的。相当于加了一个 sychronized的锁。

但是一旦在初始化的过程中出现阻塞,那么整个过程都会出现阻塞的情况,我们来看下面的案例。


package
demo.jvm.test6?

import
java.lang.reflect.Executable? import
java.util.concurrent.ExecutorService? import
java.util.concurrent.Executors?

import
demo.jvm.test6.DeadLoopClass.Hello?

class
DeadLoopClass2 {
static
class
Hello {

static
{

if
(true) {


pool-1-thread-14---开始 pool-1-thread-12---开始

Thread[pool-1-thread-5,5,main]我现在在类的初始化的过程 pool-1-thread-13---开始 pool-1-thread-15---开始 pool-1-thread-16---开始 pool-1-thread-20---开始 pool-1-thread-17---开始 pool-1-thread-19---开始 pool-1-thread-18---开始

//在这里我们可以看到线程就一直阻塞在这了。因为代码被锁死在初始化的代码块中了。

ll

原文地址:https://www.cnblogs.com/qingruihappy/p/9691477.html

时间: 2024-10-15 05:27:30

Jvm(56),虚拟机类加载机制----类加载的过程----初始化的相关文章

Jvm(55),虚拟机类加载机制----类加载的过程----解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在前一章讲解 Class文件格式的时候已经出现过多次,在Class文件中它以CONSTANT_Class_info. CONSTANT_Fieldref_info.CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢? 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

虚拟机类加载机制------类加载的过程

1.加载 虚拟机需要干三件事: ①.通过一个类的的全限定名来获取定义此类的二进制字节流(没有规定二进制字节流从那里获取,怎样获取,许多java技术也都建立在这基础上) ②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(将常量池转变成运行时常量池) ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区着各类的各种数据的访问入口. 相比较于类加载过程的其他阶段,非数组类获取类的二进制字节流的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器

虚拟机类加载机制——类加载时机

由于道行不够深,所以此篇类加载机制的讲解主要来自于<深入理解Java虚拟机——JVM高级特性与最佳实践>的第7章 虚拟机类加载机制. 在前面<初识Java反射>中我们在开头提到要了解Java反射,就得要了解虚拟机的类加载机制.在这里,我们来试着窥探一下何为类加载. “虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,类型的加载.连接和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.”这句话确实读着好懂,但到底类加载做了什么呢?我们都知道

Jvm(51),虚拟机类加载机制----类加载的时机

在了解下面的举的例子之前我们先来了解一下类的加载顺序? 1 public class test1 { 2 public static void main(String[] args) { 3 C c = new C(); 4 } 5 } 6 7 class A{ 8 int a = 0; 9 Method m = new Method(a); 10 static int a1 = 10; 11 static{ 12 System.out.println("A:执行静态代码块A"+a1)

JVM类加载机制---类加载的过程

一.类加载的时机 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用.卸载 7个阶段,其中验证.准备.解析 3个部分统称为 连接. 二.具体步骤解析 1.加载 加载阶段,虚拟机要完成以下3件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流: 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构: 3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口. 相对于类加载过

虚拟机类加载机制--类加载器

准备阶段的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何如获取所需要的类.实现这个动作的代码模块称为"类加载器" 1.类与类加载器 每一个类加载器都有一个独立的类名称空间,由类加载器和类一起合作才能确定一个类在虚拟机中的唯一性.也就是说:比较两个类是否"相等",即使他们来自同一个Class文件,在同一个虚拟机上被加载,如果加载它们的类加载器不同,那么这两个类就不相等. 这里的

Java虚拟机 - 类加载机制

[深入Java虚拟机]之四:类加载机制 类加载过程     类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).另外注意这里的几个阶段是按顺序开

Java 虚拟机程序执行:02 虚拟机的类加载机制

虚拟机的类加载机制 虚拟机的类加载机制 类加载的时机 类的显式加载和隐式加载 类加载的过程 类的生命周期 加载 加载的 3 个阶段 分类 验证 准备 解析 初始化 类加载器 如何判断两个类 “相等” 类加载器的分类 双亲委派模型 类加载的时机 JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作.也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次.那么什么是主动引用,什么是被动引用呢? 主动引用

两道面试题,带你透彻解析Java类加载机制

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.ou