程序员人生 网站导航

《深入理解Java虚拟机》笔记

栏目:php教程时间:2017-02-08 08:29:22

学习Java的同学注意了!!! 
学习进程中遇到甚么问题或想获得学习资源的话,欢迎加入Java学习交换群,群号码:183993990  我们1起学Java!


在C里面我们想履行1段自己编写的机器指令的方法大概以下:

typedef void(*FUNC)(int);
char* str = "your code";
FUNC f = (FUNC)str;
(*f)(0);

  也就是说,我们完全可以做1个工具,从1个文件中读入指令,然后将这些指令运行起来。上面代码中“编好的机器指令”固然指的是能在CPU上运行的,如果这里我还实现了1个翻译机器:从自己定义的格式指令翻译到CPU指令,那末就能够履行根据自定义格式的代码了。那末上面这段代码是否是相当于最简单的1个虚拟机了?下面来看JVM的整体结构:


ClassLoader的作用是装载能被JVM辨认的指令(固然不只是从磁盘文件或内存去装载),那末我们先了解1下该格式:

魔数和版本就不说了(满大街的文件格式都是这个东西),接着的便是常量池,其中不过是两种东西:

  1. 字面常量(比如Integer、Long、String等);
  2. 符号援用(方法是哪里的?甚么样的?);

而我们知道,在JVM里面Class都是根据全限定名去找的,那末方法的描写固然也应当如此,那末就得到这些常量之间的关系以下:

 

在接下来的“访问权限”中表明了该Class是public还是private等,而this&super&interface则表面了“本类”、“继承自哪一个类”、“实现了哪些接口”,实际上这里只是保存了代表这些信息的CONSTANT_Class_info的下标(u2)。

 

感觉这里的NameIndex和DescriptorIndex加起来和NameAndType有点像,那末为何不直接用1个NameAndType的索引值表示?MethodInfo和FieldInfo之间最大的不同点就是Attributes。比如FieldInfo的属性表中寄存的是变量的初始值,而MethodInfo的属性表中寄存的则是字节码。那末我们来顺次看这些Attributes,首先是Code:

 

有几个成心思的地方:

  1. 从Class文件中可以知道在履行的进程中栈的深度;
  2. 对非静态方法,编译器会将this通过参数传递给方法;
  3. 异常表中记录的范围是指令的行数(而不是源代码的);
  4. 这里的异常是指try-catch中的,而与Code同级的异常表中的则是指throws出去的;

Exceptions则非常简单:

LineNumberTable保存了字节码和源码之间的关系,结构以下:

 

LocalVariableTable描写了栈帧中局部变量表的变量和源代码中定义的变量之间的关系,结构以下:

 

SourceFile指明了生成该Class文件的Java源码文件名(比如在1个Java文件中申明了很多类的时候会生成很多Class文件),结构以下:

Deprecated和Synthetic属性只存在“有”和“没有”的区分:

  1. Deprecated:被程序作者定为不再推荐使用,通过@deprecated注释说明;
  2. Synthetic:表示字段或方法是由编译器自动生成的,比如<init>; 

这也就是为何Code属性后面会有Attribute的缘由?

类加载的时机就很简单了:在用到的时候就加载(空话!)。下来看1下类加载的进程:

 

履行上面这段进程的是:ClassLoader,这个东西还是非常重要的,在JVM中是通过ClassLoader和类本身共同去判断两个Class是不是相同。换句话说就是:不同的ClassLoader加载同1个Class文件,那末JVM认为他们生成的类是不同的。有些时候不会从Class文件中加载流(比如Java Applet是从网络中加载),那末这个ClassLoader和普通的实现逻辑固然是不1样的,通过不同的ClassLoader就能够解决这个问题。

但是允许使用不同的ClassLoader又引发了新的问题:如果我也声明了1个java.lang.Integer,但是里面的代码非常危险,怎样办?这里就引出了双亲委派模式:

除顶层的启动类加载器外,其余的类加载器都应当有父类加载器(通过组合实现),它在接到加载类的要求时优先委派给父类加载器去完成。

这样的话,在加载java.lang.Integer的时候会优先使用系统的类加载器,这样就不会加载用户自己写的。在Java程序员看到有3种系统提供的类加载器:

  1. Bootstrap ClassLoader:负责加载<JAVA_HOME>\lib目录中的类库,没法被Java程序直接援用;
  2. Extension ClassLoader:负责加载<JAVA_HOME>\lib\ext,开发者可以直接使用;
  3. Application ClassLoader:加载ClassPath上所指定的类库,如果没有自己定义过自己的类加载器则会使用它;

这样默许的类会是有Application ClassLoader去加载类,然后如果发现要使用新的类型的时候则会递归地使用Application ClassLoader去加载(在前面的加载进程中提到)。这样,只有在自己的程序中能使用自己编写的ClassLoader去加载类,并且这个被加载的类是不能被他人使用的。

