java编程

Jvm虚拟机学习一些基本概念

字号+ 作者:风潇潇 来源:原创 2017-05-18 11:12 我要评论( )

记录一下我看jvm一些概念理解

先说一个简单的程序。
一个java代码,java代码中有main方法。Main方法里面new一个对象。就如图MyTest.java,翻译的class文件为MyTest.class:

 
Jvm应该怎么运行这个代码呢?
    ClassPath是啥
首先jvm和java是两个系统,jvm运行java程序从本质上来说运行的不是java代码,而是经过翻译的jvm可识别的字节码。所以java代码必须经过翻译成class字节码。
这里这个步骤如果以后研究再补上。
现在到了class字节码了。Jvm怎么定位这个文件并且加载呢?这个和classpath有关。Java环境安装都要设置一个叫classpath的变量,里面大致是这样.;%JAVA_HOME%\lib ,那这个是什么意思呢?启动一个java,jvm需要定位并且找到这个字节码位于哪个文件夹,这个就是告诉jvm,你到那个文件夹下面去找class文件。
而启动一个jvm,首先jvm会启动最开始的类加载器,这个加载器是用c++编写的,叫bootclassloader,这个类加载器只加载java最核心的东西,比如rt.jar 。他是jre/lib 下面的东西以及它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。当bootclassloader加载这些类之后,位于java里面的这些class就可以使用了。
然后启动extclassloader,而extclassloader实际上是bootclassloader的子加载器。而他负载加载jre/lib/ext里面的class。里面一般是jar包的形式,这里lib包。这里面装的是java的扩展类
而加载我们自个程序的是appclassloader。Appclassloader是extclassloader的子加载器。Appclassloader去加载我们自定义的类。Classpath变量中最前面是个点(.),就是把当前路径也规划进去,如果在其他路径找不到,就从当前路径找。

 
而类加载器基于委托机制的。委托机制就是,当前classload会去判断是否已经加载,已经加载了就返回此class信息,如果没加载就委托给parent加载器,parent加载器加载不到,再自己加载。parent也会重复去做这个操作。委托机制说白了就是加载class信息优先父ClassLoader去加载,怕出现java的安全问题
    AppClassLoader 的loadClass方法:
    public Class loadClass(String paramString, boolean paramBoolean)
                throws ClassNotFoundException {
            int i = paramString.lastIndexOf(46);
            if (i != -1) {
                ……//这里有其他相关代码,不关注
            }
            return super.loadClass(paramString, paramBoolean);//加载Class信息
        }

//super.loadClass实现        
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);//查找当前是否已被加载
            if (c == null) {//如果没加载到
                try {
                    if (parent != null) {//判断有没有父加载器
                        c = parent.loadClass(name, false);//调用父加载器加载
                    } else {
                        c = findBootstrapClassOrNull(name);//如果没父加载器了,也就是到bootstrap加载器了。就用最核心的加载器加载
                    }
                } catch (ClassNotFoundException e) {
                    //加载不到,有可能会抛异常,不处理
                }
                if (c == null) {//如果parent加载失败了,
                    long t1 = System.nanoTime();
                    c = findClass(name);//就自己去加载
                   ……//这里还有一串代码,无关紧要,这里只关注类加载的委托机制
                }
            }
            if (resolve) {
                resolveClass(c);//处理Class信息
            }
            return c;
        }
    }
当appclassloader找到这个class文件之后,就会去读取这个文件。这就涉及到怎么解析class文件。
解析Class文件
类的加载过程为:1、通过一个类的全限定名来获取此类的二进制流 2、将这个字节流代表的静态存储结构解析出来转换为方法区的运行时数据结构 3、在内存中生成一个java.lang.Class对象,作为访问方法区的这个类的数据的访问入口。而这只是加载,其他过程还有验证、准备、解析、初始化等。如下图
 
验证:验证是确保Class文件字节流中的信息符合jvm要求,并且不会危害到jvm
准备:这里是指为类变量在方法区分配内存,并且赋初始值,注意不是实例变量。而且这里的赋初始值不是赋我们定义的值,是赋0值。比如public static int value=123 这里的value 赋值不是赋值123 而是赋值 为0;123 这种值会在编译之后放在cinit 方法里赋值
解析:解析是将符号引用直接替换成直接引用
初始化:这里是初始化用户自定义的值,执行cinit里面的指令
下面说一下加载过程的一部分中的解析class字节流数据
MyTest.class 这种class文件都是结构紧凑的二进制文件,而且是unicode编码。解析些什么呢?比如类名、方法名、方法参数等都统一存进常量池;将方法的的指令解析放到方法区。下面是java class的结构表,u1表示8位字节的无符号数,u2、u4依次类似
 
其中Class类型的常量池结构:
 
Utf8型的常量池结构:

常量池取值类型:
 
总的类型结构:
 


 
这里具体分析一下:
下图是MyTest.class的字节码文件。
 
