一道JAVA面试题所引出的知识点

当我看到这道面试题时有点晕,由于平时没关注ClassLoader

下面给出的JAVA中ClassLoader的描述正确的是?()

A.ClassLoader没有层次关系

B.所有类中的ClassLoader都市AppClassLoader

C.通过Class.forName(String className),能够动态加载一个类

D.不同的ClassLoader加载同一个文件,所得的类是相同的

解析:A.错误,ClassLoader具备层次关系

            B.错误,ClassLoader不止一种

            D.错误,不同的类装载器分别创建同一个类的字节数据属于完全不同的对象,没有任何关联

答案:C

一.什么是JVM

        Java虚拟机(JVM)是Java语言的基础。它是Java技术的重要组成部分。Java虚拟机(JVM)是一个抽象的计算机,和实际的计算机一样,它具有一个指令集并使用不同的存储区域。

       Java虚拟机,java源文件(.java)通过编译器生成字节码文件(.class),字节码文件(.class)通过JVM(Java虚拟机)中的解释器再翻译成特定机器上的机器码。 编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。当一个程序从开始运行一个程序,这时虚拟机就开始实例化了。多个程序启动就会存在多个虚拟机实例。程序退出或者关闭。则虚拟机实例消亡。多个虚拟机实例之间数据不能共享。

二.JVM的体系结构

如下图所示,JVM的体系结构包含几个主要的子系统和内存区:

1.垃圾回收器(Garbage Collection):负责回收堆内存(Heap)中没有被使用的对象,即这些对象已经没有被引用了。

