理解JVM

1、Java代码执行机制

(1)源码编译机制

        要运行一个java文件,需要将其编译成class文件。编译需要经过词法分析(如判断关键字是否有误)、语法分析(生成抽象语法树)、注解处理、语义分析(将语法糖转化成普通语法,消除泛型,检查异常等)等步骤,最后生成class文件。

        class文件是一个自描述文件,包含以下内容:

        结构信息:class文件格式版本号

        元数据:常量池、方法声明信息、接口/父类信息

        方法信息:字节码、局部变量区和求值栈大小、异常处理表、求值栈的类型记录、调试用符号信息

        我用一段简单的代码来展示一下这些信息:

public class Test{
	public void say(){
		System.out.println("hello world!");
	}

}
        使用javac命令编译后,在javap -c -s -l -verbose class文件名 查看内容如下
Classfile /E:/Test.class
  Last modified 2018-1-31; size 388 bytes
  MD5 checksum 96ee3637a0a0050574b00580aded1d38
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #17            // hello world!
   #4 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #20            // Test
   #6 = Class              #21            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               say
  #12 = Utf8               SourceFile
  #13 = Utf8               Test.java
  #14 = NameAndType        #7:#8          // "<init>":()V
  #15 = Class              #22            // java/lang/System
  #16 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
  #17 = Utf8               hello world!
  #18 = Class              #25            // java/io/PrintStream
  #19 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
  #20 = Utf8               Test
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/System
  #23 = Utf8               out
  #24 = Utf8               Ljava/io/PrintStream;
  #25 = Utf8               java/io/PrintStream
  #26 = Utf8               println
  #27 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Test.java"

(2)class加载机制

        JVM将类加载过程分为装载、链接、初始化三个阶段。

        ①装载(load)

       装载阶段负责将二进制字节码加载到内存中,JVM通过类的全限定名和类加载器(classloader)完成加载。所以一个加载了的类由类限定名+类加载器唯一标识。

        ②链接(link)

        链接阶段负责检验字节码的格式(失败会抛出verifyError)、初始化类中的静态变量(不一定)及解析类中调用的接口和类(失败会抛出NoClassDefFoundError)、验证属性和方法(检查是否存在,是否具备权限(public、private),失败会抛出NoSuchMethodError、NoSuchFieldError)。

        ③初始化(Initiative)

        初始化并非一定是在加载过程发生,主要由以下五种情况触发:

         a、new 一个对象

         b、调用类的静态方法和静态变量

         c、使用反射

         d、子类被初始化

         e、JVM启动过程指定初始化类

         类的初始化顺序为:父类静态成员——子类静态成员——父类静态代码块——子类静态代码块——父类非静态域——父类构造方法——子类非静态域——子类构造方法。

         类的加载通过classLoader及其子类来完成。classLoader的结构如下:

         

       Bootstrap ClassLoader

         jdk使用c++实现了Bootstrap ClassLoader类,该类并非classloader类的子类,在代码中无法得到该对象。

        jdk使用该加载器加载$JAVA_HOME$/jre/lib/rt.jar里所有class文件,jar中包含了java规范定义的所有接口及实现。

        Extension ClassLoader

        JVM用此加载器加载扩展功能的一些jar包,在Sun JDK中类名为ExtClassLoader。

         System ClassLoader

        JVM用此加载器加载启动参数指定的classpath中的jar包和目录,在Sun JDK中对应的类名为AppClassLoader。

        User-Defined ClassLoader

        User-Defined ClassLoader是用户自己实现的加载器。自定义的classloader可以加载非classpath(网络获本地)的jar及目录,还可以对class进行解密。

        jvm在加载类时,首先会从父加载器中寻找,如果父加载器找不到,再向上一级寻找,如果最后还是找不到的话,才会从classloader中直接加载。如果直接从加载器中加载也是可以,因为jvm使用类全限定名+classloader标志一个加载类,所以不会发生冲突,但是有可能会重复加载从而浪费内存。

        ClassLoader抽象类提供了几个关键的方法:

        loadClass:

        该方法按照既定顺序加载类,如果想改变类的加载顺序,可以覆盖该方法;如果加载顺序相同,则覆盖findClass即可。

        findLoadedClass:

        该方法负责从当前classloader实例缓存中中寻找已加载类。

        findClass:

        覆盖此方法以自定义方式加载类。

        findSystemClass:

        该方法从System classLoader中寻找类。

        defineClass:

        此方法负责将二进制的字节码转换为class对象,该方法对于自定义加载器非常重要。如果字节码格式有误,则抛出ClassFormatError;如果生成的类名与字节码中的信息不符合,则抛出NoClassDefFoundError;如果加载的class是受保护的、不受签名的、或者类名以java开头的,则抛出SecurityException;如果该类已加载过,则抛出LinkageError。

        resolveClass:

        此方法负责class对象的链接,如果已经链接,则直接返回。

        类加载常见异常:

        ClassNotFoundException:

         找不到类。

        NoCLassDefFoundError:

        加载的类中引用的类不存在。

        LinkageError:

        上面已经提过。

        ClassCastException:

        如果两个A对象由不同的classloader加载,如果将其中一个A对象引用到另一个A对象时,则会抛出该异常。

