浅析JVM

来源:互联网 发布:串口数据采集软件 编辑:程序博客网 时间:2024/06/01 07:11

上图:

这里写图片描述

要了解JVM,首先得了解他的内存结构:

栈:

  • 存放基本类型变量,局部变量,对象的引用;

  • 系统自动分配与回收内存,效率较高,快速,存取速度比堆要快;

  • 是一块连续的内存的区域,有大小限制,如果超过了就会栈溢出;

  • Java会自动释放掉为该变量所分配的内存空间;

  • 存放线程调用方法时存储局部变量表,操作,方法出口等与方法执行相关的信息;

  • 栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义: int a = 3;int b = 3;编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况

堆:

  • 存放new创建的对象和数组; 在运行时动态分配内存(比如 new()),较慢,但灵活;

  • 是不连续的内存区域,在发出申请的时候,系统首先会遍历一个存有空闲地址节点的链表,找到第一个满足申请大小的节点,将他从链表删除,并分配给申请者,如果有多,则将多出来的加入链表;

  • 由Java虚拟机的自动垃圾回收器来管理

静态区 又叫方法区:

  • 存放静态变量,常量,全局变量。

  • 因为方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待。

  • 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。同样方法区也不必是连续的。方法区可以在堆(甚至是虚拟机自己的堆)中分配。jvm可以允许用户和程序指定方法区的初始大小,最小和最大尺寸。

    在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的

类型信息
对每个加载的类型,jvm必须在方法区中存储以下类型信息:

  • 一 这个类型的完整有效名

  • 二 这个类型直接父类的完整有效名(除非这个类型是interface或是 java.lang.Object,两种情况下都没有父类)

  • 三 这个类型的修饰符(public,abstract, final的某个子集)

  • 四 这个类型直接接口的一个有序列表

程序计数器

  • 是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

本地方法栈

  • 则是为执行Native方法服务,但这个在不同JVM内有不同的内部实现,比如在HotSpot
  • JVM中Java虚拟机栈和本地方法栈被实现为同一个栈区

JVM的生命周期
JVM实例对应了一个独立运行的java程序它是进程级别
a) 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点

b) 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程

c) 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出

运行步骤(原理)
作系统装入JVM是通过jdk中Java.exe来完成,通过下面几步来完成JVM环境.

(1)首先会通过java.exe找到jre文件。

先找jdk目录下是否有\JDK\bin\java.dll,如果有就将\JDK作为jre文件的路径;如果没有就找\JDK\jre\bin\java.dll,如果有就将\JDK\jre作为jre文件的路径;如果不存在调用GetPublicJREHome查HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\“当前JRE版本号”\JavaHome的路径为jre路径。

(2)通过刚刚找到的jre,接着找位于jre里面(jre\bin\server)的jvm.dll,
Jvm.dll才是真正的虚拟机,找到他后系统就装载这个动态链接库,激活虚拟机。

(3)激活后就会做一些初始化工作,比如读取参数,获得本地的调用接口;

(4) 初始化成功之后就会产生一个类加载器(bootstrap classloader 根加载器),根加载器完成初始化工作后就会加载扩展类加载器(extension classloader),并将其父加载器设置为根加载器;然后再加载用户自定义类加载器(app classloader)并将其父加载器设置为扩展类加载器。这两个加载器都是以静态类的形式存在。而根加载器是c++做的,所以在调用的时候会是null。

类加载机制
将字节码文件从磁盘读入内存的过程就是加载。加载过程分为三步:装载、链接、初始化。

装载
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图(引用某位大神的图)来描述:
引用某位大神的图

1)Bootstrap ClassLoader

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查(双亲委派模型),只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

装载过程从源码清析可见:

