Java 虚拟机解析系列
[jvm 解析系列][一]Java 内存区域分配和内存溢出异常 OOM
学过操作系统的同学应该比较清楚,一个操作系统必须要有完善的内存管理系统(页/段式的管理),相应的 jvm 全称 java 虚拟机应该也有类似的一种管理内存的方式,这种方式是建立在真实的操作系统内存管理方式之上的,他把内存分配成了不同的区域,形成了 java 内存模型。
那么,对于其他博客讲解这种题目要先抛一个图解出来,我并不想这样。因为这种模型的出现肯定是要解决问题的,我们需要顺延着前人设计 jvm 内存模型的脚步,看看到底为什么要这样设计,才能更好的理解它。
首先,我们思考一下,java 需要什么区域呢?
1、必不可少的,一个程序计数器,用来记录当前程序运行的字节码指令,jvm 使用了栈而不是寄存器结构(往后看相信你会理解的)。对于线程来讲,每个线程肯定是需要记录自己的执行位置,所以程序计数器是线程私有的
2、既然有了程序计数器我们还需要一个区域来存放当前运行的程序,那就是 Java 虚拟机栈,每一个方法的执行就会添加一个栈帧在虚拟机栈里(如果想要详细的了解栈帧请关注博客,随着系列的加深,我会一个个讲解)。对于线程来讲虚拟机栈同样的是线程私有。
3、一样的常用 jvm 的都知道还有一种 jni 的调用,为了这种方法的运行,设计了一种跟 java 虚拟机栈的双胞胎区域叫本地方法栈,除了运行 native 方法外其他的几乎一致。(在有的虚拟机里不区分 native 和 java)
4、大家都知道 java 是面向对象的,既然方法都出来了,那么类存储在那呢?没错,jvm 也分化出来了这样的一个区域叫做方法区(也称为永久代,因为这个区域很长时间不会发生 gc)。这个区域里存储了虚拟机家在的所有的类信息,常量和静态变量等。(内含一个运行时常量池主要用于存放编译器生成的字面量和符号引用)他是线程公有的。
5、如果类都有了,那么对象呢?没错这就是 jvm 的重头戏,很多工作都在这个地方完成,他就是传说中的 java 堆,在有的地方甚至把 jvm 笼统的分为堆栈,可见 java 堆的重要性。java 堆里面存放了几乎所有的对象实例,在 java 虚拟机规范中这样说:所有的对象实例和数组都要在堆上分配。随着 JIT 编译器等的发展也不那么绝对了。(java 堆仍然很复杂,后期单开一篇博客详解)。它肯定是线程公有的。
6、到这里,好像 java 所有的需要都被分配了,以前确实是的,但是在 JDK1.4 之后,java 引入了 nio 技术(不在本章范围内讲解), 这就要求需要分配一个新的区域,没错,他叫直接内存。值得一提的是,这一区域并不是 jvm 的规范中的一部分,也不是虚拟机运行时数据区的一部分,但是他会引起 OOM。
那么至此所有的内存区域分配完毕,配图如下帮助理解,相信你已经差不多的了解和理解了 jvm 分配的方式和目的了。
内存区域分配配图

上节说到~jvm 的内存分配,所以对应的应该有 6 种溢出方式,实际上程序计数器一般不会溢出,我又把两个方法栈合在一起讲,所以溢出的可能性有 4 种
嘿嘿嘿嘿,我要去考试了,等我回来再补吧~
回来了继续补
1、Java 堆溢出
java 堆溢出一般都是因为在 for 或者递归里调用了太多次 new 对象的操作,并且可以被 GCROOT 查找到不能 GC,而导致 java 堆内存不足报错,报错一般如下:
OutOfMemoryError:Java heap spce
2、虚拟机栈和本地方法栈溢出
虚拟机栈和本地方法溢出一般也是陷入了无限循环里并且在无限循环里一直在调用一个方法,因为一般情况下栈深能有 1000~2000, 报错一般都是 StackOverflowError,OOM 反而不常见,在处理这种异常时很棘手,需要减少其他区域的内存,因为这块内存是用总内存减去堆和方法区得到的。报错如下:
StackOverflowError.
3、方法区和运行时常量池溢出
这个区域想要溢出单单使用创建类的方法和常量是困难的,所以可以使用 String.intern () 方法直接将字符串加入到常量池。报错一般如下
OOM:PermGen space
4、本机直接内存溢出
由于上文知道直接内存一般用于 NIO 操作,所以使用了 native 方法而且因为这个区域的特殊性 Heap dump 并没有明显的报错。错误信息如下:
OutOfMemory。
[jvm解析系列][二]Java堆的详细讲解和对象的分配过程和访问
上回说到Jvm内存的分配,犹如划地分治,把一块本机内存分裂成了6块。
这回我们就讲讲java堆的详细信息
java堆里也不是铁板一块,类似jvm的分治,java堆内部也好不到哪里去,大致上可以分成新生代和老年代,他们内部也不和谐,新生代又可以细分为Eden和两个survivor空间。
(按照复制算法画图并解析,其他算法请见第四章)图片如下:

是不是看着大小比例很别扭呢,没办法呀,就是按照内存大小画的-。-iii
至于他们的作用的,请关注博客,下回分解。那么回到原题,我们分解了java堆的内部情况,接下来是不是就要讲对象的分配过程了?
yeah,对象怎么分配的呢?(默认以hotspot 虚拟机为例)
在hotspot中把对象分成了对象头,实例数据和对齐填充。
在对象头中包括了两部分分别是:
1、存储对象自身的运行时数据:hashcode,GC分代年龄信息(后面会详细说明),锁标志等等。
2、类型指针:它指向它的类,没错就是关在方法区的那家伙。(个别例外没有,如数组)
在实例数据中才是干货:
定义的各种类型的字段内容(包括父类继承)。
还有一部分是对齐补充
因为内存管理的系统要求对象起始地址必须是8字节的整数倍,如果对象不能正好8字节整数倍结束只能凑了。
好了,对象的问题解决了,那么我们分配了总不能不用吧,那么我们该怎么找到我们的对象呢?
想想我们我们什么时候喜欢使用对象呢?在方法中调用!像这样XXX.xxx();对吗?
还记得方法被关在哪了吗?不记得请回去看看第一篇,谢谢。
好,你不看我就告诉你吧,方法在调用的时候被转化成栈帧,放在了方法栈里。在方法栈里调用的对象是被存储成reference数据的,这个reference可以看成一个指针,其实它也就是个指针,在JVM规范中它被规定成了指向对象的引用。我们拿到这个reference的内容就能找到对象的内存地址了。
不对!现在男女比例这个鸟样,程序猿想找对象哪有那么简单?
这就要看你是什么虚拟机了?你要是高富帅虚拟机还是简单的,你找对象估计就是这个过程:

