《深入理解Java虚拟机》--Understanding the Jvm(上)

 2019-12-22 11:22  阅读(886)
文章分类:JVM

前言:跟”Thinking in Java”不同的是,《深入理解Java虚拟机》是一本修炼内功心法的书。因为虚拟机对开发者来说,几乎是屏蔽的,可能了解虚拟机内部运转对敲代码不会有直接的效果,但是对读程序,理解程序如何执行的认识会更深一步。”Thinking in Java”从代码层面上分析代码的设计和高效用法,既有现学现用的功效,又有流连忘返的回味,是一个“怎么写(How)的问题”;而《深入理解Java虚拟机》从虚拟机的高度上去分析执行Java程序的原理,以及在这些原理下Java程序应该遵守怎样的规则,是一个“为什么(Why)的问题”。
大概用了一个月读了一遍周志明的《深入理解Java虚拟机》第2版,自己归纳一些点整理如下,当做笔记,方便以后复习。

1.JDK和JRE的概念
开发java时总说JDK、JRE的,但是它们到底指什么东西呢?
Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java Development Kit);
Java API类库中的Java SE API子集与Java虚拟机这两部分统称为JRE。

2.为什么”Thinking in Java” 的封面是许多种类的昆虫?
自从JDK1.3开始,Sun大约每隔2年发布一个JDK的主版本,以动物命名,期间发布的各个修正版本则以昆虫做为工程名称。

3.什么是Java虚拟机?
Java虚拟机不是单单指一个虚拟机,它是一个体系,一个家族,由历史上许许多多的java虚拟机组成,现在开发流行用的Java虚拟机是Sun公司的HotSpot虚拟机。 (SEA公司的JRockit和IBMJ9也很出色),而这些丰富多彩的Java虚拟机广泛用在除计算机之外的其它设备,机器人、机顶盒、终端移动等等。

4.Java虚拟机的意义
虚拟机是在计算机操作系统之上的更高一层,Java在任意一台虚拟机上编译的程序可以在任何一台虚拟机上运行,不管底层是什么操作系统,是什么硬件都可以做到统一,实现”一次编译,到处运行“(最直观理解就是跨平台)。

5.Java虚拟机所管理的内存的划分

2019120001671\_1.png

每个区域的功能如下:
程序记数器:是一块很小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
虚拟机栈:描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈:与虚拟机栈非常类似,区别是它们的服务对象不同:虚拟机栈服务于Java方法,而本地方法栈服务于虚拟机使用到的native方法。
堆:Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,堆唯一的目的是存放对象实例,几乎所有的对象实例在这里分配内存。
方法区:也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

6.Java是面向对象的语言,那么对象是如何被创建的?

2019120001671\_2.png

7.Java虚拟机(以下的Java虚拟机都指Hotspot虚拟机)在内存中是如何存储和访问对象的?
对象在内存中分为3部分:对象头、实例数据和对齐填充。
对象头包括对象自身的运行时数据和类型指针(确定对象是哪个类的实例)。
实例数据部分是对象真正存储的有效部分。
对齐填充没什么实际意义,对象必须是8字节的整数倍,当对象某些部分没有达到时,就需要通过对齐填充来补齐。
简单理解为:堆在内存中是由上往下的,栈是由下往上的,堆内存保存对象实例,栈(虚拟机栈)保存变量(数据类型存放在方法区,这里就不画出来了,自己脑补):

2019120001671\_3.png

既然知道对象在内存中的存储方式,那么Jvm怎样去访问对象呢?
有两种方式:句柄和直接指针
优缺点:句柄的reference存储的是稳定的句柄地址,对象被移动时(例如垃圾回收),只需要改变句柄的实例数据指针,而reference不需要改变。(reference的本质是一个地址)
直接指针方式只需要指定对象类型数据,不需要像句柄还要指定对象实例数据,所以速度块,但是增加堆的内存负担(再次证明,在计算机世界里,时间和空间永远是一对不可调和的矛盾)。HotSpot用的是直接指针方式。
原理图如下:

