• 【转】深入理解JVM


    http://www.cnblogs.com/liupeizhi/articles/1942764.html

    1.类的加载:

    在java中可分为四类加载器

    引导类加载器 Bootstrap Loader   用来加载%java_home%lib下的核心类库像String、Date等等

    扩展类加载器 Extension Loader   用来加载%java_home%lib/ext下的扩展api

    系统类加载器 AppClassLoader   加载classpath下面的类文件,我们的所有类文件默认都是由它来加载的,怎么样,感觉亲切吧

    用户自定义的类加载器

    下面我们举例说明类的加载

    public class A{

    public static void main(String args[]){

    B b=new B();

    }

    }

    Public class B{

    Public static String a="hello";

    public static String b=getValue();

    Static{

    System.out.println("Hello World");

    }

    Public static void getValue(){

    }

    }

    假如我们自己定义了一个名字为A 的类,当我们启动虚拟机即(java A)的时候,会创建一个JVM实例。现在我们就看类B 的加载过程。由于B是被A 引用 的,所以B是由A的类加载器进行加载。在这里我们不得不说一下类加载器是比较孝顺的孩子,为什么这么说呢,因为类加载器在加载类的时候采用双亲委托机制。简单说就是在加载类的时候,加载器会调用父类加载器来加载,父类再调用父类,依次类推。这个说起来比较抽象,我们这里给出源代码来表示一下其中的加载过程:

    public Class loadClass(String name){

    ClassLoader parent=this.getClassLoader().getParent();

    Try{

    Class c=findLoadedClass(name);

    //如果这个类没有被加载

    If(c!=null){

    //如果有父类加载器

    If(parent!=null)

    parent.loadClass(name);

    Else

    BootstrapLoader.loadClass(name)

    }catch(FileNotFoundException ex){

    //如果父类加载器找不到,就调用自己的findClass查找

    this.findClass(name);

    }

    //如果这个类已经被加载

    Else

    Return c;

    }

    这代码是我自己写的,是对源代码的简化表示,不要直接拷贝使用,如果想要知道详细内容,建议参源码。这段可以完全清晰地表示出类加载器的调用关系了。但是里面有个问题,相信各位都会发现了,就是 BootstrapLoader.loadClass(name).BootstrapLoader为什么不创建实例呢?因为BootstrapLoader并不是用java写的,是一个本地方法(native),也就是说是用c/c++或者其他语言编写的方法。为什么要这么做呢?主要是因为我们的类加载器也是类,如果他们都是用java实现,那么他们如何加载?所以,sun给了我们一个引导类加载器用来加载其他的类加载器,之后我们才能用这些类加载器加载我们的类文件。这里我们说一下他们的父子关系。

    我们自定义的类加载器的父类加载器是  AppClassLoader

    AppClassLoader的父类加载器是Extension Loader

    Extension Loader的父类加载器是 Bootstrap Loader

    当我们加载类B的时候,由于没有指定它的类加载器,默认由AppClassLoader进行加载,调用loadClass()方法,AppClassLoader发现它的parent不是null,就会调用父类加载器(Extension Loader)加载,Extension Loader发现它的父母是null(因为BootstrapLoader 不是java写的,所以不会被Extension Loader访问到)于是就调用BootstrapLoader来加载,由于我们的B类是在我们的classpath中,所以必然会产生ClassNotFoundException ,接着调用自己的findClass进行查找,ExtensionLoader访问的是%java_home%/lib/ext下面的类,必然也无法找到我们的B。于是会在AppClassLoader中捕获到异常,然后接着调用AppClassLoader的findClass进行加载,结果找到了。

      终于啊,经过这么复杂的递归调用和冒泡查找后找到了我们的类B 了。至于为什么要设计的这么复杂,直接加载不就完了吗,干嘛搞得这么难受。这主要是出于安全性的考虑。你想想,这个过程中总是由Bootstrap来加载核心类,假如你自己写了一个名字叫String的类,里面含有攻击性的代码,如果能加载成功,必然会导致其他依赖此类的类导致错误,整个JVM就会崩溃。然而这个类是无法加载到内存中的,因为类的加载总是由BootstrapLoader开始,当他发现已经加载了String,就不会再加载了,有效地保证了系统的安全性。类的加载过程基本就这样,下面贴出一段代码,自己实现的类加载器。

    import java.io.ByteArrayOutputStream;

    import java.io.FileInputStream;

    import java.io.FileNotFoundException;

    import java.io.IOException;



    public class MyClassLoader extends ClassLoader{

    private String path="F:\\JSP\\ClassLoaderDemo\\classes\\";

    private String fileType=".class";


    @Override

    public Class findClass(String name){

    byte bytes[]=this.loadClassData(name);


    return this.defineClass(name, bytes, 0, bytes.length);

    }

    //加载类数据,返回一个byte数组

    public byte[] loadClassData(String name){

    try {

    FileInputStream fin=new FileInputStream(path+name+fileType);

    ByteArrayOutputStream bout=new ByteArrayOutputStream();

    int ch=0;

    while((ch=fin.read())!=-1){

    bout.write(ch);

    }

    return bout.toByteArray();

    } catch (FileNotFoundException e) {

    e.printStackTrace();

    } catch (IOException e) {

    e.printStackTrace();

    }

    return null;

    }

    }

    //类加载器的测试类

    public class TestLoader {

    public static void main(String argsp[]){

    MyClassLoader loader=new MyClassLoader();

    try {

    //在指定的目录中加载HelloWorld.class文件

    Class myclass=loader.loadClass("HelloWorld");

    //加载完毕后进行实例化,这个过程包含了对类的解析

    myclass.newInstance();

    System.out.println(myclass.getName());

    } catch (ClassNotFoundException e) {

    e.printStackTrace();

    } catch (InstantiationException e) {

    e.printStackTrace();

    } catch (IllegalAccessException e) {

    e.printStackTrace();

    }

    }

    }


    这段代码可以直接拷贝运行.

    我们大篇幅的讲述了类的加载过程,这是jvm运行的第一步,建议各位读到这里的时候在脑海中回顾一下类的整个加载过程,以便于理解下面我们要说的类的链接。如果你觉得理解的没有问题,我们继续说一下类的连接阶段。

    当我们将类加载到内存中的时候,我们就需要对他进行验证。这里我们先介绍一下JVM是如何进行虚拟的。

    想必大家都知道我们的CPU是由控制器,运算器,存储器,输入输出设备组成的,这些是我们程序运行所必需的硬件环境。你可以这样认为,我们的JVM为我们的java程序虚拟出了完整的一套运行环境,他的控制器由我们的JVM直接担任,像垃圾处理了内存分配了等等,运算器当然就是我们的cpu了,存储器是jvm的运行数据区,输入输出设备就是硬件设备了。这里你可以发现,我们的java是不能直接与硬件进行交互的,底层功能的实现需要通过本地方法进行实现,这就是我们的java跨平台的原因。我们的JVM会根据硬件环境的不同(这里主要是指CPU的指令集的不同),将我们的class文件解释成cpu可以识别的指令码,这样,我们的CPU就能够运行我们的java程序了,这就是java的伟大之处。更确切的说使我们JVM的伟大之处了,呵呵。

    这里,你只需要大概的了解一下JVM的原理就OK了,之后我们会细细的讲解。

    我们现在再说说类的连接阶段。

    当我们把类加载到内存之后,我们如何保证他的正确性呢,或者说我们如何保证加载进来的二进制码是不是符合我们的Class类型的结构呢?关于结构,要细细说来需要很大的篇幅,这里你只需要这样理解他Class就像是类的模板一样,它包含类的所有信息,包括访问控制符号(public,private,友好型)、是类还是接口,直接父类是谁,实现的接口有什么,以及字段信息,方法信息 ,还有一个常量池。你看看,这不就是我们类所包含的所有信息吗。他们按照一定的结构组织在内存中,我们把这样的一块内存结构称为Class。就是我们常说的类类型。

    我们接着说,为了保证我们加载的二进制代码是Class结构,所以我们需要进行校验,很多地方称为验证,我感觉称之为校验更为合适。我们的校验程序校验完毕,发现它是我们需要的Class结构的时候,就会通知JVM为我们Class在方法区域分配空间。

    说道这里,我们又要说说我们JVM的运行时数据区了,也就是他的存储结构,下面,我会用图示的方法来解释它。

    我们的JVM将运行数据区分为如下几块

    堆:用来存储我们创建的对象的地方

    栈:JVM会为每个线程创建一个栈,用来存储局部变量和操作数,栈跟栈之间不能通信。存储单位是栈帧。我们每调用一个方法,就新建一个栈帧压入栈中。栈帧之间是屏蔽的,这就是为什么一个方法无法访问另一个方法中的变量。栈帧由局部变量区(用数组实现),操作数栈(栈结构),帧数据(主要用来支持对类常量池的解析,方法的正常返回,异常处理)

    方法区:用来保存Class类型数据

    JVM的内存主要结构就这么多了。

    好了,我们接着说,也许你现在对这张图还有很多疑问,稍后你就会明白了。我们的类现在已经通过验证了,校验器告诉我们它符合我们的Class结构,而且在方法区域为他分配了空间,我们非常高兴。下面就是关乎初始化问题了。有人会问,不对还有解析呢。呵呵,在写程序我们也知道了,这个阶段是可选的,也就是说你可以让你的类加载后马上初始化,也可以加载完毕不进行初始化。在这里我们要让我们的类初始化,下面即使解析阶段了。

    解析阶段的主要任务:将类变量的符号引用解析成直接地址引用。就拿我们的变量a来说,他的值是"hello",这个东西在Class中只是一个符号而已。然而,我们的Class需要将所有的常量都存放在常量池中,所以hello会被存储到常量池中,然后提供一个入口地址给a。a 就能直接引用它了。这里得好好地理解理解。

    我们的方法b引用的是一个方法,这个方法在Class中只是一个符号而已,这个方法的实际代码存放在一张表中,这张表我们成为方法表。我们的b就指向了方法表的一个引用。

    解析完毕之后,就要初始化了,初始化很简单,就是执行静态代码块中的内容了。整个加载到此已经完毕,想必大家已经很清楚了吧。

    然而,我们的JVM的任务才刚刚开始。

    下面我们说一下对象的创建吧,想必这个问题在很多人看来都是很不解的。那么我们马上开始吧。

    对象的实例是什么呢?在内存中的样子是什么呢。

    如果你知道了方法区中的东西,对于对象也就不难理解了。对象就像一种结构,其中存储了指向方法的引用,实例变量,一个指向类常量池的引用(这就是为什么实例可以访问类变量和类方法)。这些数据按照一定的结构(就像Class结构一样,只是简单很多)存储在我们的堆区,这就是我们耳熟能详的对象。当我们new的时候,JVM就会按照上面的过程,在堆区为我们构造一个这样的数据结构,然后将块数据的引用存储到栈里面。稍后我们会细细讲解栈的结构

    说到堆,我们不得不说JVM的内存管理机制,或者堆空间的垃圾处理机制。假如你自己写了一个JVM,你肯定会碰到这样一个问题,我们不断的在堆里面创建对象,再大的内存也有耗尽的时候,那我们如何进行垃圾处理呢。以前,在JDK1的时候采用的是对整个堆空间进行扫描,查找不再被使用的对象将其回收,可想而知这种策略是多么的低效。后来,我们聪明的java工程师给我们提供了这样的存储结构,他们将堆分为了两大部分,新生区和永久区。

  • 相关阅读:
    druid 配置监控界面和开启spring支持
    关于ComboGrid取值为非下拉框数据是,隐藏面板数据清空
    Error:Execution failed for task ‘:app:processDebugManifest’.
    The newly created daemon process has a different context than expected
    在CentOs6.5下安装Python2.7.6和Scrapy
    【转】Ehcache详细解读
    项目代码的缺陷
    hibernateTemplate
    java.text.SimpleDateFormate错误
    关于集合的空指针问题
  • 原文地址:https://www.cnblogs.com/michelleAnn2011/p/2340193.html
Copyright © 2020-2023  润新知