深入理解java虚拟机(9后)

 2019-12-22 10:41  阅读(703)
文章分类:JVM

第九章 垃圾回收

内存回收,处理堆碎块。java程序员不可能错误地释放内存。

垃圾收集算法

垃圾检测通常通过建立一个根对象的集合并且检查从这些根对象开始的可触及性来实现。
如果正在执行的程序可以访问到的根对象和某个对象之间存在引用路径,这个对象就是可触及的。对程序来说,根对象总是可以访问的。
java虚拟机根对象集合根据实现不同而不同,但总是会包含局部变量中的对象引用和栈帧的操作数栈。另外一个根对象的来源是被加载的类的常量池中的对象引用,比如字符串。还有一个来源是传递到本地方法中的,没有被本地方法“释放”的对象引用。再一个潜在的根对象来源就是,java虚拟机运行时数据区中从垃圾收集器的堆中分配的部分。

引用计数收集器

堆中每一个对象都有一个引用计数,当对象被创建,并且指向该对象的引用被分配给一个变量。这个对象的引用计数为1。当任何其他变量被赋值为这个对象的引用时,计数加1。当一个对象的引用过过了生存期或者被设置一个新的值时,对象的引用技术减1,为0的时候被当做垃圾收集。

跟踪收集器

跟踪收集器从根节点开始的对象引用图。在追踪过程中遇到的对象以某种方式打上标记,总的来说,要么对象本身设置标记,要么用一个独立的位图来设置标记,当追踪结束时,未标记的对象就知道是无法初级的,从而可以被收集。

压缩搜集器

对付堆碎块的策略。压缩收集器把活动的对象越过空闲区滑动到堆的一端,在这个过程中,堆的另一端出现了一个大的连续空闲区,所有被移动的对象的引用也被更新,指向新的位置。
更新被移动的对象的引用有时候通过一个简接对象引用层可以变得更简单。不直接引用堆中的对象,对象的引用实际只需一个对象句柄表。对象句柄才指向对中对象的实际位置。

拷贝收集器

拷贝垃圾收集器把所有的活动对象移动到一个新的区域。在拷贝过程中,他们被紧挨着布置,所以可以消除原本他们所在旧区域的空隙。

按代收集的收集器

把对象按照寿命分钟解决效率低下的问题,更多的收集那些短暂出现的年幼对象,而非寿命较长的对象。

自适应收集器

自适应

火车算法

垃圾收集器一般都会停止整个程序运行的运行来查找和手机垃圾对象,它们可能在程序执行的任意时刻暂停,并且暂停的时间也无法确定。
通常渐进式先收集器都是按代收集的收集器。
火车算法把成熟的对象空间划分为固定长度的内存块,算法每次在一个块中单独执行。每一块属于一个集合,在一个集合内的块排了序,这些集合本身也排了序。
你可以想象新的对象可能有两种方法到达火车站,要么被打包成车厢,挂接到除了号码最小之外的火车的尾部,要么作为一列新的火车开进火车站。

记忆集合和流行对象

为了促进收集过程,火车算法使用了记忆集合,一个记忆集合是一个数据结构,它包含了所有对一节车厢或者一列火车的外部使用。算法为成熟对象空间内每节车厢和每列火车都维护一个记忆集合。所以一节特定车厢的记忆集合包含了指向车厢内对象的所有引用的集合。一个空的记忆集合显示车厢或者火车中的对象都不再被车厢或者火车外的任何变量引用,被遗忘的对象就是不可触及的,可以被垃圾收集。

对象可触及性的声明周期

三种状态:可触及的,可复活的,以及不可触及的。

public class LizeTest {

        private static LizeTest lizetest;

        public static void main(String[] args) throws InterruptedException {
            lizetest = new LizeTest();
            lizetest = null;
            System.gc();
            Thread.sleep(3000);
            if (lizetest == null) {
                System.out.println("die");
            } else {
                System.out.println("live");
            }
            lizetest = null;
            System.gc();
            Thread.sleep(3000);
            if (lizetest == null) {
                System.out.println("die");
            } else {
                System.out.println("live");
            }
        }

        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize--");
            // TODO Auto-generated method stub
            super.finalize();
            lizetest = this;
        }
    }
    //finalize--
    //live
    //die
引用对象

强引用,软引用,弱引用,影子引用。除了强引用都可被垃圾收集。
要创建一个软引用、弱引用或者影子引用,简单地把强引用传递到对应的引用对象的构造方法中去。biubiu要创建一个对某个Cow对象的软引用,就把一个指向Cow对象的强引用传递到一个新的SoftReference对象的构造方法中。通过维护对这个SoftReference对象的强引用,也维护了对这个Cow对象的软引用。

第十章 栈和局部变量操作

常量入栈操作