2019120001671\_4.png

8.String.intern()和运行时常量池的陷阱
代码如下:

public class RuntimeConstantPoolOOM{
        public static void main(String[] args){
            String str1=new StringBuilder("computer").append("software").toString();
            System.out.println(str1.intern()==str1);
            String str2=new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern()==str2);
        }
    }/*Output true false *///~

上面的代码在JDK1.6是中运行会得到两个false,在JDK1.7中运行会得到一个true和一个false。在1.6中,intern()方法会把首次遇到的字符串实例复制到常量池中,返回的也是常量池中字符串实例的引用,而StringBuilder创建的字符串实例在堆上,显然不是同一个引用。而1.7中不会复制字符串实例,只是在常量池中记录首次出现引用,所以StringBuilder创建的引用记录在常量池中再返回来是同一个,但是java,这是个关键字,在API中不知有多少个,不满足首次出现的原则,这是因为上面代码中的StringBuilder创建的java跟第一次StringBuilder在堆中创建的不是同一个引用。

9.揭开GC(Garbage Collection)神秘的面纱
提起Java,相信很多人都会想起垃圾回收机制,这里有个历史的小问题,很多人都以为java是第一个使用垃圾回收机制的语言,其实不是,古老而强悍的Lisp语言才是第一个引入垃圾回收的语言。(Lisp与C并称编程语言的两大派系,以Lisp为始祖的语言有Perl、python、ruby…以C为始祖的语言有C++、Java、C#…)
了解GC之前,我们先分清楚如何去了解它,根据马克思主义哲学,把未知问题分开三部分:why?when?how?
首先why?:这个问题比较明显,因为C、C++的手动回收令开发人员非常头疼,所以Java引进GC
其次when?:即对象什么时候被回收?
对象“已死”时就可以被回收!
那么怎么判断对象“已死”呢?
利用可达性算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,就会被判定为可回收的对象。(可达性算法的回收是指堆的对象的回收,因为绝大部分的对象都在堆中,至于方法区的回收,会有些不同)
下面看看虚拟机利用可达性算法判定对象是否回收的原理图:

2019120001671\_5.png

上面说的是堆中对象的回收(占据大部分的内存垃圾),那么方法区的废弃常量和无用的类如何回收?
这部分占的内存较小,判定回收的条件也苛刻了许多,主要有三个:
(1) 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
(2) 加载该类的ClassLoader已经被回收;
(3) 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

最后how?就是怎么去回收这些内存?
3个算法:
(1) 标记-清除算法
这是最基础的算法,首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。但是它暴露出两个问题:一个是效率问题,标记和清除的效率都不高;另一个问题是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致以后程序在运行过程中需要分配较大的对象时,无法找到足够的连续内存;
(2) 复制算法
将可用内存划分为大小相等的两块,每次只使用其中一块,当这块内存用完了,将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,这样就避免了碎片的问题,但是在回收时,需要一个内存块在流动,供给存活的对象放置;
(3) 标记-整理算法
标记部分跟标记-清理算法的标记一致,整理部分是把所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存;
那么,Java虚拟机采用何种方式回收垃圾呢?
Java虚拟机采用综合的“分代收集”算法,一般是把堆分为新生代和老生代,新生代中,每次回收时都有大量的对象死去,之有少量存活,所以新生代采用复制算法;老生代的对象存活率高,空间也比较小,就采用标记-清理/标记-整理算法进行回收。

10.HotSpot虚拟机是如何执行垃圾回收机制的?
首先脑补一下HotSpot的工作过程:一边在执行Java程序,遇到安全点时,转去执行垃圾回收,这根操作系统的中断处理类似:
2019120001671\_6.png

那么虚拟机是怎样去做到垃圾回收的呢?
靠的是垃圾收集器,垃圾收集器不止一种,就像我们做家务打扫一样,可以用扫帚,可以用吸尘器,可以用鸡毛毯…HotSpot虚拟机包含的所有收集器如图所示:

2019120001671\_7.png

简单说说各个垃圾收集器的特点:

Serial:它的历史最悠久,是一个单线程的收集器,在进行垃圾收集时,必须暂停其它的工作线程,直到收集结束。典型的“Stop the world”。
优点:简单高效
缺点:用户体验差,每次收集都要暂停工作线程,试想如果每一小时有5分钟会暂停响应,这是很难令人接受的。

ParNew:其实就是Serial的多线程版本。
优点:适合多CPU环境
缺点:不适合单CPU环境

Parallel Scavenge:它是一个极端,其它的收集器追求在不同的环境下缩短收集垃圾时造成的用户线程停顿时间,而它的目标是达到一个可控制的吞吐量,Parallel Scavenge也被称为“吞吐量优先”收集器。(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
优缺点:可以控制吞吐量动态调整出最适合的停顿时间和最大的吞吐量(也叫自适应调节策略),停顿时间短可以提高用户体验,高吞吐量可以高效率地利用CPU时间,是一种折中策略,具体看怎么去用,很难区分好坏。

Serial Old:是Serial的老年代(Tenured generation)版本,使用“标记-整理”算法回收垃圾,它平淡无奇,但是可以与许多其他种类的收集器一起使用,与其他收集器互补。
优缺点与Serial类似

Parallel Old:是Parallel Scavenge的老年代版本,使用多线程和“标记-整理”算法。与Parallel Scavenge组合,可以在新生代、老年代真正实现完整的“吞吐量优先”,该组合适合注重吞吐量以及CPU资源敏感的场合。
优缺点也很难定义,具体看怎么使用。

CMS:也是另一个极端,基于“标记-清除”算法实现的,CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
优点:响应速度极快(Java EE的最佳选择)
缺点:对CPU资源非常敏感,导致应用程序变慢;
CMS收集器无法处理浮动垃圾;
基于“标记-清除”算法,容易产生空间碎片

GI:GI收集器是当今收集器技术发展的最前沿成果之一,它将Java堆划分为多个相等大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。
优点:并行与并发,在回收垃圾时,Java程序还可以继续执行
分代收集,能够采取不同的回收方式去回收不同堆的对象
空间整合,从整体看,GI基于“标记-整理”算法,从局部上看,Region基于“复制”算法实现 的,可以有效的利用内存空间
可预测的停顿,可以人为设定停顿时间的长度,灵活性更高
缺点:论追求极致的降低停顿时间,比不上CMS,论追求极致的吞吐量,也比不上Parallel Scavenge

11.对象的内存分配与回收
往大方向讲:就是在堆上分配(也可能是经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden(新生代分Eden区和Survivor区,两者比例默认为1:8)区上,少数分配可能直接分配在老年代中,具体的分配规则不是固定的,具体细节还要看看是使用了哪一种垃圾收集器组合。
下面是几条常用的策略:
1).对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代的垃圾收集动作);
2).大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,最典型的是长字符串和数组。经常出现大对象会触发垃圾收集以获取足够的连续空间来“安置”这些大对象。把它们直接放置在老年代,目的是避免在Eden区和两个Survivor区之间发生大量的内存复制;
3).长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)记数器。如果对象在Eden出生并经过第一次Minor GC 后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

