深入理解Java虚拟机-类加载连接和初始化解析

不管学习什么,我一直追求的是知其然,还要知其所以然,对真理的追求可以体现在方方面面。人生短短数十载,匆匆一世似烟云,我认为,既然来了,就应该留下一些有意义的东西。本系列文章是结合张龙老师的《深入理解JVM》视频做的一个笔记,其中将自己在学习过程中的实践记录、思考理解整合在了一起。希望在巩固自己的知识时让更多的朋友能够通过我的整合文章少走一些弯路。文中不免会有错误之处,无论什么东西,都应该带着怀疑的眼光去看待,拥有自己的独立思维是非常重要的一件事情,共勉。

方法论

每一个在学习的过程中都应该有一个方法论的存在,在学习之前我们要知道如何去学,学习本身也是一门功课,我们平时可以多多借鉴身边一些优秀的人的方法论,这是一种非常好的方式。学习分为两个方面,从人,从事。从事:只有自己亲身经历才能感悟到学习和理解相关技术所遵从的要点;从人: 这是一种更为高级的学习方式,我们应该吸收一些优秀的人身上的闪光点,就好比平时看书一样,这种方式成本是较低的,因为并不是我们亲身经历的,一定要有辨别能力,从其他人身上去借鉴出真正适合自己的闪光点。
当然从事情学习我们要付出的成本是较高的,俗话说不撞南墙不回头,当我们真正的遇到挫折了,失败了,踩到坑了,那么你才会真正的发现自己的方式存在哪些问题,哪些弊端。从这个过程中去吸收一些经验,从失败的教训当中获得一些总结,接着指导着你未来在前进过程中学习的方向。
更为高效的方式还是从人这个角度来去学习,因为不同的人做事情的方式是不一样的,那么你要从不同的人身上去吸收优秀的闪光点,通过自己的一系列的加工,比如说做笔记,写博客等等一系列的输出方式将别人所拥有的技能进而转换为自己所拥有的技能,并且把它真正固化成自己的一部分。这个跟我们去看书,看一些别的教程视频道理是一样的,在学习的过程中我们一定要保证自己有输入,当我们在看的时候就是一种输入,但是要注意到有输入就必须要有输出,没有输出的话你的输入效率非常非常低的,那什么是输出呢,做项目,记笔记,写博客都是一种很好的输出方式。
那么更为高层次的输出方式的话就是你给别人去讲,给别人分享你所掌握的这些个技能,这个对于个人来说的话是一个莫大的提升,我们可以在掌握学习一门技术之后一定要争取一切机会去给你周围的人去讲,这是一个非常非常好的学习方式,因为你在讲解的过程中你会遇到之前自己在输入的时候遗漏或者说根本就没有理解的这么一些技术点,要想给别人讲明白的首先一个前提是你自己得明白。换句话说就是你自己都不明白,你是不可能去给别人讲明白的。但是反过来说你自己明白了,真的能给别人讲明白的?其实也不一定。比如说在日常的工作学习中,别人去问你一些技术问题,可能这个问题你已经知道了,但是你跟别人去讲,那么讲的过程中可能会出现三种情况:第一种就是这个技术你明白了,讲完之后别人也明白了。这是最好的结果,说明你真正已经掌握了这门技术,第二种情况呢,就是这个技术你已经明白了,但是给别人讲完之后别人还是不明白,那么这说明什么呢,说明你可能在两个方面出现了偏差,第一是你对这个技术本身理解就出现了一些问题,你没有把他给真正有效的输出出来,第二个是你的掌握是没有问题的,但是呢你的表达方式是有问题的,第三种就是这个技术你明白,讲完之后呢你自己也不明白,然后呢对方也不明白,这是最差的一种结果,简称:装逼失败。那么一旦出现了这种问题你一定要去反思你对这个技术的理解或者是说你对这个技术的认识是不是真的达到了理解的程度。因为什么?那是因为当别人提出他自己对某个细节的观点时,如果理解不透彻时你会产生困惑,不知道哪个观点是正确的,和自己的理解不太一样,可能就颠覆了自己对于某一个技术点或者某一个技术体系的认知。接下来就好好好反思一下自己的学习是不是高效的,你所花费的时候是不是真的值得了,哪些时间是不是真的产生的相应的价值,这个价值最终体现在我们是不是真正彻底理解了这门技术。如何让一个小时的学习真的就产生一个小时的价值,其实是值得我们每一个人去仔细思考的问题。
很多人都容易出现的一种问题就是学习一种技术,为什么过了一段时间就忘了,很多细节当时理解的非常清楚透彻,但是过了一段时间就发现这些东西好像根本就没有学过一样,其实不光是你会产生这样的疑问,我在刚开始学习的时候也和大家犯了同样的错误:
这个问题我们可以从生物学的角度来分析一下,人的记忆分为瞬时记忆,短期记忆和长期记忆,当相同的讯息反复地被输送到海马体之后,讯息就会不断进入额叶,进而被长期保存形成长期记忆。