但是屌丝虚拟机怎么办?屌丝找对象都说是个女的就行。。一般也找不着。。最后还不是都是靠媒婆(句柄)

当然它们各有各的好处,不然也不会同时存在。
高富帅虚拟机他比较省时间,直接找对象哪,不要媒婆在中间当然会节省时间,但是这种自己谈的对象感情深啊,每次对象出差都要揪心(对象在堆中GC时会被移动,每次移动都需要修改reference的数据)
屌丝虚拟机找对象是慢了点,但是大家感情都不深,对象出去出个差也不是那么揪心,只要在句柄那修改一下就好了。
[jvm解析系列][三]Java的垃圾回收(一)如何鉴别垃圾,四种引用类型
垃圾回收就想垃圾车,每次天亮就会沿着街区开一圈,把垃圾都带走。
有的区域不需要这种垃圾车,也许它们不造垃圾吧,作为一个中级图钉我对这种地区很无奈,但是它们就是很少有垃圾甚至没有垃圾。首先应该是程序计数器,这玩意要什么垃圾回收,我都不用讲。下一个应该是虚拟机栈本地方法栈,栈帧的进出都受到控制,谁的垃圾谁带走,在进门的时候就算清楚了你要用多少内存,最后你再全部带走一点垃圾也不剩也不需要。(还有一个原因,是上述三个区域都是线程私有的,当线程撤退的时候把垃圾也都带走了)
那么根据第一章讲的,我们还剩几个区域?不记得就回去看吧^_^
还剩方法区(又称永久代)和java堆。也就是说只剩这两个区需要垃圾回收了,垃圾佬表示很兴奋啊,终于有垃圾收了。(虽然叫永久代,但是现在永久代也是回收垃圾的,只是垃圾的回收率低,并且条件十分苛刻)
那么什么是垃圾呢?垃圾如何定义呢?
嗯,在我们的概念中,不要的东西就是垃圾。在程序中如何确定一个对象我们不用了呢?,引用计数法,也就是每当该对象被引用时+1,被去引用时-1,到引用为0的时候也就是该对象变垃圾的时候。嘿嘿嘿,我又瞎说了,没错是有这么个用法,但是主流jvm一个都没有使用这种方法定义垃圾的!
为什么呢?这个方法不是挺好的么?
我举个例子,如果我有
A a = new A();
B b = new B();
a.b = b;
b.a = a;
这样的情况下是不相互调用了?如果看不懂没关系我画幅图给你

你蒙蔽了吗,是不是a和b对象直接就这样泄漏了,我的妈,你的android如果用这种方式定义垃圾你是不是半天就得重启一回防止累计下来的大量的内存泄漏呢?
所以我们的jvm没有选择引用计数算法,我们采用了可达性分析算法
在大多数商用的有垃圾回收的语言中多数都是采用这个算法的。简单的说来就是从某一个GC Roots开始往下找引用,没找到的就是垃圾。具体请看图:

图一帖上来打架都应该清楚了。1和2是垃圾,要回收。这个就是可达性分析算法。那么1说了:“凭什么root可以作为GC Roots”而我不行!“2跟着附和:”这中间肯定有不可告人的“某图钉:”py交易“。
其实并不是这样,GC roots是有要求的,我们来分析一下,我们说一个对象不是垃圾,肯定我们是能得到的,我们在写程序的时候什么类型是可以在任何地方都能得到的呢?
1、虚拟机栈中引用的对象(即我在写方法的时候A a =new A(),在方法没结束的时候就是a就是可以作为root使用的)
2、方法区中类静态属性(所以啊,我们应该少用类的静态属性,不然内存回收不了)
3、方法去中常量引用的对象
4、本地方法栈中JNI引用的对象
当一个类型是上述四个中的一个的时候,能跟他攀上关系的对象都不会被回收。
但是!后来改革了你知道吗?(四种引用强度)原本的时候攀上一个大佬就高枕无忧了,但是在jdk1.2之后这些对象引起了重视被改革了。
号外号外!一号文件规定以后引用不再是boolean类型了,不再是有或者没有的问题了~
它变成了四类引用
1、强引用
强引用是大家最常见的引用,他是jvm必不可少的,当强引用过多时宁愿oom也不回收内存,最常见的应该就是new出来的对象了。
2、软引用
软引用如果jvm不缺内存的话不会回收,一旦缺少内存就会回收软引用而不是报oom。SoftReference一般都是软引用
3、弱引用
弱引用更不用说了,只要被jvm扫描到了立即回收。WeakReference一般都是软引用
4、虚引用
虚引用。。。这玩意一不小心就没了,至于他为什么和弱引用分开是因为他有特殊的机制,在jvm回收内存的时候如果这个对象是虚引用,必须把这个虚引用放在引用队列里,如果引用队列里有一个虚引用说明这个对象要挂了。
[jvm解析系列][四]Java的垃圾回收(二)垃圾收集算法,内存分配和回收策略
上回说到如何鉴别一个垃圾。
这回咱们讲讲怎么收集垃圾收集垃圾有几种算法如下:
1、标记-清除算法
这个算法最为基础,我们先讲算法再说优缺点。
实现过程:
标记出所有需要回收的对象,当标记完成后统一回收。图解如下:

优缺点:
可能画出来图的时候大家都发现了,这个算法有一个很明显的问题,那就是大量的不连续的内存碎片,这样的内存碎片遇到大对象分配的时候很可能遇到内存不足的 情况,当然出了这个情况以外还有一种问题就效率太低(可以对比之后的算法)。
2、复制算法
实现:把内存划分为大小相等的两块,当GC的时候,就把其中一块复制到另一块上,然后直接清理掉原本的那一半内存。

优缺点:
这种算法要比标记清除算法效率高并且没有内存碎片,但是这样会浪费一半内存而且如果存货较多对象,复制效率也很低。
修正与应用:
现在商业虚拟机大多都采用这种方式回收新生代,但是不回划一半内存那么大。它们把内存氛围Eden空间(80%)和两块Survivor空间(10%+10%),每次GC时, 从Eden和一个survivor区里复制活着的对象到另一个survivor里,因为新生代 GC频繁且效率高,所以一半清除后的对象一个survivor基本可以存下,但是如果空间不够 用,就会引起老年代的分配担保(在本章稍后讲解)。
3、标记-整理算法
实现:
让所有存活的对象向前端移动,最后清除后面的内存

优缺点:
效率高于标记-清除算法,而且可以保证所有的内存都可以使用
应用:
这种算法可以有效的用在老年代中,因为老年代gc不频繁而且每次效率不高。
我们了解到了怎么清理内存之后,这在之前还有一个问题,分配对象的规则是什么呢?
1、分配对象
我们用流程图来讲解:

1、我们可以看出来一个对象在分配的时候先要看看Eden区是否可以装得下(或者设置了PretenureSizeThreshold参数,根据参数来判断)
2、如果装不下就直接分配到老年区
3、如果分配的下又会查询Eden空间是否是充足
4、如果Eden剩余可用内存充足就把对象放在了Eden区域
5、如果Eden不充足的话就会引起MinorGC(新生代GC,新生代GC效率较高大概是FullGC(老年代GC)的十倍),如果发现Eden中的对象不足以放在survivor中就 会直接放在老年代里,然后分配对象到新生代。
2、长期存活对象转入老年代
图解:

特殊情况:
虚拟机中并没有严格的遵守必须年龄>=MaxTenuringThreshold才能转到老年代,如果Survivor空间中相同年龄所有对象大小的综合大雨Survivor空间的一半,年 龄>=它们的也可直接转到老年代。
3、空间分配担保
实现:
在MinorGC之前每次都会查看老年代的最大连续可用空间是不是大于新生代所有总对象总空间,如果大于的话说明新生代可以转入老年代,如果空间不足保证所有的 survivor区域里的话说明不安全,因为很有可能MinorGC后晋升老年代,老年代根本存不下,所以这个时候要查看是否设置了允许冒这个险,(参数 HandlerPromotionFailure)除了检测参数外还要查看是否大于以往晋升对象的平均值,都允许的话就开始冒险。如果不允许就先FullGC。
冒险:尝试把MinorGC后剩余的对象放入老年代,如果成功最好,省了FullGC,如果失败只能跑一次FullGC了,时间花销最大。
图解如下:

[jvm解析系列][五]类文件结构,魔数,最大最小版本号
上一会讲完了JVM的内存分配和垃圾回收策略我们该讲一讲如何组织一个class文件了
一个class文件怎么被加载运行的?
我们可以说java的野心很大,早在97年的时候JVM虚拟机规范中就说以后可以支持其他语言,到JDK1.7的时候基本已经实现了,怎么做到的呢?
这是JVM做的语言无关性即JAVA/Scala/JRuby等都可以编译成class文件,对于JVM而言我不管你之前什么文件反正我只要class文件就好了:

很明显的,我们这一章就是用来讲解class文件的结构的
这一块比较复杂,我们不做特别详细的讲解,大概了解一下class文件的结构即可
class文件时一组以8字节为基础单位的二进制流,它们中间的数据是按照顺便紧密排列的,也没有用到任何的分隔符,所以规则的理解相当重要。
Class文件格式采用一种类似于C语言的伪结构来存储数据,但是只有两种数据结构,分别是无符号数和表
1、无符号数:
无符号数分别以u1,u2.u4,u8来表示1248个字节,无符号数主要用来描述数字,引用,字符串。
2、表:
表是由无符号数或者其他表作为item的复合的数据结构,这种数据结构是具有层次的(可以类比Java里的list<T>还是带泛形的这种)。于是我们可以说class本身就是一种表,复杂的表,里面包含如下的内容:

虽然画的是烂了点,但是这幅图还是能够表达出一个class的结构,这幅图从左往右,从上往下顺序排列就是class的结构。
在接下来的任务里我们就是一个个的讲解这个大的图。
1、magic魔数
作用:用来确定这个文件是否为一个能被虚拟机接受的Class文件。说白了这个东西的作用跟后缀名.class一样,只是怕你乱改后缀名所以加一个魔数免得你改一个.png成.class去骗他,class文件的魔数为:0xCAFEBABE。
2、minor_version和major_version
作用:确定版本号,minor是次版本号,major是主版本号,Java的版本号从45开始。jdk1.0使用了45.0~45.3之后的每一个版本加一,到jdk1.7的时候版本号是51.0。我截取以前写的类我们来看一看。

我已经给大家划出来了几个部分,怎么样没骗你们吧
[jvm解析系列][六]class里的常量池,访问标志,类的继承关系,如何把一个类在字节码中描述清楚?
上回我们说到了魔数和版本号,今天我们接着说。为了方便起见,我把那幅图拉过来方便大家看

由图可见接下来是一个叫constant_pool_count翻译过来叫常量池数量,前面我们说到class文件中只有两种数据结构,无符号数和表,而且整篇没有分隔符,在没有分隔符的情况下我们怎么区别数量非1的表的分界线呢。(举个例子:图中的constant_pool,我们想想该怎么标记出constant_pool的结束位置?)没错他的结束位置全都是靠前面的u2类型的constant_pool_count实现的,我打开一个class文件大家看一下。
这里我把43划掉了为什么呢?因为设计者把0空出来了也就说常量池一共有[1,43)个常量所以是42。(只有常量池0空缺,其余都从0开始)
也就是说接下来有42个cp_info表类型的数据结构,那么我们来看一下cp_info怎么构成的。
常量池中我们存什么?字面量和符号引用。
1、字面量,百度百科中这样解释:

其中有几个关键字,源码中,固定值。仔细阅读下来应该跟Java中常量差不多。不过它们的区别在于。字面量一般为右值也就是说int a = 0;这个0就是字面量。
2、符号引用,一般指类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。说白了也就是类和接口的名字以及字段的名字和类型,方法的名字和返回值等能标志出自己的东西。
我们新开一个小一点的类,来仔细看一下它的结构(这个类只实现了一个main方法,很简单的一个类,没有其他变量,静态变量存在,之后解析都是这类)
先上类图:

类的常量池count和常量池:

我们可以看到它的常量池是0x10转化成十进制就是16再去掉0位就是一共15个常量
我们使用javap -verbose xxxx.class来看一下这个class的结构。

我们可以看出来第一列有很多类似于Class,Utf的名称,这个就是cp_info的表结构,这个表结构有一个特殊的地方,就是开始都会有一个u1的标志位。里面一共有11种类型,在jdk1.7后新增了3中暂且不表。着11种类型,这11种类型我们可以分为两种。
1、引用类型:上图中的Class,Methodref,NameAndType以及不在上面的Fieldref和InterfaceMethodref
2、字面量:Utf8,interger float long double string
引用类型的意思是这些类型是引用其他类型的,这样说不好理解我们举个例子,看图#1是class类型他引用了#2,#2是utf8类型代表了Class的限定名。
关于这11个详细的结构网上有很多,我们的class解析是初步解析,以后我可能会加进来,这次暂且不加。其实这一块很简单结合javap完全可以看得懂,相信大家多看看也可以看懂。
那么根据最上面的图,结束了常量池之后就是访问标志。这个类用来形容这个class文件是类还是借口,是public还是abstract,是不是有final修饰符呢。同样的,我们打开一个class文件来看一下

图中所示访问标志位0x0021,那么我们怎么看出来它的信息呢?我送你一张表来看:

请叫我灵魂画师,上面还残留了我输入的框框忘了点掉了。。。。。。
那么根据上图来看我们刚刚剖析的那个类应该占用了public和super两个所以值是0x0021
再根据最上面那个图来看看呢我们接下来应该讲this_class和super_class还有一个不定长的interfaces,关于interfaces这种结构我们在常量池中已经讲过了。其实我们分析到现在,关于一个类的描述也基本完结了(下一节讲方法),除了类的继承关系没讲,那我们就把泪的继承关系讲完来结束掉他吧。
1、this_class顾名思义,其实指向的就是类的全限定名
2、super_class,这个讲的就是弗雷德全限定名了
3、interface,当然就是说接口了。
来来来,我给大家看一下图
00
一不小心少截了一个00,我自己打字补上的,其实thisclass是01,superclass是03,接口没有继承所以count是00。我们看看之前我们的常量值都索引到哪里去了

可以看到01是指向Test类的,03指向Object类跟我们预测的一样,到此我们的类的描述就算讲完了,出了常量池中那11个表结构没有讲,但是我们一般借助javap工具是不需要了解那些表结构的,之后有时间再补上
[jvm解析系列][七]字段表集合、volatile、volatile和属性表集合,你的变量该如何存储?
上段我们说到一个类的基本构成,今天我们来讲一下类的内部结构。类的内部一般情况下由类的成员变量(类字段),方法和内部类等构成。
1、字段表:
不好意思我们这一次需要先上字段表的内容然后一个一个讲解,不能像前面那样像设计者一样一个一个的推出来了,主要原因还是字段表里包含了属性表。

access_flags
还记得我们在上一篇里讲的,在类的继承关系之后应该就是field_info他主要描述接口或者类中声明的变量。field包括类和实例级变量(声明在方法外的变量)。那么我们想一下一个成员变量应该有什么?我们平时这样声明一个变量。如:private int a = 0;所以,首先要有的应该是一个作用域(public private protected),接下来是一个int,这个被叫做字段数据类型(也就是int或者其他类型,还有对象,数组)。当然有时候我们还会申请这样一个变量:public static final int a =0;那么我们应该还有一个用来表示他是否是final的变量,被叫做可变性。还有一个static变量需要描述。当然不能缺少了一个enum类型的变量,有时候我们还会见到一些不常见的修饰符,如volatile、transient。
volatile关乎到jvm的变量赋值问题,在jvm中,a++这个方法不是每次都要去堆内直接操作内存的,他被编译成了四个部分
1、把内存里的变量复制到自己的方法栈内
2、进行完修改,这里有赋值和加两部份
3、再赋值回去。这样如果有A,B两个线程同时访问变量a,并且同时修改+1,在返回堆中的时候a并不会+2而是+1。

所以volatile变量声明的变量可以保证每次读取的数据都是最新数据,但是注意,不可以保证原子性。
除此之外volatile还可以保证指令重排优化,这里我们以后会讲。
2、transient,在序列化的时候,被transient修饰的变量不参与序列化,默认不被transient修饰。
综上所述,一个字段表应该包括字段的作用域,static,final,volatile,transient,是否由编译器自动生成的,字段是否enum。如下图所示

name_index,descriptor_index
前面我们已经说过了,这个位置应该是一个field_info的表,既然是一个表就不可能仅仅只有上面一个最多u2类型的字段构成。所以除此之外我们还需要字段的简单名称和描述符。
1、简单名称就是如int a;a就是简单名称
2、描述符:描述符主要用来描述字段的数据类型、方法的参数列表和返回值。这一块不在JVM范围内,后面会在杂项中补充。
attributes
最后也就是一个需要细细讲解的部分,属性,在Class文件,字段表,放发表中都可以携带自己的属性表集合。
一个属性应该是下列这样的结构:

attribute_name_index 代表了一个常量池关于属性名称的索引。
而info代表了每一个属性的内容,我们并不强求info的长度,我们只需要一个u4类型的length来表示这个属性到底有多长就可以了。
而一个字段表内可以声明如下的属性:

关于每一个属性表的具体结构,我们会到放到下一章,因为方法表中也有属性,综合在一起讲一些比较常见的属性。
[jvm解析系列][八]方法表集合,Code属性和Exceptions属性,你的字节码存在哪里了?
根据我们第五章的总构图来看,这一章我们正该讲到方法表集合:
大家可能注意到在java中声明一个方法和声明一个变量很相似,public int a = 0;和public int a(){};于是在方法表集合中和字段表集合也很相似。
一个方法表的结构应当和下图一样:

对比字段表应该发现几乎是一样的。access_flags里的可选项略有不同而已。
access_flags:

这样以来我们把方法表和字段表对比来看应该很好理解了。对于属性表又是一大块内容。上次我们说到了属性表的结构
并且说了在字段表中常用的属性表。同样的今天我们贴出来在方法表中几个重要的属性表并详细讲解一下

在这里我们就贴了两个比较常见的属性:
1、Code
Java程序方法中的代码经过javac编译之后形成字节码存在了Code属性内,Code属性存在方法表集合内
code属性表结构如下:

从图上看max_stack属性,我们之前讲过,一个字节码的执行是依靠栈的,所以max_stack就是栈的最大深度
max_locals代表了局部变量表的所需空间,单位是Slot是虚拟机为局部变量分配内存所使用的最小单位。
code才是真正用来存放字节码指令的,每一个code占用u1类型,也就是0~255,就是说java最多可以表达256条指令,目前java只有200条左右的指令
exception对有的方法可以有,有的方法可以无,并不是一个必须的表,异常表的姐哦股如下图所示(注意跟Exception属性分开)

很明显多的他对应着try catch这种东西,start_pc和end_pc划分了try{},而catch_type代表了catch(exception)里面的那个参数exception,如果抓到异常就转到handler_pc处理。
不好意思各位我需要出门一趟,下午回来继续更新。已经回来了,继续更新。
除却了Code属性,接下来就是Exceptions属性了,它的表结构如下:

前两个不用解释了。
第三个number_of_exceptions表示了这个方法可能抛出number_of_exceptions种一场,其中的每一个一场就用exception_index_table表示,他只想了常量池中Constant_class_info类型的索引。
[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?
通过前面好几章的或详细或不详细的介绍,我们终于把字节码的结构分析的差不多了。现在我们面临这样一个问题,如何运行一个字节码文件呢?
首先,java语言不同于其他的编译时需要进行链接工作的语言不通,java语言有一个很明显的特性,那就是动态加载,一个字节码的加载往往都是在程序运行的时候加载进来的,很多时候这种方式给我们带来了便利。虽然从某种意义上来说他可能消耗了一定的资源降低了性能。
类的生命周期?
没错,一个类的生命周期,在很多人眼里可能类天生都摆在那里了,随着程序生,随着程序死。但是事实情况并不是这样,java语言的动态加载要求了一个类肯定有他的生命周期,一个类的生命周期有7个阶段,如下图所示:

这个图里第二行我特意放了三个并排就是因为第二行可以统称为链接,这中间遵循了严格的开始顺序,但是解析是有可能在初始化之后开始,这也是为了java语言的动态绑定而做的改变。并且这中间只有开始的先后关系,没有结束的先后关系。
首先我们说一说第一个情况加载:什么时候开始一个类的加载过程,jvm规范没有规定,不同的虚拟机有不同的实现。
加载这个过程主要是通过一个类的权限定名获取类的二进制字节流
然后把字节流转化为一种方法区运行时的数据结构
最后在内存中形成一个Class对象
图示:

这里面我们可操作性比较强的也就是加载字节码这个过程了,我们都很熟悉一个方法叫做loadClass,没错他就是用来加载字节码的,我们可以自定义一个类加载器重写loadClass方法去控制字节流的加载方式,于是我们可以从网络上获取,从数据库获取,从文件中获取,还有一种动态代理技术是通过计算生成的。
加载完成后就是验证了。
验证主要是确保Class文件里没有坑爹的东西,不会损害虚拟机自身。大概就是分为4个验证流程。
1、文件格式验证:我们之前讲过的魔数和大小version就是在这个位置验证的,常量池中是不是所有类型都支持,Constant_utf8_info是不是都是UTF8编码等等。这一阶段的目的主要是验证输入的字节流能够正确的解析,保证格式上的正确,如果通过了这个验证就把字节流加入到了方法区中,下面的验证都是对方法区的验证。(给几个名词链接魔数,大小版本号,方法区)
2、元数据验证:这一个阶段主要是看看元数据是否符合语义,像父类是否继承了不被允许的类(final类),是不是实现了父类和接口中所有要实现的方法等。
3、字节码验证;这个阶段的验证,主要是看看逻辑上有没有错误,比如有一个跳转指令跳到了方法的外部这种。
4、符号引用验证:这个大家应该很常见,比较常见的就是是不是能访问到某个类(不是private和protected),通过字符串描述的权限定名能不能找到对应的类。
在验证之后,我们就会开始准备。
在准备阶段里,会为类变量分配内存并且设置类变量(static修饰的变量)的初始值,而且诸如static int a = 1;这种情况,在准备阶段是不会赋值1的。而是赋值最基本的初始值0,因为1需要时初始化的时候在类的构造器中调用。
但是,如果字段变量被final修饰,这个字段表就会存在一个ConstantValue属性(详见ConstantValue),在这个时候这个变量就会被赋值,如static final int a = 1;这时候a就会被赋值为1;
在准备之后,jvm会开始解析过程。
解析在通俗意义上讲就是把常量池里的符号引用替换成直接引用。首先我们来解释一下这两个名词的意思
1、符号引用:符号引用就是指使用一组符号来进行引用,被引用的目标不一定加载到内存中
2、直接引用:直接引用是指直接指向目标的指针,相对偏移量或是句柄。
对于解析,jvm并没有具体规定什么时候执行,只要在操作符号引用之前进行解析就可以了。所以具体的实现还要看jvm是怎么设计的(在加载时解析还是在使用前解析)
接下来就是初始化了
我们之前说过,一个类的加载时间是要依靠具体的虚拟机而定,但是遇到主动引用时加载验证准备工作必须完结
有五种情况,我们把这五种情况叫做主动引用。(这之前已经进行了加载验证准备,遇到下面情况直接初始化即可)
1、遇到new、getstatic、putstatic和invokestatic这4条字节码时,对应到java语言就是new了一个类,调用了一个类的静态字段(被final修饰的已经在常量池了,不算)
2、使用java.lang.reflect包对类进行反射条用的时候。
3、初始化一个类,他的父类没有进行过初始化的时候,需要先初始化他的父类。
4、虚拟机启动的那个执行类(也就是带有public void main(String[] args){}方法的那个类)
5、jdk1.7之后新增特性动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个类没有初始化的时候。
当然除了这五个主动引用以外,其余所有方法都不会出发初始化,我们叫做被动引用,下面举几个例子。
1、通过子类引用父类的静态字段,不会触发子类的初始化。
2、通过数组定义来引用类,不会触发类的初始化,他把原来的类翻译成了另外一个类,这个类带有一些数组元素的访问方法。
3、常量不会触发类的初始化。
初始化的过程:
在初始化的时候就是真正开始执行java代码的时候了,这个时候会执行一个叫做类构造器的东西(<client>())他负责收集类中所有的static{}然后合并,顺序和原顺序一样,在static{}中只能访问定义在静态语句块之前的变量,对于后面的变量只能赋值不能访问。
我们之前就讲过在初始化一个类的时候,如果他的父类没有进行过初始化,则需要先初始化他的父类。于是父类的<client>()方法肯定优于子类执行,也就是说父类的静态代码块优于子类的静态代码块执行。但是接口的<client>()方法并不一定先于子类方法执行,因为父接口的<client>()方法是在调用时才执行的。
于是我们可以看出来,在整个加载过程中,程序员可以操作的部分仅仅只有ClassLoader(加载字节码)和初始化静态代码块部分。所以我们接下来就会讲ClassLoader
[jvm解析系列][十]类加载器和双亲委派模型,你真的了解ClassLoader吗?
上一章我们讲到,一个类加载到内存里我们可以操作的部分只有两个,一个是加载部分一个是static{},我相信static{}不用多讲了。
接下来我们就来解析一下ClassLoader即类加载器,他就是用来加载字节码到方法区的类。
当年出现ClassLoader这个东西动态加载类的字节码主要还是为了满足JavaApplet的需求。虽然后来JavaApplet挂掉了,但是ClassLoader这个形式还是保留了下来,而且活的很好。
类的相等和instanceOf:
来我们来写一个例子
-
public class ClassLoaderTest {
-
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException{
-
ClassLoader loader = new ClassLoader() {
-
-
public Class<?> loadClass(String name) throws ClassNotFoundException {
-
// TODO Auto-generated method stub
-
try {
-
String className = name.substring(name.lastIndexOf(".")+1)+".class";
-
InputStream is = getClass().getResourceAsStream(className);
-
if(is ==null){
-
return super.loadClass(name);
-
}
-
byte[] buffer = new byte[is.available()];
-
is.read(buffer);
-
return defineClass(name, buffer, 0, buffer.length);
-
} catch (Exception e) {
-
// TODO: handle exception
-
throw new ClassNotFoundException(name);
-
}
-
-
}
-
};
-
Object object = loader.loadClass("top.jjust.jvm.ClassLoaderTest").newInstance();
-
System.out.println(object.getClass());
-
System.out.println(object instanceof top.jjust.jvm.ClassLoaderTest);
-
}
-
}
上述代码先重写了一个loadClass方法,然后用重写的方法加载了自己这个类并生成实例。
两行输出结果如下
-
<span style="font-size:12px;">class top.jjust.jvm.ClassLoaderTest
-
false</span>
我们可以看到class是没错的,但是我们用重写loadClass读取的class文件却不被认为是原类的一个子类(可能说起来比较拗口,大家可以看看代码就明白了)
这边牵扯到一个相等的问题,判断两个类是否相等(equals、isAssgnableFrom、isInstance、instanceof)有一个前提就是:这两个类由同一个类加载器加载。如果两个不同的加载器加载,判断是一定不等的。
双亲委派模型
在jvm中,有两种不一样的类加载器,
一个是加载<JAVA_HOME>/lib下的文件,也就是jvm本身的类(并且jvm会识别名字,单纯放在目录下是没用的),也就是加载自身的类加载器
还有一种是加载其他类的类加载器。这个种类的类加载器又可以细分为扩展类加载器和应用程序类加载器。
扩展类加载器主要是加载<JAVA_HOME>/lib/ext文件下的类库而应用程序类加载器主要是加载用户类路径上指定的类库,平时getsystemClassLoader就是返回的它,程序里没有定义过自己的类加载器一般情况下也是用它。这几个类加载器我们用图片表示一下。

在类中加载需要按照一种层次,这种层次我们画在下面:

这是什么意思?也就是说碰到一个类需要加载时,先要把这个请求交给父类,直到顶层,如果Bootstrap ClassLoader说我不做这个,才会由下一层尝试加载。如果所有父类都不能加载,才会自己加载。
为什么要设计这种模型呢?我们具一格例子,就拿所有的父类Object讲,这个类一般是由Bootstrap ClassLoader加载的,如果不使用双亲委派模型,在自定义加载器中加载了这个Objcet,那么一个jvm中就会出现多个Objcet。而不是像双亲委派模型一样,都由Bootstrap ClassLoader加载,不会重复加载。
其实在重写方法的时候,建议重写findClass()方法而不是loadClass,下面是loadClass的源码
-
protected Class<?> loadClass(String name, boolean resolve)
-
throws ClassNotFoundException
-
{
-
synchronized (getClassLoadingLock(name)) {
-
// First, check if the class has already been loaded
-
Class<?> c = findLoadedClass(name);
-
if (c == null) {
-
long t0 = System.nanoTime();
-
try {
-
if (parent != null) {
-
c = parent.loadClass(name, false);
-
} else {
-
c = findBootstrapClassOrNull(name);
-
}
-
} catch (ClassNotFoundException e) {
-
// ClassNotFoundException thrown if class not found
-
// from the non-null parent class loader
-
}
-
-
if (c == null) {
-
// If still not found, then invoke findClass in order
-
// to find the class.
-
long t1 = System.nanoTime();
-
c = findClass(name);
-
-
// this is the defining class loader; record the stats
-
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
-
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
-
sun.misc.PerfCounter.getFindClasses().increment();
-
}
-
}
-
if (resolve) {
-
resolveClass(c);
-
}
-
return c;
-
}
-
}
我们发现,如果我们重写loadClass则会破坏双亲委派模型。
[jvm解析系列][十一]字节码执行之栈帧,你的字节码是如何运行的?
在之前的章节中我们讲解了jvm的内存分配和管理,class的文件结构,就差之行了。那么从第十一章开始我们就开始讲java虚拟机是如何执行一个class文件的。
首先我们应该明确虚拟机是区别于物理机的一种说法,物理机的执行引擎是建立在处理器,硬件 ,指令集之上的。而我们的虚拟机则由自己实现。在虚拟机中大致分为两种执行方式:解释执行和编译执行。
我们之前讲过,虚拟机运行方法的时候运行在java虚拟机栈里面,里面的存储结构是栈帧,需要了解一个虚拟机如何运行字节码文件的,首先我们需要了解一个栈帧的结构。
栈帧:
栈帧作为一个方法的代替者(在执行时)它肯定需要有方法的所有特征。那么我们在方法中定义的变量和方法传递的参数是不能缺少的。这一部分在栈帧中叫做局部变量表,其次每一个+-*/和其余的各种赋值等操作还需要占有一个区域,这个区域叫做操作数栈,在运行的过程中我们仍然有可能调用了其他的方法,这个时候我们需要一个类似于指针的引用去寻找方法的入口,但是我们直到在jvm中所有的引用都是用符号引用实现的,所以我们必须还需要一个动态链接的部分在运行时动态的把符号引用转成内存地址。剩下的还有返回信息对应了一个方法返回地址。所以我们得到了如下的一种数据结构,成为栈帧。

接下来我们来详细讲解每一部分:
局部变量表:
主要用于存放方法参数和方法内部定义的局部变量。我们之前分析过方法表,方法表中Code属性有一个参数是max_locals就定义了这个方法所需要分配的局部变量表的最大值。
局部变量表使用Slot作为最小单位。jvm中说一个Slot应该能存放一个boolean ,byte,char,short,int,float,reference,returnAddress类型的数据,所以Slot并没有一个固定的大小,随着上述类型的数据的长度不同,Slot的长度也不同。在Jvm中上述数据类型占用32位,所以Slot也占用了32位。如果遇到64位的数据类型Slot选择以高位对齐的方式分配两个连续的Slot空间。在执行的时候使用索引来寻找Slot,Slot同样也是可以复用的,因为一个Slot有可能不占用整个方法的生命周期,但是这种复用会影响GC。我们举个例子:
-
public class SlotGCTest {
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
byte[] holder = new byte[64*1024*1024];
-
System.gc();
-
}
-
-
}
使用上面的代码进行垃圾回收,应该符合我们的预期这64mb的内存不会被回收,因为依然可以通过GCROOT找到。使用-verbose:gc参数输出结果如下:
-
[GC (System.gc()) 67502K->65976K(188416K), 0.0007246 secs]
-
[Full GC (System.gc()) 65976K->65811K(188416K), 0.0039451 secs]
但是我们把gc的代码放在方法外呢?
-
public class SlotGCTest {
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
{
-
System.out.println("构造代码块");
-
byte[] holder = new byte[64*1024*1024];
-
}
-
System.out.println("main");
-
System.gc();
-
}
-
-
}
我们从上述代码中可以看出,我们gc的位置已经在构造代码块之外了,GCROOT应该找不到holder了,这64mb的内存必定会被回收。输出如下:
-
构造代码块
-
main
-
[GC (System.gc()) 67502K->65928K(188416K), 0.0018770 secs]
-
[Full GC (System.gc()) 65928K->65811K(188416K), 0.0057137 secs]
我就问你蒙蔽吗?为什么还是没有回收呢?这就是Slot复用对GC的影响了,我们稍微加一行代码
-
public class SlotGCTest {
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
{
-
System.out.println("构造代码块");
-
byte[] holder = new byte[64*1024*1024];
-
}
-
int a = 0;
-
System.out.println("main");
-
System.gc();
-
}
-
-
}
我们只加了int a = 0 ;这一行代码。最后输出如下:
-
构造代码块
-
main
-
[GC (System.gc()) 67502K->65992K(188416K), 0.0011489 secs]
-
[Full GC (System.gc()) 65992K->275K(188416K), 0.0038030 secs]
你会发现在Full GC中居然回收了。这就是因为虽然64mb的Slot已经离开了作用域但是Slot没有被覆盖,所以GCROOT中的局部变量表依然保存着holder的关联。所以我们有一个置空的说法,即holder = null;
在我们生成类变量的时候,会先在准备阶段赋值一次,在初始化的时候第二次赋值所以类变量直接int a而不赋值是可行的。
如果是局部变量不赋值,结果如下:

局部变量不赋值会报错。
操作数栈:
操作数栈是一个标准的栈,遵循后入先出的原则,和上一个参数,局部变量表一样,它的栈深也需要存在Code属性的max_stacks数据相中,操作数栈的元素可以是任意的Java数据类型。我们举个例子a++;在栈中是这样操作的。

动态链接:
链接分为两种,静态链接和动态链接,在编译期可知,运行期不变的属于静态链接,主要有static方法,私有方法,实例构造器和父类方法。动态链接因为不能确定具体调用的方法地址,所以往往要到执行时才会转换成内存,关于动态链接的具体内容因为篇幅问题我们之后会单独讲。
方法返回地址:
一个方法的结束只有两个情况,一个是return了(或执行完毕),另一种情况是遇到异常了,还throw出去了。一个方法结束之后肯定要返回之前调用方法的位置,所以我们需要记录一些信息,来存储调用位置的执行状态。一般情况下本方法执行完毕后,恢复调用方法的栈帧,并且压入返回值到操作数栈(如果有的话),最后把PC+1.
[jvm解析系列][十二]分派,重载和重写,查看字节码带你深入了解分派的过程。
重载和重写是分派中的两个重要体现,也是因为这个原因我们才把重载和重写写在了标题上。这一章我们的很多部分都在代码试验上。
总的来说分派分为静态分派和动态分派两种。
静态分派:
首先我们来看一段源码:
-
public class Dispatch {
-
public static void main(String[] args){
-
Animal a = new Dog();
-
Animal b = new Cat();
-
sound(a);
-
sound(b);
-
}
-
public static void sound(Animal a){
-
System.out.println("....");
-
}
-
public static void sound(Dog a){
-
System.out.println("wang..");
-
}
-
public static void sound(Cat a){
-
System.out.println("miao..");
-
}
-
static class Animal{
-
-
}
-
static class Dog extends Animal{
-
-
}
-
static class Cat extends Animal{
-
-
}
-
}
这段源码就是写了三个重载的方法,根据参数的不同输出不同的信息。应该大家都能知道正确输出,输出如下:
-
....
-
....
Animal a = new Dog();在这个里面我们把Animal叫做外观类型(静态类型)把Dog叫做实际类型。静态类型是在编译期可知的,但是一个对象的实际类型要在运行期才可知。而jvm在重载的时候通过参数的静态类型作为判定依据。我们来看一下字节码是不是和我们想的一样