12.虚拟机性能监控
一般的Java开发人员只是下载jdk,然后配置环境变量,最后集中精力去开发应用。然而jdk中包含有很多东西是我们没用过的,比如虚拟机的监控工具,jdk自带的,可以查看运行在虚拟机上的进程,内存使用情况,跟linux的进程查看差不多的感觉。第一次点开有点震撼,进入jdk的bin目录下,奥妙就在这里。
先看看几条简单的命令:
jps:虚拟机进程状况工具

2019120001671\_8.png

jstat:虚拟机统计信息监视工具
其中的S0、S1、E、O、P、YG、FC分别指两个Survivor区、Eden区、老年代、永久代、新生代收集、老年代收集的相关信息

2019120001671\_9.png

jstack:Java堆栈跟踪工具

2019120001671\_10.png

Jconsole:Java监视与管理工具
是不是看起来很神奇?

2019120001671\_11.png

13.虚拟机的调优
这里推荐以看书为主,整个第5章都写的不错。案例分析的自行看书,简单总结一下以eclipse为例子的调优策略,基本就是牺牲空间换取时间,追求速度。我在eclipse官网下了4.3的,然后按着书里的调优,启动速度应该是快了那么一点点,感觉不出来(主观感受)。具体的调优有下面几方面:
-Xverify:none//禁用字节码验证
-Xmx512m//新生代
-Xms512m//堆
-Xmn128m//永久代
-XX:PermSize=256m//1
-XX:MaxPermSize=256m//2 1和2都是让老年代和永久代的内存容量固定,不发生扩容
-XX:+DisableExplicitGC
-Xnoclassgc//忽略程序的GC
-XX:+UseParNewGC//3
-XX:+UseConcMarkSweepGC//4 3和4采用ParNew与CMS垃圾收集器
-XX:CMSInitiatingOccupancyFraction=85