事实上,对记忆内容的每次回忆都会重新启动巩固记忆的完整过程,其中包括为形成新的突触终端而进行的蛋白质生成过程。一旦我们把显性存储的长期记忆送回工作记忆区,记忆内容就会再次变成短期记忆。当我们再次巩固这些记忆的时候,它又会获得一些新的神经连接这是一种新环境。约瑟夫.勒杜克斯解释说:“恢复记忆的大脑不是那个形成初始记忆的大脑。为了让老记忆能在当前大脑中生效,记忆必须及时更新。”生物记忆一直处于不断更新的状态。相形之下,存储在计算机中的记忆内容是静态的比特形式,你可以把这些比特数据从一个磁盘转移到另一个磁盘上,只要你愿意,转移多少次都可以,这些内容永远都会跟以前一样无比精确。
提出记忆外包这个想法的那些人也把工作记忆和长期记忆混为一谈了。当一个人无法在长期记忆区巩固一个事实、一个想法或者一次经验的时候,他是不会“释放”大脑空间,用来执行其他功能的。工作记忆区容量有限,而长期记忆区则具有不受限制的伸缩弹性,因为大脑具有生发、去除突触终端,不断调整神经连接强度的能力。二者因此形成鲜明对比。美国密苏里大学记忆研究专家纳尔逊.考恩(Nelson Cowan)写道:“正常的人脑不会像计算机那样,永远不会出现个人经历再也装不进记忆中的情况,人脑不会被塞满。”托克尔.科林博格表示:“长期记忆区能够存储的信息量实质上是无限的。”此外,也有证据表明,随着我们个人记忆内容的不断增加,我们的大脑也会变得更加敏锐。临床心理学家希拉.克罗威尔(Sheila Crowell)在《学习的神经生物学》(The Neurobiology of Learning)中解释说:“记忆这项行为可以按照某种方式调整大脑,让大脑今后更容易学会观念和技能。”
在我们存储新的长期记忆内容时,并不会抑制我们的脑力,相反还会提高脑力。记忆每增加一次,智力就会加强一些。网络为个人记忆提供了一个非常便利的补充,这种便利让人难以抗拒。但是,当我们开始利用网络代替个人记忆,从而绕过巩固记忆的内部过程时,我们就会面临掏空大脑宝藏的风险。

当涉及长期记忆的保存时,首先会在前额叶皮质形成一段静默的拷贝;在海马体的记忆痕迹被逐渐抹去的同时,这段记忆才被逐渐巩固下来。至于巩固长期记忆的因素是什么,论文第一作者北村隆表示,这还需要进一步的研究才能确定。
巩固记忆的另一个关键是前额叶皮质需要同时接收来自海马体和杏仁核的信息输入。杏仁核是大脑的情绪中枢。当研究人员切断其中任意一方的神经信号输入时(还是采用光控制技术),大脑皮层的记忆就无法巩固下来。