看到图中invokestatic的字符引用,我们应该就知道了编译时期确实完成了方法的定位。
而这些需要靠静态类型定位方法的情形称为静态分派,静态分派发生在编译阶段。也就是说在这个方法执行之前仅仅在编译阶段,sound方法就认定了最后要输出一个"...."而不是wang和miao。
动态分派:
我们依然先给一段代码:
-
public class Dispatch {
-
public static void main(String[] args){
-
Animal a = new Dog();
-
Animal b = new Cat();
-
a.sound();
-
b.sound();
-
}
-
static class Animal{
-
public void sound(){
-
System.out.println("......");
-
}
-
}
-
static class Dog extends Animal{
-
public void sound(){
-
System.out.println("wang!");
-
}
-
}
-
static class Cat extends Animal{
-
public void sound(){
-
System.out.println("miao!");
-
}
-
}
-
}
这一段代码的输出结果相信我不用贴大家都明白。
-
wang!
-
miao!
但是,为什么jvm能够找到这一段方法并且执行的呢?
我们来看一下javap的输出信息

图中画红线的两个部分就是编译后的字节码文件了,他们调用的依然是animal的sound方法,看来动态分派不是在编译期完成的。问题就出在了invokevirtual上面了
jvm运行invokevirtual方法有一个过程,大致如下。
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果C与常量中的描述符和简单名称都符合的方法,说明这个方法就是我们找的,只需要进行访问权限校验(看看是不是private之类)。
3、如果不符合描述符或简单名称。则对C的父类进行第二步中的搜索。
4、最终都没有找到抛出AbstractMethodError。
可能第3步比较难懂,我们再来仔细说明一下
如果我们把代码修改如下:
-
public class Dispatch {
-
public static void main(String[] args){
-
Animal a = new Dog();
-
Animal b = new Cat();
-
a.sound();
-
b.sound();
-
}
-
static class Animal{
-
public void sound(){
-
System.out.println("......");
-
}
-
}
-
static class Dog extends Animal{
-
public void sound(String a){//修改后的方法
-
System.out.println("wang!");
-
}
-
}
-
static class Cat extends Animal{
-
public void sound(){
-
System.out.println("miao!");
-
}
-
}
-
}
那么很明显上述中的C在Animal a = new Dog();中就是Dog类,Dog类中只有一个sound(string)的方法并不存在一个sound()方法,所以去C的父类中即Animal类中找到sound方法,最终输出如下:
-
......
-
miao!
[jvm解析系列][十三]字节码指令小节,从字节码看JVM的栈解释器执行过程。
众所周知,JVM以前一直采用的是解释执行,但是后来在历代的版本更迭中也加入了编译执行。所以总的来说JVM是包含了解释执行和编译执行。这一部分不属于JVM的范畴了,已经属于编译了,大多数都是进行词法分析之类的,以后有时间会补充。
同时大家都知道现在大体上分为两种指令集架构,第一种就是基于栈的第二种是基于寄存器的,简单点说,基于寄存器的架构速度更快,但是可移植性不强,但是基于栈的指令集架构虽然慢,但是可移植性很强,大家都知道java本身就是依靠可移植性出名的,所以无可争议的使用了栈的指令集架构。(也有例外,dalvik是基于寄存器的)
下面我们详述一下JVM的栈解释器执行过程,在此之前我们先来讲一下字节码的指令含义:
加载和存储:加载和存储指令一般可以把栈帧中的局部变量放到操作数栈中,然后把操作数栈中的变量存回栈帧中。
把局部变量加载到操作数栈中:主要有iload,liload_<n>,lload,(每一个指令前面代表的是它操作的数据类型iload就是int lload就是long,接下来我们去除前面的前缀统一用x代替减少篇幅。)
从操作数栈中存回局部变量表 xstore_<n>,xstore(注意有的后面跟了_<n>这是省略了诸如xstore_1,xstore_2这样的指令,xstore默认为xstore_0,之后统一用xstore_<n>替代)
加载一个常量到操作数栈:xipush,xdc,xconst_<n>
运算指令:执行加减乘除取余等运算
加:xadd
减:xsub
乘:xmul
除:xdiv
取余:xrem
取反:xneg
位移:xshl。xshr
按位或指令:xor
按位与指令:xand
按位异或指令:xxor
比较指令:xcmpl
指令上我们大概就讲这么多,接下来就是我们查看字节码的时刻了。首先写一个方法如下:
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
int a = 2;
-
int b = 1;
-
int c = a+b;
-
System.out.println(c);
-
}
然后我们用javap查看一下字节码