原先的eclipse.ini:

2019120001671\_12.png

调优后的eclipse.ini

2019120001671\_13.png

14.Java一大特性—无关性
无关性包括语言无关性和平台无关性,平台无关性更受世人关注,以前有个大神说过:“你至少做两年以上,才知道Java是干什么的。”,说的很对,在书上总看到Java是一门跨平台、安全、并发、分布式…的语言,但是到底体现在什么地方,需要积累一定的代码量才能理解,下面是两个无关性的原理图。

语言无关性:

2019120001671\_14.png

平台无关性:

2019120001671\_15.png

计算机只认识0和1,许多编程语言程序经编译器翻译成由0和1构成的二进制格式才能运行。但是平台不同,执行程序的指令也不同.。在Java中,程序员编写的代码称为源文件,经由编译器编译后成一种中间的class文件,再与类库相结合,形成一种被jvm执行的jar存档文件,再被jvm执行,所以不管是何种平台,linux也好,window也好,编译出来的class文件都可以被虚拟机识别,换句话说,window下编译出的class文件扔到linux上去运行,也没有问题,这就是所谓的平台无关性。
也正是因为虚拟机规范要求在class文件中使用许多强制性的语法和结构化约束,担任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交互媒介,这便是语言无关性。

15.class 类文件的结构
class 文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本的数据结构,表是由多个无符号数或者其它表作为数据项构成的复合数据类型。从形状上看,class文件的内容就好像一连串数字,每一个部分都代表不同的意义,主要分为8个部分:

2019120001671\_16.png

1).魔数:class文件的头4个字节,唯一的作用是确定这个文件是否为一个能被虚拟机所接受的class文件;
2).版本:第5、6个字节是次版本号,第7、8个字节是主版本号,Java的版本号是从45开始的,JDK1.1以后的每个JDK大版本发布主版本号加1。JDk是向下兼容class文件的;
3).常量池:是class文件的资源仓库,主要存放两大类常量:字面量和符号引用;
4).访问标志:紧接在常量池后的2个字节,用于识别一些类或接口层次的访问信息;
5).索引:分为类索引、父类索引与接口索引,用来确定类的继承关系;
6).字段表集合:用于描述接口或者类中声明的变量;
7).方法表集合:描述方法;
8).属性表集合:在class文件、字面量、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息;
总之,class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可拓展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。
这里面包含的详细信息太多,还是推荐看书为主。

16.字节码指令
Java虚拟机的指令由一个字节长度的、代表者某种特定操作含义的数字,由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。指令有什么用呢?我们写Java代码经编译后成为class文件,虚拟机通过指令去执行class文件的内容。Java虚拟机的指令集有个特点:对于特定的操作只提供了有限的类型相关指令去支持它。
指令的分类有加载和存储指令、运算指令、类型转换指令、对象创建与访问指令、操作数栈管理指令、控制转移指令、方法调用和返回指令、异常处理指令和同步指令。
下面通过一个简单的例子来说明:
源代码:

//:object/HelloDate.java
    import java.util.*;
    /**The first Thinking in Java example program. *Displays a String and today's date *@author wayne *@version 4.0 */
    public class HelloDate{
        /**Entry point to class & application. *@param args array of string arguments *@throws exceptions No exception thrown */
        public static void main(String[] args){
            System.out.println("Hello World,it's:");
            System.out.println(new Date());
        }
    }/*Output:(55% match) Hello World. it's: Web July 14 17:42:36 MCT 2016 *///~

待编译后用javap命令查看字节码:

[wayne@wayne
    fedora23 summer]$ javap -verbose HelloDate.class
    Classfile /home/wayne/summer/HelloDate.class
      Last modified 2016-7-14; size 505 bytes
      MD5 checksum 6aecfb43300a9eeca9f69bb859d6a1d6
      Compiled from "HelloDate.java"
    public class HelloDate
      SourceFile: "HelloDate.java"
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref #9.#18 // java/lang/Object."<init>":()V
       #2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String #21 // Hello World,it's:
       #4 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class #24 // java/util/Date
       #6 = Methodref #5.#18 // java/util/Date."<init>":()V
       #7 = Methodref #22.#25 // java/io/PrintStream.println:(Ljava/lang/Object;)V
       #8 = Class #26 // HelloDate
       #9 = Class #27 // java/lang/Object
      #10 = Utf8 <init>
      #11 = Utf8 ()V
      #12 = Utf8 Code
      #13 = Utf8 LineNumberTable
      #14 = Utf8 main
      #15 = Utf8 ([Ljava/lang/String;)V
      #16 = Utf8 SourceFile
      #17 = Utf8 HelloDate.java
      #18 = NameAndType #10:#11 // "<init>":()V
      #19 = Class #28 // java/lang/System
      #20 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
      #21 = Utf8 Hello World,it's:
      #22 = Class #31 // java/io/PrintStream
      #23 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
      #24 = Utf8 java/util/Date
      #25 = NameAndType #32:#34 // println:(Ljava/lang/Object;)V
      #26 = Utf8 HelloDate
      #27 = Utf8 java/lang/Object
      #28 = Utf8 java/lang/System
      #29 = Utf8 out
      #30 = Utf8 Ljava/io/PrintStream;
      #31 = Utf8 java/io/PrintStream
      #32 = Utf8 println
      #33 = Utf8 (Ljava/lang/String;)V
      #34 = Utf8 (Ljava/lang/Object;)V
    {
      public HelloDate();
        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 8: 0

      public static void main(java.lang.String[]);
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=1, args_size=1
             0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3 // String Hello World,it's:
             5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
            11: new           #5 // class java/util/Date
            14: dup           
            15: invokespecial #6 // Method java/util/Date."<init>":()V
            18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
            21: return        
          LineNumberTable:
            line 14: 0
            line 15: 8
            line 16: 21
    }

首先是文件的信息,路径、最后修改时间、从哪个java文件编译过来的。接着是版本信息,0代表第一个次版本号,51代表主版本号,45是JDK1.1,逐次加1,51代表JDK1.7。接着是访问标志flags,说明该文件是公有的,再接着是常量池,它包含了文件中所有的字面量和符号引用:Methodref表示类中方法的符号引用,Fieldref表示字段的符号引用,String表示字符串类型字面量,Class表示类或接口的符号引用,Utf8代表utf-8编码的字符串常量,NameAndType代表字段或方法的部分符号常量,具体表示文件中的哪个部分看注释即可。再接下来就是字节码指令,aload将一个局部变量加载到操作栈,invokespecial指令用于一些需要特殊处理的实例方法,包括实例化初始化方法,私有方法和父类方法等,return是返回,getstatic是访问static的类字段或实例字段,ldc将一个常量加载到操作数栈,invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派,new是创建实例的指令,dup复制栈顶一个或两个数值并将复制值或双份的复制值压入栈顶。