操作码在执行常量入栈操作前,使用如下三种方式指明常量值:常量值隐式包含在操作码内部,常量值在字节码流中如操作数一样紧随在操作码之后,或者从常量池中取出常量。
如果多个类使用了同一字符串文字,比如“Harumoh!”,java虚拟机将只会创建一个具有”Harumoh!”值对象来表示所有的字符串文字。

通用栈操作
把局部变量压入栈
弹出栈顶部元素,将其赋给局部变量
wide指令

第十一章类型转换

转换操作码

执行转换工作的操作码后面没有操作数,转换的值从栈顶端获得,java虚拟机从栈顶端弹出一个值,对它进行转换,然后再把转换结果压入栈。
任何byte,short和char类型值在压入栈的时候,就已经有效地被转换成int类型的值了。
涉及byte,short和char类型的运算操作首先会把这些值转换成int类型,然后对int类型值进行运算,最后得到int类型的结果。因此,如果把两个byte类型值相加,最后会得到一个int类型的结果。

byte a = 1;
     byte b = 2;
     byte c = a+b;//会报错,默认int

第十二章整数运算

二进制补码运算

java虚拟机所支持的所有的整数类型–byte(2^3)、short(2^16)、int(2^32)、long(2^64)。他们都是带符号的二进制补码数。

第十三章 逻辑运算

逻辑操作码

<<,>>,>>>,&,|,^

第十四章 浮点运算

浮点数

由符号、尾数、基数和指数四部分组成。
2019120001211\_1.png

第十五章 对象和数组

在java虚拟机中,内存只能以对象形式在垃圾收集堆中分配。除非作为对象的一部分,否则不能为基本类型在堆中非配内存。
数组对象本身通常包括基本类型数组或者对象引用数组。如果声明了对象数组,获得的将是对象引用的数组。对象本身必须通过new操作显示创建,并赋给数组成员。

第十六章 控制流

if,if-else,while,do-while,for,switch
除了switch语句外,java编译器使用同样的操作码集。例如,java提供的最简单的控制流语句是if语句。当编译一个java程序时,针对if语句表达式的不同行为,if语句能够被转换成任意一组操作码。从栈中弹出一个值与0比较,从栈中弹出两个值的操作码对这两个值比较。
值比较对基本类型(long,float,double)进行比较操作。这些操作码本身并不会执行分支操作,而是把代表比较结果的int类型值(0表示相等,1表示大于,-1表示小于)压入栈,然后使用一种前面已经介绍过的对int类型进行比较的操作码进行实际分支跳转。
与null比较
对象间比较,是否指向堆中同一对象
switch tableswitch和lookupswitch指令都包含一个默认的分支偏移量和一组可变长度的case值/分支偏移量 对。这两条指令都会将键值从栈中弹出。他们会把键值与所有case值进行比较。如果发现匹配项,则取与该case值相关的程序分支偏移量,若未发现匹配项,则取默认程序分支偏移量。

第十七章 异常

异常的抛出与捕获

份两部分 ,第一部分正常执行路径,第二部分catch子句。

异常表

每一个被try语句块捕获的异常都与异常表中的一个入口(项)相对应。异常表中的每个入口都包含四部分信息:
起点,终点,将要跳转到字节码序列中的pc指针偏移量,被捕获的异常的常量池索引。
2019120001211\_2.png
上述异常表说明,ArithmeicException异常在pc指针偏移量19到22(含)被捕获try语句块的终点在to栏中列出。这个终点值总是比捕获异常位置的pc指针偏移量的最大值还要大1。在这个例子中,终点值为23,但捕获异常位置的pc偏移量最大值为22。偏移量19到22(含)之间的语句对应实现方法内部try语句块代码的字节码序列。上述表格中列出的target栏指出了如果AnthmeticException异常在pc指针偏移量19到22之间抛出,pc指针偏移量将要跳转到的位置。

第十八章 finally子句

微型子例程

字节码中的finally子句在方法内部的表现很像“微型子例程”。java虚拟机在每个try语句块和与其相关的catch子句的结尾处都会“调用”finally子句的子例程。finally子句结束后(这里的结束指的是finally子句中最后一条语句正常执行完毕,不包括抛出异常,或执行return、continuc、break等情况),隶属于这个finally子句的微型子例程执行“返回”操作。程序在第一次调用微型子例程的地方继续执行后面的语句。
jsr指令是使java虚拟机跳转到微型子例程的操作码。当java虚拟机遇到jsr或者jsr_w指令,它会把返回地址压入栈,然后从微型子例程的开始处继续执行。返回地址是紧接在jsr操作码和操作数后字节码的地址。该地址的类型为returnAddress。
微型子例程执行完毕后,将调用ret指令,ret指令的功能是执行从子例程中返回的操作。ret指令只有一个操作数,这个操作数是一个存储返回地址的额局部变量的索引。
java方法与微型子例程使用不同的指令集。
实现finally子句的字节码被称为“微型子例程”,因为它们在一个方法的字节码流中的表现如同一个小子例程一样。