双亲委派模式不是1个强迫性的束缚,而是Java设计者推荐给开发者的类加载实现方式。双亲委派模式出现过的3次“破坏”:

  1. 为了兼容JDK 1.0,建议使用者去覆盖findClass方法;
  2. 在基础类要访问用户类的代码会出现问题(比如JNDI):线程山下文类加载器;
  3. 用户的1些需求,比如HotSwap、OSGI等; 

加载完完成后,接下来就要看程序是怎样运行的。栈帧是用于支持虚拟机进行方法调用和履行,帧的意思就是1个单位,在调用其他方法的时候会向栈中压入栈帧,结构以下:

 

在Class文件编译完成以后,在运行的时候需要多少个局部变量就已肯定(在前面Class文件中也已看到过了),那末这里需要注意这个特性可能会引发GC(具体如何引发就不在这里细说了)。在栈中,总是底层的栈去调用高层的栈(并且1定的相邻的),那末他们在参数传递(返回结果)的时常常是通过将其压入操作数栈,有些虚拟机为了提高这部份的效力使得相邻栈帧“纠缠”在1起:

 

那末我们接下来要去看是方法是如何履行的,第1个问题就是履行哪一个方法?在“面向进程”的编程中似乎不存在在个问题,但是在Java OR C++中这都是比较蛋疼的1个问题。缘由就是平时不会这么用,但是你必须去弄明白= =。JVM肯定目标方法的时候有两种方法:

  1. 静态分派:根据参数类型和方法名称来决定调用哪一个方法。但是,其实不是说没有发现匹配的类型就报错,比如有:func(int a),而在调用func('a')的时候也会调用该方法(固然是在没有func(char a)的条件下),这样给人的关键就有点像1个处理的链条。不管多么复杂,这些都是在编译期间肯定的,由于这里是向上找的。
  2. 动态分派:最普遍的就是Interface a = new Implements(),a调用方法到底应当是哪一个类的在编译期间是没法肯定的。其实动态分派实现起来也很简单:在调用方法的时候先拿到对象的实际类型。

其实“静态”和“动态”给人的感觉还是比较模糊的,“静态分派”给人的感觉是根据参数的类型向上查找方法,“动态分派”给人的感觉则是根据实例的真实类型向上查找。虚拟机优化动态分派的效力1般是为类在方法区中建立1个虚方法表:

虚方法表中寄存各个方法实际入口地址,如果某个方法在子类中没有被重写,那末子类的虚方法表里面的地址入口和父类相同方法的地址入口是1致的,都指向父类的实现入口。如果子类重写了这个方法,子类方法表中的地址将会被替换为指向子类实现版本的入口地址。其实往简单里说,就是1个预处理。

具体单个方法的履行非常简单,写1个简单的程序然后使用javap -c,再结合每条指令的含义就可以大概知道程序时怎样履行和返回的了(大体上就是基于栈),这里就不深入和细说了。

1般情况下,从Java文件到运行起来,总的会经历两个阶段:Java到Class文件和履行Class文件。第1个阶段其实就是编译了,在这个进程中比较成心思的是“语法糖”(其他的比如词法分析和语法分析就不说了,此处省略1万字~!~)。所谓Java的语法糖是:for遍历的简写、自动装箱、泛型等(其实有无感觉String+String也是语法糖,在实际中会变成StringBuffer的append)。其中比较成心思的是泛型:

Java中的泛型和C++中的泛型原理是上不1样的:对C++来讲List<A>和List<B>就是两个东西,而在Java中List<A>和List<B>都是List<Object>,由于在Java中Object是所有对象的父对象,那末Object o可以指向所有的对象,那末就能够用List<Object>来保存所有的对象集了(感觉实现的有点废)。

这里触及到1个问题就是对象删除,比以下面代码:

static void func(List<Integer> a){
        return;
}

在使用javap查看生成的Class的时候会发现:

static void func(java.util.List);
  Signature: (Ljava/util/List;)V
  Code:
   0:   return

其中根本没有任何Integer的痕迹,但是如果加上返回值,也就是:

static Integer func(List<Integer> a){
        return null;
}

此时再查看的时候就会变成:
static java.lang.Integer func(java.util.List);
  Signature: (Ljava/util/List;)Ljava/lang/Integer;
  Code:
   0:   aconst_null
   1:   areturn
通过泛型实现的原理可以理解很多在实际中会遇到的问题,比如使用List的时候稀里糊涂的类型强迫转换毛病。

接下来开始讨论第2个部份,也就是Class文件的实际的履行。在C++中常会提到的两个概念是:Debug和Release,而在Java中常提到的两个概念是Server和Client(虽然他们划分的根据完全不1样),Client和Server两种模式对应两种编译器:

  1. Client对应C1编译:将字节码编译本钱地代码并进行耗时短且可靠的优化,在必要的时候加入性能监控。
  2. Server对应C2编译:将字节码编译本钱地代码并进行耗时比较长的优化,还可能会根据性能监控的结果进行1些不可靠激进的优化。