17.总览虚拟机类加载机制
Class文件中描述的各种信息,最终都需要加载到虚拟机之后才能运行和使用。而虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
值得一提的是,在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java是一门静态类型语言,Java里面天生可以动态拓展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
先看看类的生命周期,从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。如下图:

2019120001671\_17.png

18.类加载的过程
1).加载
在加载阶段,虚拟机需要完成3件事:
(1)通过一个类的全限定名获取定义此类的二进制字节流;
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;
2).验证
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。验证阶段大致上完成下面4个阶段的验证动作:
(1)文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理;
这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证,字节流才会进入内存的方法区进行储存,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
(2)元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,保证不存在不符合Java语言规范的元数据信息;
(3)字节码验证
通过数据流和控制流分析,确定程序是语义是合法的、符合逻辑的,保证被校验的方法在运行时不会做出危害虚拟机安全的事件;
(4)符号引用验证
可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行;
3).准备
准备阶段是正式为类变量分配内存并设置类变量初始值阶段,这些变量所使用的内存都将在方法区中进行分配。这里进行内存分配仅仅是类变量(被static修饰的变量),而不包括实例变量,实例变量将在对象实例化时随着对象一起分配在Java堆中;
4).解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行;
5).初始化
初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化是如何被触发的:
(1)遇到new、getstatic、putstatic或involestatic这4条指令时;
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候;
(3)初始化一个类时,如果父类还没被初始化,则先触发父类的初始化;
(4)虚拟机启动时,用户需要指定一个要执行的主类 (包含main()方法的那个类),虚拟机会先初始化这个主类;
(5)如果一个 java.lang.invoke.MethodHandle 实例最后解析的结果是 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,若句柄所对应的类没有进行过初始化,则将它初始化;
初始化的执行其实是执行类构造器()方法的过程,而()方法的执行过程,涉及的内容比较细节,自行查找资料了解。

19.类加载器
把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个代码模块称为“类加载器”。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的名称空间。也就是说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,即使是来自同一个class文件,被同一虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。

import java.io.*;

    public class ClassLoaderTest{

        public static void main(String[] args) throws Exception{

            ClassLoader myLoader = new ClassLoader(){

                public Class<?> loadClass(String name) throws ClassNotFoundException{
                    try{
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if(is == null){
                            return super.loadClass(name);
                        }
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e){
                        throw new ClassNotFoundException(name);
                    }
                }
            };

            Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();
            System.out.println(obj.getClass());
            System.out.println(obj instanceof ClassLoaderTest);
        }
    }/*Output class ClassLoaderTest false *///:~

一个是由系统应用程序加载器加载的,另外一个是由自己定义的类加载器加载的,所以两个对象所属的类并不相等。

20.双亲委派模型
先了解一下类加载器:在14中的平台无关性中,我们写的代码会与基本类库结合,这个结合的动作是虚拟机完成的。绝大部分Java程序都会使用到3种系统提供的类加载器,这3中类加载器的前2种会帮忙加载类库。
1).启动类加载器
负责将存放在\lib 目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中;
2).拓展类加载器
负责加载\lib\ext 目录中的,或者被java.ext.dirs 系统变量所指定的路径中的所有类库;
3).应用程序类加载器(也叫系统类加载器)
负责加载用户类路径上所指定的类库(类似于 javaee 中引进的第三方jar包);
除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。我们的应用程序都是由这3种类加载器互相配合进行加载的,它们之间的层次关系,称为类加载器的双亲委派模型:

2019120001671\_18.png

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。双亲委派模型带来的好处是使Java类随者它的类加载器一起具备了一种带有优先级的层次关系。
不过,虽然双亲委派模型一直沿用至今,但是局部也发生了一些变化,最有名的是OSGi技术。它把树状结构的双亲委派模型变化为网状结构,有很大的灵活性,但是复杂度也随着上升。

点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 《深入理解Java虚拟机》--Understanding the Jvm(上)

相关推荐