怎么看这个字节码文件?这里的数据属于二进制数据。然后为什么这里是一些数字或者字母呢?这里查看工具每4位2进制转成了一位16进制,诸如1111(2)=F(16)。然后一个8位bit即为一个byte,意味着图中的两个数字即一个字节。注意,在class文件中,都是无符号的。
根据class字节码的格式,前四个字节是CAFEBABE 没错;
紧接着是u2类型的主版本号0000;然后u2类型次版本号0033,换成10进制即为51;
接下来是u2类型的常量池中常量的数量,偏移位置为0000008,值为19,转换成10进制为25,但常量池的索引号是从1开始的,0位置是空闲的,所以解析出来应该是24个索引位置;
接下来是第一个常量描述,偏移位置为0000000A,值为0A ,对应的10进制数为10,表示Methodref_info 类型常量,接下来的两个u2类型的都为索引号,值为16进制的9和14,对应的10进制为9和20。则第一个常量为:#1 =Methodref   #9.#20
再接着是没有空格的,直接就是第二个常量tag 值。07(16)=07(10) 表示Class_info的。Index 为 15,对应的10进制为21,解析为       #2 = Class    #21。
其他的就依次,按照这方法解析
当然也有人奇怪,jvm的方法指令存到哪儿去了,java程序主体里的代码经过javac编译之后存储在属性表的code属性里面。当然并非所有的class都会有code属性,比如抽象方法、接口类型就没有
这里是翻译之后的常量池:
 
其他类型的,就按照class结构一个个去看就好了
Jvm规范定义指定java的main函数就是程序的入口。Jvm会定位这个方法,执行这个方法里面的指令。
在初始化对象之前,jvm必须将这个文件解析成jvm可识别的数据结构。比如有些什么方法,有些什么变量,有些什么常量。这里还涉及到java的内存结构。Jvm的内存结构图如下。jvm会将class文件解析之后,放到方法区里面。在很多资料里还提到常量区,这里的常量区就是方法区里的一部分。

Jvm怎么定位main方法呢?jvm规范就是专业,jvm运行就会执行main方法里面的指令。那怎么调用其他方法呢?
请看下面的常量池
   #5 = Class              #34            //  Caculator
   #8 = Methodref          #5.#36         //  Caculator.addAB:(IIJLjava/lang/String;F)I
  #24 = Utf8               addAB
  #25 = Utf8               (IIJLjava/lang/String;F)I       
  #36 = NameAndType        #24:#25        //  addAB:(IIJLjava/lang/String;F)I
这是调用方法的执行指令:invokevirtual #8 以上常量池只是显示了一部分。常量池的索引号是从1开始到某个数字结束。 #8 是个方法型的引用,但在常量池中还是个常量,那jvm怎么知道具体的方法的指令在哪儿呢?这就涉及到jvm将常量池中的符号引用转换成直接引用。
符号引用和直接引用
那什么是符号引用呢?什么是直接引用呢?符号引用就是以一个符号来描述一个所引用的目标,只要能准确的定位到目标即可。不管其是不是加载到内存,不管其内存结构咋样。直接引用就是直接可以定位到目标,可以是一个指针、一个地址或者一个句柄等。而解析过程就是将上述符号引用替换成直接引用,然后用一个标志记录这个符号引用被解析过。这一过程在加载Class之后初始化之前就解析了。因为这个是可以唯一确定
还有一种需要方法表支持的叫做分派,过程有点类似,不过在解析之前会载入当前实际对象,根据当前实际类型去定位方法,这就是java的方法的重写(override)和重载(overload)的支持。
那jvm怎么new对象呢?线程之间怎么分配内存。至于对象在堆里面是怎样的数据结构,不同的虚拟机有自己不同的实现。但分配内存大底有两种方法:

创建对象
当jvm遇到一条new object的指令时,jvm会去常量池中去定位类的符号引用并且检测改类是否已经被加载,如果没有加载了就、解析、初始化过,如果有已经加载过了就在堆中分配一个确定大小的内存给此对象。所需要的内存空间在加载之后就可以完全确认。
其有两种方法管理jvm内存:
       1、指针“碰撞法”;指针碰撞法是在堆里将内存整理好的情况下才使用的策略,一边是空闲内存、一边是已使用的内存,中间用指针作为分界点的。如果要分配内存就是将指示指针往空闲区移动一点点就是了,但这种策略在并发情况下会遇到较多的问题,并且有几种解决办法。一种是使用线程同步,二种是每个线程都有自己的分配空间,这样就不会相互作用,空间满了再分配新的空间
        2、是“空闲列表”,在jvm的会在空闲列表找到一块可用内存并分配出来

这里还提一下垃圾收集器分类,不同的jvm收集算器,其分配内存的方式也不同,比如分代收集中serial就是基于指针碰撞法。而CMS是基于空闲列表法,下图是垃圾收集器种类
 
对象访问
    怎么对对象进行访问呢?主流方式有两种:
一种是句柄,如图
 
二种是直接访问
 
执行方法,怎么确切的去执行一个方法。可以参考我的博文:
http://www.getby.cn/javabiancheng/2016/0617/74.html
线程执行状态。
 




转载请注明出处。

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

相关文章
  • json解析与java对象序列化成json

    json解析与java对象序列化成json

    2017-09-07 11:47

  • jvm中class文件结构解析学习

    jvm中class文件结构解析学习

    2017-05-18 02:51

  • java动态绑定以及invokespecial指令

    java动态绑定以及invokespecial指令

    2017-06-19 14:57

  • java LinkedList数据结构

    java LinkedList数据结构

    2017-03-30 15:33

网友点评
评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)