在监测器发现有热门代码(被调用了很屡次的方法或是履行很屡次的循环体)的情况下,将会想即时编译器提交1个该该方法的代码编译要求。当这个方法再次被调用时,会先检查改方法是不是存在被JIT编译过的版本,如果存在则优先使用编译后的本地代码。在默许的情况下,编译本地代码的进程和旧的代码(也就是解释履行字节码)是并行的,可使用-XX:-BackgroundCompilation来制止后台编译,也就是说履行线程会登岛编译完成后再去履行生成的本地代码。

在具体编译优化的时候有1个比较好玩的东西,逃逸分析(所谓逃逸是指能被从方法外援用),对不会逃逸的对象可以进行优化:

  1. 在栈上分配对象,可以减少GC的压力;
  2. 不需要对为逃逸的对象进行线程同步;
  3. 如果1个对象没法逃逸,可以在方法里面不申明这个对象,而是放1些“零件”;

关于Java和C++效力的问题,感觉讨论起来就没有甚么意义了:语言到最后肯定是要生成机器指令的,在语言的机制上面各有千秋,致使不同的语言之间生成机器指令的进程可能不同,但是这个生成的进程跟我们这些码农没有半毛钱关系(更准确的说我们生成的进程我们毛都不知道),所以在弄清楚之前就不要争到底哪一个效力高(乃至是哪一个更好)。 

程序的并发主要是斟酌不同的线程操作同1块内存时候可能产生的1些问题(至于文件锁之类的东西,咳咳),首先就先了解线程和内存的关系:

 

这里的主内存就像是内存条,工作内存就像是寄存器+Cache。Java内存模型定义了8中操作,他们的履行以下:

 

Java虚拟机中最轻量级的同步机制:volatile,它的性质以下:

  1. 变量产生修改的时候会立刻被其他线程看到;
  2. 制止指令重排序优化;

从Java内存模型操作的角度来看volatile的实现还是挺简单的:在use之前必须load,在assign以后必须store,这样就保证了每次用都是从主内存中读取,每次赋值以后都会同步到主内存(貌似说的是空话)。线程的同步主要是从3个方面斟酌:

  1. 原子性:Long和Double需要特殊斟酌;
  2. 可见性;除volatile以外还有final(synchronized就不说了吧);
  3. 有序性:指令重排,固然可以制止指令重排;

如果任什么时候候都斟酌同步那代码写起来就累死了。下面是Java内存模型的天然先行产生关系:

  1. 控制流被履行的顺序和代码的顺序保持1致;
  2. unlock先行产生于后面对同1个锁的lock操作;
  3. 对volatile变量的写操作先行于后面对这个变量的读操作;
  4. Thread的start方法先行于线程的任何1个动作;
  5. 线程的所有动作都先行于线程的终止检测;
  6. 对线程interrupt方法的调用先行于被中断线程的代码检测到的中断事件的产生;
  7. 对象的初始化完成先行于finalize方法调用;
  8. 传递性;

其实上面的这8条规则还是很成心思的,如果其中的某1条不成立会产生甚么?说到底Java线程还是用户级的线程,那末它究竟是个甚么东西(在学C的时候也纠结过这个问题- -)。实现线程主要有几种方式:

  1. 使用1个内核线程(轻量级进程)来代理;
  2. 完全在用户态实现,内核都感觉不到;
  3. 用户和内核混合实现,各自做自己善于的事情;

这里就不深入的去看了(虽然这里的介绍根没说1样),想一想看都知道不同虚拟机在不同的操作系统上面的实现方式极可能是不1样,如果想深入看还是pthread比较成心思1点。关于线程的其他要注意的地方(比如状态转移甚么的)就不在这里讨论了。

线程安全:当多个线程访问1个对象时,如果不用斟酌这些线程在运行时环境下的调度和交替履行,也不需要进行额外的同步,或在调用方进行任何其他的调和操作,调用这个对象的行动都可以取得正确的结果,那这个对象是线程安全的。

Java中线程同享的变量可以分为以下5种:

  1. 不可变:这个就不需要解释了(其实不1定非得用final修饰);
  2. 绝对线程安全:也就是满足上面的线程安全描写的;
  3. 相对线程安全:简单的说应当是对单个行动的调用不会出错;
  4. 线程兼容:对象其实不是线程安全,但可以通过调用方的同步来弥补;
  5. 线程对峙:不管调用方怎样处理都不能在多线程环境下使用;

锁的话有以下几种实现方式:

  1. 互斥同步,其实不是说等待的线程会1直等下去;
  2. 非阻塞同步,乐观(冲突并没有我们想象的那末多);

如果线程之间的切换非常频繁的话自旋锁是1个不错的选择,这样就不需要线程切换时候的系统调用的开消了。如果1个任务能够很快的完成的话,将全部进程都锁住也许是个不错的选择(而不是给每一个子进程上锁)。其他的锁优化包括“轻量级锁”和“偏心锁”。

学习Java的同学注意了!!! 
学习进程中遇到甚么问题或想获得学习资源的话,欢迎加入Java学习交换群,群号码:183993990  我们1起学Java!

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