protected synchronized Class<?> loadClass(String name, boolean resolve)    throws ClassNotFoundException{    // 先检查是否已被当前ClassLoader装载。    Class c = findLoadedClass(name);    if (c == null) {        try {        if (parent != null) {            // 如果没被当前装载,则递归的到父中装载 自底向上。            c = parent.loadClass(name, false);        } else {           // 装载器树已到顶,还没找到的话就到Bootstrap装载器中找。注意:虽然Bootstrap是所有加载器的根,但它是C++实现的,不可能放到子的"parent"中,因此,第二层装载器是所有的根了。            c = findBootstrapClass0(name);        }        } catch (ClassNotFoundException e) {            // 如果祖先都无法装载,则用当前的装载。子类可在findClass方法中调用defineClass,把从自定义位置获得的字节码转换成Class。            c = findClass(name);        }    }    if (resolve) {        // Links the specified class.链接        resolveClass(c);      }    return c;}

通过findLoadedClass判断是否已经加载了,如果加载了,则返回这个类,否则就判断有木有父加载器,如果有就交给父加载器加载。如果祖先都不能加载,就交给当前的加载器加载。
findClass()方法就是实现类的加载规则,找到类的字节码,然后调用defineClass(byte[],int,int)方法生成类的class对象。
defineClass方法就是用来将byte字节流解析成虚拟机能够识别的class对象。

双亲委派模式
很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载(分工与责任明确,解决部分安全问题,如自定义的加载器不能加载根加载器加载的类,很好的避免了恶意写入基础类。),但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。线程上下文类加载器也没有遵循双亲委派模型。

JVM的两种加载方式:
1:隐示加载,就是不通过在代码里面调用classLoader来加载需要的类,而是通过JVM来自动加载需要的类。比如当一个类中继承了或者引用了某一个类,而这个类并不在内存中,那么就会自动将这些类加载到内存中。

2:显示加载,就是通过在代码里面调用classLoader来加载需要的类,调用this,getClass.getClassLoader().loadClass()或者ClassforName()来加载。

链接
链接就是把load进来的class合并到JVM的运行时状态中。

链接 是三个阶段中最复杂的一个。可以把它分成三个主要阶段:

校验
保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
1)文件格式验证,如版本号是否对,是否以魔数OxCAFEBABE开头等,经过这个阶段基于字节流进行的验证之后,字节流进入内存的方法去中进行存储,后面的三个验证阶段都是基于方法区的存储结构进行的。

2)元数据验证,这一阶段是语义分析阶段,如是否有父类(除Object外都应该有父类),是否继承了不允许被继承的类。

3)字节码验证,这一阶段是对方法体进行验证,主要是进行数据流和控制流的分析。如保证跳转指令不会跳转到方法体以外的字节码指令上。

4)符号验证,将符号引用转化为直接引用
如果所运行的代码已经被验证过,在实施阶段可以使用-Xvefiry:none参数来关闭大多数的类验证措施,以缩短虚拟机类加载的时间。

准备
为类变量分配内存并设置类变量的初始值(即默认值 0 null false),但不包括实例变量,常量的话会直接赋值他原来的值,不会是赋值默认值。
解析
将常量池类的符号引用替换为直接引用
1,符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用目标并不一定已经加载到内存中

2,直接引用:直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,直接引用与虚拟机实现的内存布局相关,如果有了直接引用,引用目标必定已经加载到内存中

解析可能会在初始化之后,这是为了java的动态绑定。

初始化
执行所有的类变量赋值和静态语句块的执行。

Class.forName()与ClassLoader.loadClass()
这两方法都可以通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象,区别如下:
1. 初始化
Class.forName()会对类初始化,而loadClass()只会装载或链接。可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的。你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath下), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化
2. 类加载器可能不同
Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器。当然,它也有个重载的方法,可以指定加载器。 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器(调用代用代码类加载器通getClassLoader0()获得)

常见错误

ClassNotFoundException:这个异常通常在显示加载类的时候,当JVM要加载字节码到内存时,找不到这个类的字节码的文件。解决方法就是检查当前的classpath目录下有木有指定的文件存在

NoClassDefFoundError:当使用new、属性引用某个类,实现某个接口或者继承某个类的时候,触发JVM隐士加载类,但这些类的字节码文件不存在。解决方法就是检查当前的classpath目录下有木有指定的文件存在

UnsatisFiedLinkError:通常是在JVM启动的时候,不小心删了JVM中的某个lib。一般在解析native标志的方法是虚拟机找不到对应的本机库文件。

ClassCastException:类强制转换错误。虚拟机在做类型转化的时候会按照以的规则转换:
1:对于普通对象,对象必须是目标类的子类或者是实例,或者是接口的实现类。
2:对于数组类型,目标类必须是数组类或者是java.lang.object java.lang.Clonable java.io.Serializable
如果不满足上面的就会报错。
解决方法:
1:在使用集合的时候使用泛型
2:在强制转换之前应先通过instanceof检查可不可以强制转换。

1 0
原创粉丝点击