(3)执行机制

        jvm通过解析字节码执行方法,这部分内容没有深入研究,就不多说了。需要注意的一点是jvm对于执行频率高的代码进行静态编译(执行更快,但编译时间长,而且占用内存),对于执行较少的代码使用解释的方式(节省内存,启动快,但执行速度慢)。

2、jvm内存管理机制

(1)内存分布

        看一下jvm的内存分布图:

        

        (注:方框大小于内存空间大小没有联系)

        对于栈区,我想认真学过汇编的读者应该明白它的作用,这里就不做详细描述,大家要知道的是在栈上分配变量空间是比堆要快很多的,因为不需要申请分配,只需要移动栈指针即可。这里重点讲一下方法区和堆区。

        方法区又称为持久代,保存类信息、数据域信息、方法域信息以及常量池,经过静态编译后的代码也保存在这个区域。如果该区域空间使用完了,则会抛出OutOfMemoryError。可通过 -XX:permSize和-XX:maxPermSize设置空间最小值和最大值。

        jvm(实际上有些虚拟机没有这样做)将堆区分为新生代和旧生代,其中新生代保存新建的对象,旧生代保存经过多次GC还存活的对象以及大对象。其大小可通过-Xms(最小heap内存)和-Xmx(最大heap内存)。如果堆内存不够会抛出OutOfMemoryError错误。

(2)内存分配

        由于堆内存是所有线程共享的,所以jvm在堆上分配内存时需要加锁,开销较大。为了提高效率,jvm新生代区为每个线程分配独立的TLAB空间,在该区域分配内存时不需要加锁。当TLAB空间不足时,再在公共堆区创建对象。

(3)内存回收

        jvm使用gc回收垃圾对象,回收主要包括两部分的内容,一是判断对象是否存活的算法,二是回收空间的算法。

        ①分析算法

        a、引用计数法

       引用计数法十分简单,它为每一个对象建立一个计数器,当对象被引用时,计数器加1;当引用取消时(赋值为null),计数器减1.当计数器为0时,jvm判定该对象为垃圾对象,进行回收。

        b、可达性分析法

        在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。


        在Java语言里,可作为GC Roots对象的包括如下几种:
        虚拟机栈(栈桢中的本地变量表)中的引用的对象
        方法区中的类静态属性引用的对象
        方法区中的常量引用的对象

        本地方法栈中JNI的引用的对象 

        ②垃圾收集算法

        a、标记-清除法
        实现:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:标记和清除两个过程效率都不够高;会产生内存碎片。

        b、复制算法
        实现:划分内存区域,一部分为使用区域,一部分为空闲区域,gc时将使用区域的存活对象复制到空闲区域中,然后清除原使用区域的内存,然后在逻辑上将原空闲区域变为使用区域,将原使用区域变为空闲区域。缺点:当存活对象较多时,效率变低。
        c、标记-整理法
         实现:首先标记出所有需要回收的对象,在标记完成后将存活对象向一端移动,然后清理掉端边界以外的内存。适用于老年代。

3、JVM调优

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值