深入理解java虚拟机(4)---类加载机制

时间:2022-09-06 19:42:44

  类加载的过程包括:

  加载class到内存,数据校验,转换和解析,初始化,使用using和卸载unloading过程。

除了解析阶段,其他过程的顺序是固定的。解析可以放在初始化之后,目的就是为了支持动态加载。

从java开发者来讲,我们并不关心具体细节,只要知道整个流程以及每个流程大体干了那些事情。

每个流程具体对开发代码会有那些影响就可以了。

类的加载流程

1.加载loading

  在加载过程中,虚拟机需要完成3件事情:

1)通过一个类的全限定名来获得此类的二进制字节流。

2)将这个直接流的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的class对象,作为方法区这个类的数据访问入口。

2.验证

验证是虚拟机非常重要的一步,其目的是为了确保class文件的字节流符合java虚拟机自身的要求,不会导致虚拟机崩溃。

java语言本身是比较安全的语言,它没有数组越界等情况的发生。But,class语言并不是一定由java语言产生的。甚至于,

可以直接使用16进制工具编写class文件。而这些文件就不能保证class文件的规范性。

大致分成4个阶段的验证过程:文件格式验证元数据验证字节码验证符号引用验证
 
文件格式验证:
比如是否以魔数开头,主次版本号是否在虚拟机可处理范围之内,常量池是否有不支持类型等。
经过这个阶段的验证之后,字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的
 
元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求,这个阶段可能包括的验证点有:
这个类是否有父类,父类是否集成了不允许继承的类,如果不是抽象类是否实现了其父类或接口中要求实现的所有方法,类中的字段和父类是否有矛盾
 
字节码验证:
最复杂的一个解读那,主要工作是进行数据流和控制流分析。这阶段对类的方法体进行校验分析,保证该方法在运行时不会做出危害JVM安全的行为,例如:
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,保证跳转指令不会跳转到方法体以外的字节码指令上,保证方法体中的类型转换是有效的。。。
这个验证并不能保证一定安全(停机问题,通过程序去校验程序逻辑是无法做到绝对准确的
1.6加入StackMapTable功能对这个阶段做了优化,提高速度,但这个StackMapTable也可能被篡改,可以通过启动参数来关闭这个选项。
 
符号引用验证:
这个阶段发生在虚拟机将符号引用转化为直接引用的时候。这个转化动作将在连接的第三个阶段----解析阶段中发生
可以看作是对类自身以外的信息进行匹配性的校验。
比如:符号引用中通过字符串描述的全限定名是否能找到对应的类,是否存在所描述的方法和字段。。。。
如果无法通过符号验证,将会抛出一个Java.lang.IncompatibleClassChangeError异常的子类,比如Java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError
可以使用启动参数来关闭大部分类验证措施,缩短虚拟机类加载时间
 

3.准备

准备阶段就是为类的变量正式分配内存并设置初始值。这个初始值与初始化不是同一个概念。

比如

public static int value = 12;

这个阶段value的值为0 而不是12。value赋值为12的阶段

是在初始化的过程中出现的。
 
java所有的基本类型都赋值为零值。(简单来说就是0 or null,0.0f,false等)
这里可以明确,类的属性是会默认初始值的。而局部变量没有初始值。所以是未定义的。
 

4.解析

解析是java语言面向对象的基础。

解析的过程是将常量池里面的字符引用替换为直接引用的过程。

符号引用是 一组以符号来描述所引用的目标。各种虚拟机的内存布局可以各不相同,但是字面量的形式有虚拟机规范严格规定。

直接引用就是对虚拟机内存布局的直接描述。

所以引用的目标必须已经加载到内存里面了。

1).类或接口的解析

类和接口的解析:假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要以下三个步骤:

如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C

如果C是数组类型,并且数组的元素类型是对象,则按照1的情况处理。如果元素类型不是对象,则由虚拟机生成一个代表此数组维度和元素的数组对象

如果上述步骤没有异常,C在虚拟机中实际已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具有对D的访问权限,如果没有则抛出java.lang.IllegalAccessError异常。