老生常谈温故而知新,要做到有输入的同时有输出,一定要把你学到的东西吐出来。将当时所理解的所有细节用笔记记下来,这样即使以后忘记的时候看到笔记是也能迅速的记忆起相关知识点。感觉和联想记忆差不多,当我们看到笔记时,总能联想起当时的状态,就好像找到了一把钥匙,打开记忆宝盒,虽然可能会是片段式记忆,但这就足够了。如果没有笔记,笔记记录的并不详细,就好像没有这把钥匙,那么你可能怎么也不会找到这片段的记忆,无法开启记忆宝盒,最终这段信息就会被大脑当作无用信息而被抹去。

了解

大家都知道Java这门语言应用的是非常广泛的,在语言编程的排行榜上Java与C总是不相上下的,Java一般来说都是占据第一名的,与其说是Java这门语言设计的非常棒,那么不如说呢是jvm这个平台设计的非常好,为什么这么说呢,因为在jvm这个平台上面它除了Java语言之外还可以运行其他基于jvm的语言。那么java本身呢跟jvm之间呢并不是一种紧密的绑定关系,jvm上运行的是什么?并不是java语言,而是所编译好的class文件。比如说字节码,任何一门语言只要能翻译成jvm所能理解并且能够执行的字节码,那么这门语言就可以很好的在jvm平台上去执行,去运行。这一点就体现出来jvm的强大所在。对于java来说他应该是jvm平台所能支持的第一门语言。字节码本身是有一种规范的,这个规范定义好了字节码每一部分都是什么样的内容,jvm是可以读懂的。一旦能够理解并且确认这个字节码文件是没有任何问题的,他就可以将其加载进来并执行相关的指令。
jmap(命令行)、jvisualvm和jconsole是java自带的jvm监控工具

类加载

在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。
这个类型指的是我们定义的class,我们定义的一个interface,枚举等等,注意这里面不涉及对象的概念,也就是new class()。类加载是在创建对象之前,换句话说要想创建对象,一定得要有这个对象所属的类,它的类型信息。你才能根据这个类型信息把这个对象在堆上面创建出来。类型在绝大多少情况下都是已经编写好的,如JDK提供的Object类,还有一些可以在运行期间动态生成出来,在编译之前是不存在的,最典型的就是动态代理,换句话说它是一种Runtime的概念,这一点和其他的很多编译型语言存在一个明显的区别,在很多语言处理过程中类型加载、连接与初始化特别是加载、连接是在编译阶段就完成了。但是java是在运行期间完成的,他为什么要这样去做?其实这样做的目的是给程序的开发人员或者说一些比较有创意的开发人员提供很多很多的可能性,在程序运行期间采取一些特殊的方式把之前已经存在或者在运行期才生成出来的类型有机的装配在一起。所以java本身是一门静态语言,但是具有的很多特性是动态语言才拥有的
说完了概念我们再来聊聊类加载具体场景: 将已经编写好编译完成的类的class文件从磁盘上面加载到内存,接下来是连接,连接其实要完成很多的处理过程,用比较简洁的一句话描述就是将类与类之间的关系确定好,对字节码的校验,因为字节码本身是可以人为生成和操纵的。校验完之后可能还涉及到类与类之间的调用关系,比如说将类与类之间的符号引用换成直接引用
第三个阶段叫初始化,我们对于类型里面一些静态的变量进行赋值。

类加载器深入剖析

Java里面每一个类型比如说java.lang.String...最终的数据结构信息都会进入jvm管理的内存当中,那么是怎么进入的呢?就是由类加载器去完成的,就是一个用于加载类的工具。也就是说每一个类型都是由类加载器加载到内存当中的

  • 在如下几种情况下,Java虚拟机将介绍生命周期
    • 显式的执行了System.exit()方法
    • 程序正常执行结束
    • 程序在执行过程中遇到了异常没有cache住被抛到了main方法main方法再往上抛程序就退出了或错误而异常终止
    • 由于操作系统出错导致java虚拟机进程终止

