虚拟机类加载器

7416970-260a42fa31e9e492.gif

引言

今天在听美团保障性平台Rhino负责人的分享时了解到了Java探针技术,由于对这块不是很清楚,遂上网查了些资料,了解到要想使用它,先要熟悉虚拟机的类加载机制,于是我又去复习了一遍类加载器的知识,这里就把相关的知识点简单总结一下(挖个坑,关于Java 探针技术等以后搞的稍微明白点再来写)。

类加载机制

了解类加载器之前,我们需要先了解Java中的类是如何加载到虚拟机以及如何被虚拟机解析的,也就是虚拟机的类加载机制。

由于Java是运行时进行类的加载,所以就不能在程序开始运行之前将所有的类一股脑全部加载进虚拟机,类的加载需要一些特定的时机,比如我们 new 一个 object 的时候,这个object就会被虚拟机加载,或者读取一个类的静态字段的时候,还有对类进行反射调用的时候(记得吗,使用反射时都要先获取class对象),还有一些其他的连带加载机制,典型的比如使用子类时发现父类未加载则会先加载父类(还记得做过的那些子父类中打印的笔试题吗),还有一些其他的情况会导致类被加载进虚拟机,这里也穷举不全,想要详细了解的同学可以去看Sun公司(现在是Oracle公司了)的虚拟机规范。了解了类加载的时机,接下来说下类加载的渠道,即类可以通过哪些渠道被加载进虚拟机。最典型的比如 jar包,war包,再如通过动态代理运行时写入,或者由其他文件生成(比如 JSP,JSP最终会被编译成servlet,这是Java Web基础)等等方式。读取时通过获取类的全限命名来唯一确定一个类,这也是我们使用com.xx.xx做包名的原因--防止全限命名重复。

类被加载进虚拟机就能被直接使用吗?我们知道我们编写的 .java 文件最后都会被编译成字节码文件 .class 文件。如果我们在编译后手动修改了 class文件的内容,那么这个类的行为甚至语法就会发生异常,所以虚拟机会进行class文件的验证(主要是语法与格式的验证),防止出现class文件异常的情况。我们平常在IDE写代码时的语法错误其实就是提前进行了验证。

class文件已经加载进虚拟机了,接下来是类的准备阶段,这个阶段主要为类的静态变量设置初始值(注意是初始值而不是真正的值)。接着是解析阶段,将字符串常量池中的符号引用替换为直接引用(字符串常量池不仅仅存放字符串,还有符号引用【包括类和接口的全限命名,字段名称和描述符,方法名称和描述符】),关于字符串常量池以及虚拟机区域的划分可以查看我之前的文章。,所谓的直接引用也就是真实的内存地址。

最后就是类的初始化阶段了,通俗的说就是类的构造器执行的阶段,然而这里会涉及一些静态字段,子父类等概念,所以它的执行过程也是很复杂(当年被父子类谁先打印的问题搞得头疼)。其实这个谁先打印的问题只要搞明白了构造函数的执行顺序就比较简单了(这里的构造函数代指类的构造器与类变量赋值与静态语句块的集合),首先子类的构造函数执行之前会执行父类的构造函数(Object的构造函数最先执行),由于父类构造函数先执这也意味着父类的类变量赋值静态语句块优先于子类执行,所以总结一句话就是父类优先于子类。这里说一个小tip,我们都知道多线程会有并发问题,那么当多个线程都对这个类进行初始化的时候会不会发生并发问题呢?可是我们也没有对构造器进行加锁呀,这是因为加锁的操作虚拟机已经替我们完成了,不得不感叹,并发问题无处不在。

总结一下,类的加载过程主要分为以下几个阶段,加载--验证--准备--解析--初始化--使用

类加载器

终于步入主题了。。了解了类的加载过程,该说下它的加载工具了。类加载器的定义为“通过一个类的全限命名来获取此类的二进制码流这个动作放到虚拟机外部来实现,以便让应用程序自己去决定如何去获取所需的类,实现这个动作的模块叫做类加载器”。

虚拟机内部的加载器有3种,启动类加载器(加载  \lib目录下的类),扩展类加载器(加载 \lib\ext目录下的类)以及应用程序类加载器(加载程序classpath下的类)。除了虚拟机自带的3种,我们也可以自定义类加载器。它们之间的关系如下。

7416970-feb8e33ca89da214.png
来自wiki

它们是一个从上到下的一个模型,这个模型称为双亲委派模型,它的意思是是如需加载一个类,它会一层层向上委托给“父亲”来加载,即最先尝试的是启动类加载器,如果“父亲”不能加载再依次往下尝试,直至加载成功。这样做的好处是保证了系统安全性,比如加载Object类总是由启动类加载器加载,即使我们用自己定义的类加载器来加载我们自己写的Object类,它最终也会委派给启动类加载器,保证了系统内的Object类都是同一个(不同类加载器加载的对象不相等),这样保证了系统的正确性(Obejct是所有类的父类 )。

双亲委派模型的破坏:上面说到使用双亲委派模型可以保证类能够被正确的类加载器加载,然而想象一下这个场景,Java内部的代码需要调用用户写的代码,这样使用双亲模型是完成不了的,典型的场景如JDBC服务,Java负责写好规范接口,然后让各个数据库厂商自己去实现接口(这个就是Java的SPI机制,Dubbo的插件原理也是利用SPI来实现的)。这个时候就需要线程上下文类加载器了,它能够加载用户的代码,如果线程本身未设置线程类加载器,会从父线程继承一个,如果父类也未设置,那么这个加载器就是应用程序类加载器。数据库厂商的JDBC代码就是使用线程上下文加载器加载的。(注:关于双亲模型被破坏的另一个例子--OSGI,我没有研究过,感兴趣的可以自己查找下资料)。

推荐观看:https://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html

参考:深入理解Java虚拟机

最后

最后说点题外话....最近网上一直都有裁员的信息曝出来,说是互联网寒冬已经到来,感觉慌慌的呢,我才刚入行,难道还没开始就已经要结束了吗.....啥时候可以不用担心工作就好了啊啊啊啊啊啊啊

猜你喜欢

转载自blog.csdn.net/weixin_33709590/article/details/87144160
今日推荐