2).字段解析

大体情况如下:

class D{

  public D(C c)
{
string a = c.a;
}
}

D 需要加载C.a 字段,首先,需要加载的是C类的解析内容。然后关键部分就是java语言继承的东东了。

如果C类本生就含有a的字段,直接返回a的直接引用。

搜索C类的接口,按照继承关系从上到下搜索各个接口已经父接口,直到找到a字段。如果没有

if C is not the java.lang.Object,同上,搜索C类的父类,如果有,就使用该字段的直接引用。如果没有

也就是C类及其相关类or接口没有这个字段,查找失败。

如果找到,还需要进行权限验证。

如果接口 & 类 都包含相同名的字段,java程序员有时候会无法判断到底使用的是哪个字段。

所以编译器一般会拒绝这种情况的发生。

以下是使用androidstudio 实验的结果:

public interface ICoo {
public static int A = 1;
} public abstract class CooAbstruct {
public int A = 22;
} public class Coo extends CooAbstruct implements ICoo { // public int geta()
// {
// return A;
// }
}
public class Doo {

    public Doo(Coo c)
{
int a = c.A;
}
}

E:\GitHub\jvmdemo\app\src\main\java\com\joyfulmath\myapplication\Doo.java:13: 错误: 对A的引用不明确, CooAbstruct中的变量 A和ICoo中的变量 A都匹配
int a = c.A;
^
1 个错误

可以看到,编译器明确 无法区分A到底是使用哪个字段。

在C++的多继承中,类似的情况在使用时需要明确到底是使用哪个子类的字段。

3)类的方法加载:

同样使用C类来描述这个过程:

类方法和接口方法 常量类型是分开的。所以如果C类方法发现是一个接口的方法的话,直接回抛出异常。类型检测。

直接在C类里面寻找是否有匹配的字符描述的方法。没有就继续

在C类的父类里面递归寻找,没有就继续

在C类的接口里面递归寻找,找到,说明本方法未被实现,C类是抽象类。抛出异常

都没有找到,nosuchmethod。

如果找到有效的匹配方法后,检查权限。

4)接口的加载方法

过程同类的方法基本一致。只是不需要进行权限检查。

5.初始化

初始化和准备阶段是不同的过程,而且是java程序员最关心的部分。

1.必须初始化的情况

java虚拟机规范 规定了5种 (有且仅有)情况下,必须进行初始化的操作。

1)遇到new,getstatic,putstatic,invokestatic 这4条指令的时候。对应场景:

实例化一个类,读取或者设置一个类的静态字段,调用一个类的静态方法时候。

2)使用反射方法调用的时候,需要先初始化。

3)当初始化一个类时,需要先初始化父类。

4)当虚拟机启动时,需要指定一个启动类(main类),虚拟机会首先初始化这个类。

5)当使用jdk1.7动态语言时候,具体情况本文不做分析。

一下使用几个demo来说明我们容易误解的地方:

public class MainActivity extends Activity {

    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TraceLog.i(String.valueOf(SubClass.value));
} }
public class SubClass extends SuperClass {
static { TraceLog.i("subclass init!");
}
}
public class SuperClass {
static {
TraceLog.i("SuperClass init!");
} public static int value = 12;
}

结果log:

05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/SuperClass: <clinit>: SuperClass init! [at (SuperClass.java:13)]
05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/MainActivity: onCreate: 12 [at (MainActivity.java:19)]

是的,只有父类被初始化了,子类没有初始化,why?

应为value定义的父类,所以只需要初始化父类就可以的。

public class SuperClass {
static {
TraceLog.i("SuperClass init!");
} public static int value = 12; public SuperClass()
{
TraceLog.i("SuperClass construct");
}
}

实例化construction函数没有走到,所以没有实例被创建!!!but,我们在看log,<clinit> 这个是神马?这个就是打印SuperClass.init所在的函数!!!

这个等到下面在讲,我们继续我们的demo。

    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// TraceLog.i(String.valueOf(SubClass.value));
TraceLog.i();
SuperClass[] a = new SuperClass[10];
}