类的加载、连接与初始化

  • 加载:查找并加载类的二进制数据
  • 连接
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量(类名.变量)分配内存,并将其初始化为默认值(类型的默认值不是变量的默认值)
    • 解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值
  • java程序对类的使用方式可分为两种
    • 主动使用
    • 被动使用
  • 所以的Java虚拟机实现必须在每个类或接口被Java程序"首次主动使用"时才初始化他们

什么是主动使用?

  • 创建类的实例 new一个class对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法 第二种和第三种可以看作是一种情况,因为它们反映到字节码助记符上大体上是一样的。访问在JVM层面上字节码用的是getstatic指令,赋值写入是putstatic,而调用类的静态方式使用的是invokestatic
  • 反射(如Class.forName("com.a.b"))得到类的Class对象
  • 初始化一个类的子类 new一个父类的子类 父类会被初始化,以此类推 Object会被初始化
  • Java虚拟机启动时被标明为启动类的类(Java a) 包含Main方法的类
  • JDK1.7开始提供的动态语言支持:Java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,PEF_invokeStatic句柄对应的类没有初始化,则初始化
    除了上述七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,但是有可能加载或连接

类加载解析

类的加载指的是将类的.class文件中的二进制数据加载到内存里,将其放在运行时数据区的方法区内(JDK1.8为元空间),然后在内存中创建一个Java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构,一个类多个实例对应的Class对象只有一份,Class对象可以看作一面镜子,它可以反射出class文件在方法区里所有的内容和结构。这就是反射的根源

  • 加载Class文件的方式
    • 从本地系统中直接加载
    • 通过网络下载.class文件
    • 从zip,jar等归档文件中加载.class文件 jar包
    • 从专有数据库中提取.class文件
    • 将Java源文件动态编译为.class文件 动态代理 jsp编译成为的class文件

接下来我们来实例演示一下

package com.airsky.jvm.classloader;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Child1.string);
    }
}
class Parent1{
    public static String string="Hello AirSky"; //使用static关键字定义一个静态变量 值为Hello AirSky
    static { //定义一个静态代码块,在程序加载时会被执行
        System.out.println("父类初始化");
    }
}
class Child1 extends Parent1{
    static {
        System.out.println("子类初始化");
    }
}

来看一下输出

可以发现子类是未被执行的,我们来看看程序到底做了什么,通过Main函数打印输出子类的str属性,由于子类继承了父类,所以他可以直接访问到str属性,再来做一个实验

package com.airsky.jvm.classloader;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Child1.string2); //调用子类的静态变量
    }
}
class Parent1{
    public static String string="Hello AirSky"; //使用static关键字定义一个静态变量 值为Hello AirSky
    static { //定义一个静态代码块,在程序加载时会被执行
        System.out.println("父类初始化");
    }
}
class Child1 extends Parent1{
    public static String string2="Hello World"; //在子类定义一个静态变量
    static {
        System.out.println("子类初始化");
    }
}

这次我们在子类新增一个静态变量,然后在Main函数中调用它,再来看看运行结果

我们发现这次父类和子类都被执行了 原因是什么呢?原来对于静态字段来说,只有直接定义了该字段的类才会被初始化,换句话说由于string是被父类定义的,我们访问了父类的string属性,这种情况称为对Parent1的主动使用,因此父类被初始化了,虽然我们用了Child1的类名,但是却没有主动使用该类。第二种情况为什么父类也被初始化了呢?我们再来看看主动使用中的一种情况就是初始化一个类的子类,所以父类也被初始化了 Java也声明了当一个类在初始化时,要求其父类全部都已经初始化完毕,因为Java是单继承的,父类还有父类的话,会引发链式初始化,一直进行到Object,并且初始化只进行一次,所以Object类是第一个被初始化的

posted @ 2020-01-21 11:33  AirSkys  阅读(608)  评论(0编辑  收藏  举报