不对称的调用和返回

在每一个子例程的开始处,返回地址都是从栈顶弹出,并且存储在局部变量中,稍后,ret指令将会从这个局部变量中取出返回地址。这种返回地址得到不对称的工作方式是必要的,因为finally子句本身会抛出异常或者含有return、break、continue等语句。由于这些可能性的存在,这个被jsr指令压入栈的额外返回地址必须立即从栈中移除,因此,当finally子句通过break、continue、return或者抛出异常退出时,这个问题就不必再考虑了。

方法调用和返回

方法调用

java程序设计语言提供了两种基本的方法:实例方法和静态方法。区别在于:
1)实例方法在被调用之前,需要一个实例,而类方法不需要。
2)实例方法使用动态(迟)绑定,而静态方法使用静态(早)绑定
当java虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。
对于实例方法,使用invokevirtual指令,对于类方法,使用invokestatic指令
先进行解析,再准备调用。如果这个方法是一个实例方法,它必须在一个对象中被调用。对每一次实例方法的调用,虚拟机需要在栈里存在一个对象引用(objectref)。如果该方法需要参数,那么除了objectref,虚拟机还需要在栈中存 在该方法所需要的参数(args)。如果这个方法时一个类方法。虚拟机不在需要objecttref,因为虚拟机不会在对象上调用一个类方法,栈中存在的将只有args。
虚拟机为每一个调用java(非本地)方法建立一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间,该方法的操作数栈,以及特定虚拟机实现需要的其他所有的信息。局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中,然后虚拟机就能够了解到方法的栈帧需要多少内存。当虚拟机调用一个方法的时候,它为该方法创建恰当大小的栈帧,再讲新的栈帧压入java栈。
当调用本地方法时,虚拟机不会将一个新的栈帧压入java栈。当线程进入到本地方法的那一刻,他就将java栈抛在身后。直到本地方法返回以后,java栈才被重新使用。

方法调用的其他形式

1)实例初始化(init)方法
2)私有方法
3)使用super关键字所调用的方法

指令invokespecial

类会为源文件中每个构造方法提供一个()方法。如果没有显式声明一个构造方法。编译器就会为这个类产生一个默认的无参构造方法。这些方法通常使用invokespecial调用。
原因。子类的init方法需要拥有调用超类的init方法的能力。这个过程贯穿于对象的整个生命周期。
类的init方法拥有相同的特征签名是很普遍的线象。(方法的特征签名是指它的名字、参数的数量和类型)
当处理私有实例方法时,必须允许子类使用与超类中实例方法同样的特征签名来声明实例方法。
当使用super关键字来调用方法时,尽管当前类重载了该方法,但使用者真正希望调用的是超类的方法。指令invokecvirtual只能调用当前类的方法,无法使用超类的方法。
如果创建了一颗包含3个类的继承树(Animal,Dog,CockerSpaniel)假设类Dog是Animal类的子类,CookerSpaniel类时Dog类的子类,在CockerSpaniel类中定义一个方法,它使用invokespercial来调用一个名为walk()的非私有的超类方法,再假设当编译CokerSpaniel时,编译器设定了ACC_SUPER标志。此外,假设当编译CokerSpaniel类时,方法的符号引用将会把Animal类作为它的类。当执行CokerSpaniel类中的invokespecial指令时,虚拟机会进行动态选择,并调用Animal类的walk方法。

指令invokeinterface

与invokespecial功能相同:它调用实例方法并使用动态绑定,这两条指令的区别在于:当引用的类型为类的时候,使用invokevirtual:当引用类型为接口时,使用invokeinterface。

指令调用和速度

调用 接口引用方法可能要比调用类引用方法慢。当java虚拟机遇到invokevirtual指令时,它把实例方法的符号引用解析为直接引用,所生产的直接引用很可能是方法表中的一个偏移量,而且从此往后都可以使用同样的偏移量,但对于invokeinterface指令来说,虚拟机每一次遇到invokeinterface指令,都不得不重新搜寻一遍方法表,因为虚拟机不能够假设折翼的偏移量与上一次的偏移量相同。
最快的指令时invokespecial和invokestatic,因为这些指令调用的都是静态方法,当java虚拟机为这些指令解析符号引用时,将符号引用转换成直接引用,所生成的直接引用将包含一个指向实际操作码的指针。

第20章 线程同步

监视器

