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、标记-整理法
实现:首先标记出所有需要回收的对象,在标记完成后将存活对象向一端移动,然后清理掉端边界以外的内存。适用于老年代。