2.类装载子系统(Classloader Sub-System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。

3.执行引擎(Execution Engine):负责执行那些包含在被装载类的方法中的指令。

4.运行时数据区(Java Memory Allocation Area):又叫虚拟机内存或者Java内存,虚拟机运行时需要从整个计算机内存划分一块内存区域存储jvm需要用到的东西。而这个运行时数据区里面又会分为许多的小区。每个区都有自己不同的职责。后面会讲到每个区的作用和存储的内容。 

                      push_thumb[23]

三:jvm运行时数据区
1.程序计数器 (Program Counter Register):

         是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实 现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

         由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

         如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.栈 (stack):

        这块区域是线程私有的,它的生命周期和和线程一样。在jvm中。每个方法执行的时候jvm都会创建一个栈帧。用来存储局部变量、操作栈、动态栈和方法返回等信息。每一个方法从调用到结束对应着一个栈帧在虚拟机里面的入栈和出栈。

       栈里面存储了编译器已知的八种基本数据类型,String不在里面。后面会对String类型进行详细的介绍。(boolean、char、double、int、long、byte、float、short)。 
在用java关键字 new 一个对象时。比如(ControllerHelper helper = new ControllerHelper ())时。stack内存中存储的是一个helper栈帧地址引用。其地址指向的是heap中的内存区域。如下图: 

                                                          1_thumb[13]

注意: 存Heap地址引用只对于sun 公司的虚拟机<HotSpot>。其他公司的可能会不一样。存的可能是句柄。

3.本地方法区 (Native Method Area)

    这块区域在jvm运行内存中职责就相对比较少了。只是执行Native 方法。如果这个区的内存不足也是会抛出StackOverflowError 和 OutOfMemoryError 异常。

4.堆 (Heap)

        这块区域可以说是jvm中最大的一块区域了。几乎所有的对象和数据都是存在这个区域。这块区域也是线程共享的。也是 gc 主要的回收区。可以从我第一个图看出Heap区中还可以分为新生代(Young Generation)和老年代(Old Generation)。gc每隔一段时间就会对新生代进行垃圾回收。在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC之后仍无法满足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC(Full GC)。而新生代又可以分为一个Eden空间和两个Survivor (From Survivor 和 To Survivor ) 空间。新生代中的E区和S区又有不同的职责。 
       E区:gc 触发比较频繁的区域。存储的是新new的对象。几乎所有的对象都会经过E区。如果gc过后对象还没死亡就会把未死亡的对象存储到S区。 
       S区:Survivor 区作为Eden区和old(老年代)的缓存。它是可以向老年代转移活动对象的实例。

java 堆它在屋里内存上可以是不连续的内存空间。只要是逻辑上是连续的就可以了。查看程序新生代、老年代信息可以执行命令 jstat -gcutil 6912 500

5.方法区 (Method Area)

       方法区和堆一样也是线程共享的区域,它主要是存储被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。它不属于Heap区域中的一部分。对于sun公司的HotSpot虚拟机来说。gc也会对这块区域进行垃圾回收。但是对于heap的垃圾回收而已。这块区域垃圾回收比较少。String str=”is string”。str就是存在方法区。Sample.class.getSimpleName() 方法就是从方法区中区得的结果。

        如果细分方法区还可以细分为运行常量池 (Runtime Constant Pool) 。它主要存储class文件中的版本、字段、方法、接口等描述信息。运行常量池还可以分为信息常量池 (Constant Pool Table)。它主要是存储编译器生成的各种字面量和符号引用。

       并不是所有的常量都只能在编译期产生。对于String类型的 intern( )方法就可以在运行时把变量存入方法区。当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。

四.ClassLoader知识

1.ClassLoader基本概念:ClassLoader是JVM实现的一部分,JVM运行时会产生三个ClassLoader(分别是:Bootstrap ClassLoader; 

 Extension ClassLoader;   AppClassLoader),其中 AppClassLoader继承自 Extension ClassLoader, Extension ClassLoader继承自Bootstrap ClassLoader(是由C++程序实现);

2.ClassLoader的加载流程:当运行一个程序的时候,JVM启动,运行Bootstrap ClassLoader(启动类加载器),该ClassLoader加载JAVA核心API( Extension ClassLoader和AppClassLoader此时也被加载),然后调用 Extension ClassLoade加载扩展API,AppClassLoader加载classpath目录下的定义的Class,这就是程序最基本的加载流程

   3.使用了 Parent Delegation模型(父类委托模型)

在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载

为什么要Parent Delegation?安全。因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。

比如在Applet中,如果编写了一个java.lang.String类并具有破坏性。假如不采用这种委托机制,就会将这个具有破坏性的String加载到了用户机器上,导致破坏用户安全。但采用这种委托机制则不会出现这种情况。因为要加载java.lang.String类时,系统最终会由 Bootstrap进行加载,这个具有破坏性的String永远没有机会加载。

4. 扩展ClassLoader
          我们目的是从本地文件系统使用我们实现的类装载器装载一个类。为了创建自己的类装载器我们应该扩展ClassLoader类,这是一个抽象类。我们创建一个FileClassLoader extends ClassLoader。我们需要覆盖ClassLoader中的findClass(String name)方法,这个方法通过类的名字而得到一个Class对象。

     public Class findClass(String name)
     {
         byte [] data = loadClassData(name);
         return defineClass(name, data, 0 , data.length);
     }

    我们还应该提供一个方法loadClassData(String name),通过类的名称返回class文件的字节数组。然后使用ClassLoader提供的defineClass()方法我们就可以返回Class对象了。

     public byte [] loadClassData(String name)
     {
         FileInputStream fis = null ;
         byte [] data = null ;
         try 
         {
             fis = new FileInputStream( new File(drive + name + fileType));
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             int ch = 0 ;
             while ((ch = fis.read()) != - 1 )
             {
                 baos.write(ch);
               
             }
             data = baos.toByteArray();
         } catch (IOException e)
         {
             e.printStackTrace();
         }
        
         return data;
     }


其他:forName与ClassLoader
Class clazz = Class.forName("XXX.XXX");与ClassLoader cl = Thread.currentThread().getContextClassLoader();Class clazz = cl.loadClass("XXX.XXX");都可以装载一个类那么他们的区别是什么呢?

进一步研究Class.forName()是调用Class.forName(name, initialize, loader); 
也就是Class.forName("XXX.XXX"); 等同与 Class.forName("XXX.XXX", true, CALLCLASS.class.getClassLoader());
第二个参数表示装载类的时候是否初始化该类, 即调用类的静态块的语句及初始化静态成员变量。

Class clazz = cl.loadClass("XXX.XXX"); 没有指定是否初始化的选项。只有执行clazz.newInstance();时才能够初始化类。可以说 Class.forName("XXX.XXX", false, cl)执行过程是一致的。只是ClassLoader.loadClass()是更底层的操作。

Context ClassLoader
Context ClassLoader提供一个突破委托代理机制的后门。虚拟机通过父子层次关系组织管理ClassLoader,每个ClassLoader都有一个 Parent ClassLoader(BootStartp不在此范围之内),当要求一个ClassLoader装载一个类是,他首先请求Parent ClassLoader去装载,只有parent ClassLoader装载失败,才会尝试自己装载。

但是,某些时候这种顺序机制会造成困扰,特别是jvm需要动态载入有开发者提供的资源时。就以JNDI为例,JNDI的类是由bootstarp ClassLoader从rt.jar中间载入的,但是JNDI具体的核心驱动是由正式的实现提供的,并且通常会处于-cp参数之下(注:也就是默认的 System ClassLoader管理),这就要求bootstartp ClassLoader去载入只有SystemClassLoader可见的类,正常的逻辑就没办法处理。怎么办呢?parent可以通过获得当前调用 Thread的方法获得调用线程的Context ClassLoder 来载入类。

Classloader存在下面问题:
在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的 NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。如ClassLoaderA,装载了类A的类型实例A1,而ClassLoaderB,也装载了类A的对象实例A2。逻辑上讲A1=A2,但是由于A1和A2来自于不同的ClassLoader,它们实际上是完全不同的,如果A中定义了一个静态变量c,则c在不同的 ClassLoader中的值是不同的。

因此,研究JBoss的ClassLoader策略,对于更好地实现EJB组件拼装是用好处的,因为,一个项目中可能要用其他项目的EJB组件,如何实现运行时EJB组件共享,如何实现EJB组件打包是很重要的。




  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值