java监视器支持两种线程:互斥和协作。java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而互不干扰地工作。协作则是通过Object类的wait方法和notify方法来实现,允许多个线程为同一个目标而共同工作。
我们可以将监视器比作一个建筑,它有一个特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据。一个线程从进入这个房间到它离开前,它可以独占地访问房间中的全部数据。我们用一些术语来定义这一系列动作,进入这个建筑叫做“进入监视器”,进行建筑中那个特别的房间叫做”获得监视器”,占据房间叫做“持有监视器”离开房间叫做”释放监视器”,离开建筑叫做“退出监视器”。
另一种我们提到得到被监视器所支持的同步是协作。互斥帮助线程在访问共享数据时不被其他线程干扰,而协作帮助线程与其他线程共同工作。
当一个线程需要一些特别状态的数据,而由另一个线程负责改变这些数据的状态时,同步就显得特别重要。举个例子,一个“读线程”会从缓冲区中读数据,而另一个”写线程”会向缓冲区填充数据。读线程需要缓冲区处于一个非空的状态,这样它们才可以从中读取数据,如果读线程发现缓冲区是空的,它就必须等待。写线程就负责向缓冲区中写入数据,只有写线程完成一些数据的写入,读线程才能做相应的读取操作。
java虚拟机所使用的这种监视器被称作“等待唤醒”监视器(“发信号并继续”监视器)在这种监视器中,一个已经持有监视器的线程,可以通过执行一个等待指令,暂停自身的执行。当线程执行了等待命令后,它就会释放监视器,并进入一个等待区,这个线程会在那里一直持续暂停状态,直到一段时间后,这个监视器中的其他线程执行了唤醒命令。当一个线程执行了唤醒命令后,它会继续持有监视器,直到它主动释放监视器,如执行了一个等待命令或者执行完监视区域。当执行唤醒的线程释放了监视器后,等待线程会苏醒,并重新获得监视器。
java虚拟机中的这种监视器有时也被称作“发信号并继续”监视器的原因是,在一个线程做了唤醒操作(发信号)后,它还会继续持有监视器并继续执行区域(继续),过了一些时间之后,唤醒线程释放监视器,等待线程才会苏醒。
在java虚拟机中,线程在执行等待命令时可以随意指定一个暂停时间,如果一个线程指定了暂停时间,而且在暂停时间截止之前没有其他线程执行唤醒命令买这个等待线程会从虚拟机得到一个自动唤醒命令。
java虚拟机提供了两种唤醒命令:“notify”和”notify all”。 notify命令随意从等待区域中选择一个线程并将其标志为“可能苏醒”,而notify all命令将会将等待区域中的所有线程都标志成“可能苏醒”。

对象锁

java虚拟机的一些运行时数据被所有的线程共享,其他的数据是各个线程私有的,因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问数据进行协调:
保存在堆中的实例变量。
保存在方法区中的类变量。
java栈中的数据是属于该栈的线程私有的,不需要协调

指令集中对同步的支持

要建立一个同步语句,在一个计算对象引用的表达式中加上synchronized关键字就可以了,例:
syncronized(this){ …}
正常退出和异常退出一样,虚拟机会自动释放这个对象的锁。

总结:

先了解了java的双亲委托机制,当某个类装载器试图装载该类型时会委托给双亲,双亲再委托给双亲..直到启动类加载器,如果启动类加载器无法加载再自行加载。
java虚拟机分为方法区,堆,java栈,pc寄存器,本地方法寄存器,
方法区和堆是所有线程共享的,java栈线程独享的。
方法区:常量池(所有常量的有序集合),字段信息,方法信息,静态变量。
堆:创建的所有类实例或者数组都放在同一个堆中。
程序计数器:每个线程都有自己的pc寄存器,能够持有一个本地指针,或者returnAddress,pc寄存器的内容总是下一跳准备执行指令的地址。
java栈:每个线程都有,以帧为单位保存线程的运行状态,执行两种操作:压栈和出栈。

类装载器按照:
1装载-查找并装载二进制数据。
2连接-执行验证,准备、以及解析
验证 确保被导入类型的正确性。
准备 为类变量分配内存,并将其初始化为默认值
解析 把类变量中的符号引用转换为直接引用
3初始化-把类变量初始化为正确初始值。
类的声明周期 :装载,连接和初始化,对象实例化,垃圾收集,对象终结。
过程:启动类装载器把二进制文件装载,装载中装载它的超类并连接初始化它的超类,连接,第一步校验,分配内存赋值(定义static和final不需要初始值),可以被解析(但不是必须),初始化。
垃圾回收:按代回收,新生代回收多,老生代回收少。
类型转换 byte,short,char 入栈会转成int。
异常:异常表
finally子句:执行finally时会把返回地址压入栈,在执行微型子例程,返回地址是栈顶弹出,在局部变量复制。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解java虚拟机(9后)

相关推荐