05-08 10:22:33.100 12438-12438/com.joyfulmath.myapplication I/MainActivity: onCreate:  [at (MainActivity.java:21)]

what? 对于SuperClass 没有一行log,也就是根本没有初始化SuperClass。

它触发了一个类为“[xxx.Superclass“ , 这是SuperClass对应的数组类,是由虚拟机自动生成的。

 TraceLog.i(a[0].toString());
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
at com.joyfulmath.myapplication.MainActivity.onCreate(MainActivity.java:23)
at android.app.Activity.performCreate(Activity.java:5961)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1129)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2364)

a[0] 居然是null? 是的,数组a里面都是null。对的a只是一个数组,a的类型为”[xxx.Superclass“ 不是SuperClass。所以数组不会自动初始化元数据。

常量。

常量存放在常量池里面,所以对常量的引用在编译阶段就已经被优化。

下面我们来讲讲<clinit> 这个东东。

静态代码块+所有类的变量的赋值动作。

这里有一点需要强调:编译器收集的顺序与由源代码在文件中的顺序是一致的。

<clinit>()方法是由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并而成。编译器收集的顺序和语句在源文件中出现的顺序一致,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问

<clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证父类的<clinit>()在子类的之前完成。因此,虚拟机执行的第一个<clinit>()方法肯定是java.lang.Object.

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

先看个例子来说明上述概念:

public class SuperClass {
public static int A = 1; static {
A = 2;
TraceLog.i("SuperClass init!");
} public static int value = 12; public SuperClass()
{
A = 3;
TraceLog.i("SuperClass construct");
}
}
public class SubClass extends SuperClass {
public static int B = A;
static { TraceLog.i("subclass init! B:"+B);
}
}

如图所示:父类里面A有3个地方赋值。那么B到底是多少呢?

subclass 在给B赋值以前,会首先走完superclass的<clinit>.所以 A的值是2.

so, B输出的值 就是2.在B赋值的时候,构造函数没有调用。(construction操作只有在实例化的时候,会被调用!)

<clinit>()方法并不是必须的,如果一个类没有静态语句块也没有对变量赋值操作,就不会生成

接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成<clinit>()方法,但与类不同的是,接口的<clinit>()方法不需要执行父接口的<clinit>()方法。只有当父几口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。

类加载器

关于类的解析,C++等语言都是有编译器执行器或者说IDE环境解决了,我们也无法进行干预。

但是java是由虚拟机来加载类,一般情况下虚拟就就可以加载类。But如果通过网络下发类,就会转化成2进制的代码

由于加密的原因,这个类无法被虚拟机解析,所以需要我们自己写类加载器来解析这个类。这是java流行的一个重要原因。

而目前流行的技术就是OSGi。OSGi是非常好的一个代表,所以关于这部分的内容,如果OSGi研究后,就可以非常了解类加载器。

6.1 类 & 类加载器

如何确定2个类是相同的,包括equals & instanceof等。

相同的二进制代码,由不用的加载器加载,对应的是不同的类型对象。

所以判断相同的类对象,必须是相同的二进制代码+相同的类加载器。

6.2 双亲委派模型

除了顶层加载器之外,所有的加载器都有父加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码。

工作过程:
   如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,
   只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
  好处:
   Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
   判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类。

参考:

《深入理解java虚拟机》 周志明著

http://wangwengcn.iteye.com/blog/1618337

深入理解java虚拟机(4)---类加载机制的更多相关文章

  1. 深入理解Java虚拟机(类加载机制)

    文章首发于微信公众号:BaronTalk 上一篇文章我们介绍了「类文件结构」,这一篇我们来看看虚拟机是如何加载类的. 我们的源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行.虚拟机 ...

  2. 【进阶之路】深入理解Java虚拟机的类加载机制(长文)

    我们在参加面试的时候,经常被问到一些关于类加载机制的问题,也都会在面试之前准备的时候背好答案,但是我们是否有去深入了解什么是类加载机制呢?这段时间因为一些事情在家看了些书,这次就和大家分享一些关于Ja ...

  3. 深入理解java虚拟机【类加载机制】

    Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 在加载阶段,java虚拟机需要完成以下 ...

  4. 深入理解Java虚拟机之类加载机制篇

    概述 ​ 虚拟机把描述类的数据从 Class 文件加载到内存中,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,就是虚拟机的类加载机制. ​ 在Java语言里面,类型的 ...

  5. 深入理解java虚拟机&lpar;三&rpar;-----类加载机制

    类加载机制jvm把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被jvm直接使用的java类型.在java中,类型的加载.连接和初始化都是在程序运行期间完成的 ...

  6. 深入理解Java虚拟机&lpar;八&rpar;——类加载机制

    是什么是类加载机制 Java虚拟机将class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是类加载机制. 类的生命周期 一个类从加载到内存 ...

  7. java 虚拟机的类加载机制

    Java 虚拟机的类加载机制 关于类加载机制: ​ 虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行效验.转换解析和初始化,最终 形成可以被虚拟机直接使用的Java 类型,就是虚拟机的类 ...

  8. Java虚拟机:类加载机制详解

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 大家知道,我们的Java程序被编译器编译成class文件,在class文件中描述的各种信息,最终都需要加载到虚拟机内存才能运行和使用,那么 ...

  9. java虚拟机的类加载机制

    引言 我们写的代码是放在.java文件中,经过编译器编译后,转成.class文件.Class文件是一串二进制流,它可以被各平台的虚拟机所接受,实现跨平台.      虚拟机将描述类的数据从class文 ...

  10. 《java虚拟机》----类加载机制

    No1: 实现语言无关性的基础仍然是虚拟机和字节码存储格式,虚拟机只与Class文件这种特定的二进制文件格式所关联,并不关心Class的来源是何种语言. No2: Class文件是一组以8位字节为基础 ...

随机推荐

  1. I2C总线和S5PV210的I2C总线控制器

    一.什么是I2C通信协议? 1.物理接口:SCL + SDA (1)SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道. (2)SDA(serial ...

  2. php随笔(一)

    之前的开发一直用的都是Thinkphp框架,对原生的php很不了解,近日打算把以前的项目拿一个出来用原生php再重写一次,顺便再把TP框架拆开好好分析分析. 之前的android开发虽说对面向对象的思 ...

  3. C&num;常用操作类库三&lpar;XML操作类&rpar;

    /// <summary> /// XmlHelper 的摘要说明. /// xml操作类 /// </summary> public class XmlHelper { pr ...

  4. Codeforces Beta Round &num;85 &lpar;Div&period; 1 Only&rpar; A&period; Petya and Inequiations 贪心

    A. Petya and Inequiations Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest ...

  5. C&plus;&plus; 线程的创建,挂起,唤醒,终止

    例子: 线程代码: DWORD __stdcall ThreadProc(LPVOID lpParameter) { CMultiThreadDlg * pdlg = (CMultiThreadDlg ...

  6. The Suspects&lpar;并查集求节点数&rpar;

    The Suspects Time Limit: 1000MS   Memory Limit: 20000K Total Submissions: 28164   Accepted: 13718 De ...

  7. Mac系统实现git命令自动补全

    当我第一次使用mac电脑的时候,由于我是从事软件开发的程序员,所以必须经常要使用到git,然而发现在mac系统下,git不能实现命令的自动补全,然后网上查找资料,找到了解决办法,终于可以实现了git命 ...

  8. Centos7快速部署saltstack

    saltstack是一个和ansible差不多的自动化运维工具,可以用来批量管理大量主机 OS:centos7.3 server:172.16.13.159 client: 172.16.13.156 ...

  9. threejs path controls example html

    <!DOCTYPE html> <html lang="en"> <head> <title>three.js webgl - pa ...

  10. 在VC中使用SendInput函数实现中文的自动输入

    很早以前写了一个刷卡程序,功能是定时监控读卡器,当发现有IC卡放到读卡器上后,自动识别出卡号,然后带着这个卡号搜索一个英文用户名和卡号的对照表,最后把英文用户名直接自动输入到当前光标所在的位置.本来程 ...