可以看到我们的操作从第01两行来看这事int a =2;的操作,先放置常量2到栈顶然后取出来放到常量表中1的位置。23同样。
而45这两个数字就是把局部变量表上12这两个位置的数加载到操作栈中,然后用6行相加存入常量表3的位置。
为了表明0123和7真的实在存储数字到常量表我们对方法做如下修改:
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
int c = 1+2;
-
System.out.println(c);
-
}
字节码如下

很明显的之前的0123行那种存储的行为没有了,同样我们能看到javac给我们的优化,在第0行把1+2直接变成了3。
[jvm解析系列][十四]动态代理和装饰模式,带你看源码深入理解装饰模式和动态代理的区别。
不知道大家知不知道设计模式中有一种叫做装饰,举一个简单的例子。
一天一个年轻领导小王讲话:咳咳,我们一定要xxx抓紧xxxx学***x的精神!好,今天的会议结束!
然后有一个老领导李同志接过来说:那个我在补充两点,个别同志xxx,一定要注意xxx。好散会。
然后另一天小王同志又在讲话:xxx两手都要抓,xxxx一定要注意。
这个时候老周同志出来了:嗯,小王讲的很好,我还有几点要补充xxxx。
那么很明显,小王同志的讲话方法不是很让人满意,那么老李领导或者老周领导可以接过来继续装修一下。其实这就是装饰模式。我们画张图来理一下关系。

我们根据图来写一下代码
-
public class DecoratorDemo {
-
public static void main(String[] args){
-
new 老李(new 小王()).讲话();
-
}
-
}
-
interface 开会{
-
public void 讲话();
-
};
-
class 小王 implements 开会{
-
public void 讲话(){
-
System.out.println("小王同志在讲话");
-
};
-
}
-
class 补充讲话{
-
开会 meeting;
-
public 补充讲话(开会 meeting){
-
this.meeting = meeting;
-
}
-
public void 讲话(){
-
meeting.讲话();
-
}
-
-
}
-
class 老李 extends 补充讲话{
-
public 老李(开会 meeting) {
-
super(meeting);
-
// TODO Auto-generated constructor stub
-
}
-
-
public void 讲话(){
-
super.讲话();
-
System.out.println("老李同志补充讲话");
-
};
-
}
-
class 老周 extends 补充讲话{
-
public 老周(开会 meeting) {
-
super(meeting);
-
// TODO Auto-generated constructor stub
-
}
-
-
public void 讲话(){
-
super.讲话();
-
System.out.println("老周同志补充讲话");
-
};
-
}
在调用老李补充的时候就会输出:
-
小王同志在讲话
-
老李同志补充讲话
但是这种方法虽然做到了补充功能但是整个过程都是静态编译好的。在运行时期也很难修改。
我们接下来看一看动态代理:
同样的例子,我们来实现一下代码。-
public class DynamicProxyDemo {
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
开会 meeting = (开会) new 代理().bind(new 小王());
-
meeting.讲话();
-
}
-
-
}
-
interface 开会{
-
public void 讲话();
-
}
-
class 小王 implements 开会{
-
public void 讲话(){
-
System.out.println("小王同志在讲话");
-
};
-
}
-
class 代理 implements InvocationHandler{
-
Object obj ;
-
public Object bind(Object obj) {
-
this.obj = obj;
-
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
-
}
-
-
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-
// TODO Auto-generated method stub
-
System.out.println("我要补充两句。。。");
-
return method.invoke(obj, args);
-
}
-
-
}
输出:
-
我要补充两句。。。
-
小王同志在讲话
显而易见的是动态代理实现了动态的编程,在原始类和接口都没有知道的时候就可以确定代理类需要做什么。比如说我们把小王讲话换成小张跳舞。其他的都不用变,最后结果还是一样的。
代码
-
public class DynamicProxyDemo {
-
-
public static void main(String[] args) {
-
// TODO Auto-generated method stub
-
舞厅 meeting = (舞厅) new 代理().bind(new 小张());
-
meeting.跳舞();
-
}
-
-
}
-
interface 舞厅{
-
public void 跳舞();
-
}
-
class 小张 implements 舞厅{
-
public void 跳舞(){
-
System.out.println("小张同志在跳舞");
-
};
-
}
-
class 代理 implements InvocationHandler{
-
Object obj ;
-
public Object bind(Object obj) {
-
this.obj = obj;
-
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
-
}
-
-
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-
// TODO Auto-generated method stub
-
System.out.println("我要补充两句。。。");
-
return method.invoke(obj, args);
-
}
-
-
}
输出:
<p class="p1">我要补充两句。。。</p><p class="p1">小张同志在跳舞</p>可以看出来这位老领导兴致勃勃,怎么都要讲两句,也就是说装饰模式真的是在静态的状态上补充和加强原来的代码。但是动态代理更像是一个恶霸,不管你干什么只要过来我这一定要输出我想输出的东西。
那么他是怎么实现的呢?
看我们动态代理的源码。很有意思我们唯一疑惑的地方应该是他:
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);他到底返回了什么?我们跟进去源码看看:
我把几个比较重要的步骤截取下来:
-
final Class<?>[] intfs = interfaces.clone();//得到接口
-
Class<?> cl = getProxyClass0(loader, intfs);//生成一个代理类
-
final Constructor<?> cons = cl.getConstructor(constructorParams);//得到代理类构造器
-
return cons.newInstance(new Object[]{h});//返回一个代理类实例
我们可以通过上面的代码看出来,他根据我们传入的类的信息生成了一个代理类。其实在这个过程中他根据需要的接口信息生成了一个类然后写入了代理的方法,在每一个方法里面都使用了invoke的调用,最后通过这一句话调用到了我们真正想要修饰的方法。return method.invoke(obj, args);(由于我的电脑最近新装的系统,没有反编译装置,没有办法看输出的类,所以通过文字叙述和图片描述)
图片:
-

浙公网安备 33010602011771号