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

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

21.虚拟机字节码执行引擎
执行引擎是Java虚拟机最核心的组成部分之一。所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。解析字节码过程的重点是虚拟机的方法调用和字节码执行。

22.运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。在下图的栈帧概念结构中,活动的线程只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎的所有字节码指令都只针对当前栈帧进行操作。

![2019120001670_1.png][2019120001670_1.png]

线帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。看看各部分的内容:
1).局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量;
2).操作数栈
也称为操作栈,它是一个后入先出的栈。当一个方法刚刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作,而Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的“栈”指的就是操作数栈;
3).动态连接
每个线帧都包含一个指向运行时常量池中该线帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;
4).方法返回地址
当一个方法开始执行后,只有两种方法可以退出这个方法:正常完成出口和异常完成出口。无论采用何种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行,而这个位置可以理解为返回地址(一般是Pc记数器的值);
下图是一个简单的1+1方法:
两个1入栈后,会出栈相加产生的值2会再入栈,

![2019120001670_2.png][2019120001670_2.png]

23.虚拟机是如何调用方法的呢?
涉及的指令:
Java虚拟机提供了5条指令:
invokestatic:调用静态方法;
invokespecial:调用实例构造器方法、私有和父类方法;
invokevirtual:调用所有虚方法;
invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象;
invokedynamic:分派逻辑是用户所设定的引导方法决定的;

具体的调用方式:
1).解析调用
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,调用目标在程序写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。例如静态方法、私有方法、实例构造器、父类方法4类。
2).分派调用
解析调用是一个静态的过程,在编译期就完全确定,不会延迟到运行期再去完成,而分派调用则可能是静态的也可能是动态的。根据分派的宗量数分为单分派和多分派,这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。
(1)静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载,例如:

public class StaticDispatch{

        static abstract class Human {
        }

        static class Man extends Human{
        }

        static class Woman extends Human{
        }

        public void sayHello(Human guy) {
            System.out.println("hello, guy!");
        }

        public void sayHello(Man guy) {
            System.out.println("hello, gentleman!");
        }

        public void sayHello(Woman guy) {
            System.out.println("hello, lady!");
        }

        public static void main(String[] args) {
            Human man= new Man();
            Human woman = new Woman();
            StaticDispatch sr = new StaticDispatch();
            sr.sayHello(man);
            sr.sayHello(woman);
        }
    }/*Output: hello, guy! hello, guy! *///:~

在:Human man= new Man(); 中,Human称为变量的静态类型,为Man称为变量的实际类型,静态类型是在编译期可知的,而实际类型在运行期才可确定。但编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的(因为静态类型是编译期可知的),因此,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
(2)动态分派
它与多态的重写有着很密切的关联:

public class DynamicDispatch {

        static abstract class Human {
            protected abstract void sayHello();
        }

        static class Man extends Human {
            @Override
            protected void sayHello() {
                System.out.println("man say hello.");
            }
        }

        static class Woman extends Human {
            @Override
            protected void sayHello() {
                System.out.println("woman say hello.");
            }
        }

        public static void main(String[] args){
            Human man = new Man();
            Human woman = new Woman();
            man.sayHello();
            woman.sayHello();
        }
    }/*Output: man say hello. woman say hello. *///:~

在主函数中,静态类型都是Human的两个变量man和woman在调用sayHello() 方法时执行了不同的行为,如果用javap打印出该类的字节码,会发现在调用方法时使用了invokevirtual指令,它把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
(3)单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。截止Jdk1.7,编译阶段编译器的选择过程就是静态分派的过程,Java语言的静态分派属于多分派类型。运行阶段虚拟机的选择过程其实是动态分派的过程,Java语言的动态分派属于单分派类型。

24.潮流中的动态类型语言支持
动态类型语言的关键特征是它的类型检查的主体过程在运行时期而不是编译期,比如js(Javascript):

<html>
    <head>
    <title>Javascript Dynamicpatch</title>
    <script language="javascript"> function classA (str){ document.write (str); } var obj = new classA("Hello world."); </script>
    </head>
    <body>
    </body>
    </html>

![2019120001670_3.png][2019120001670_3.png]
但是在Java中,会有一个小惊喜:

public class DynamicTest{

        static class ClassA{
            public void println(String s){
                 System.out.println(s);                    
            }

        }

        public static void main(String[] args) {
            Object obj = new ClassA();
            obj.println("Hello world.");
        }

    }/*Output: DynamicTest.java:12: 错误: 找不到符号 obj.println("Hello world."); ^ 符号: 方法 println(String) 位置: 类型为Object的变量 obj 1 个错误 *///:~

Java语言在编译期间已将println(String) 方法完整的符号引用生成出来,变量obj的静态类型为java.io.PrintStream,哪怕obj属于一个确实有println(String) 的方法,但与PringStream接口没有继承关系,代码依然不可能运行,因为类型检查不合格。但是,在拥有Method Handle 之后,Java语言也可以实现动态的调用println(String) 方法:

import static java.lang.invoke.MethodHandles.lookup;
    import java.lang.invoke.*;

    public class MethodHandleTest{

        static class ClassA{
            public void println(String s){
                System.out.println(s);
            }
        }

        public static void main(String[] args) throws Throwable{
            Object obj = new ClassA();
            getPrintlnMH(obj).invokeExact("Hello world.");
        }

        private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable{
            MethodType mt = MethodType.methodType(void.class, String.class);
            return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
        }
    }/*Output: Hello world. *///:~

25.基于栈的字节码解释执行引擎
许多Java虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译期产生本地代码执行)两种选择。

![2019120001670_4.png][2019120001670_4.png]

上图中间的分支是解释执行的过程,下面的分支是传统编译原理中程序代码到目标机器代码的生成过程。Java语言中,Javac编译器完成的动作是在Java虚拟机之外进行的,而解释器是在虚拟机的内部,所以Java程序的编译就是半独立的实现。

26.类加载器案例分析之Tomcat
做JavaEE开发的人应该对Tomcat很熟悉,做为主流的Java web 服务器,Tomcat 必须要满足web 服务器的要求:
1).部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离;
2).部署在同一个服务器上的两个Web应用程序所使用的Java 类库可以相互共享;
3).服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响;
4).支持JSP 应用的Web服务器,大多数都支持HotSwap功能;
在Tomcat 5.0 版本中,有4组目录:/common/* 、/server/*、 /shared/*和Web应用程序自身的目录/WEB-INF/*,含义分别如下:
1)./common:类库可被Tomcat和所有的Web应用程序共同使用;
2)./server:类库可被Tomcat使用,对所有的Web应用程序都不可见;
3)./shared:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见;
4). WebApp/WEB-INF:类库仅仅可以被Web应用程序使用,对Tomcat和其他Web应用程序都不可见;

![2019120001670_5.png][2019120001670_5.png]

27.动态代理的实现
在 thinking in Java 中有一句话:“代理(Proxy) 是怎么面向对象最成功的”,我的直觉理解是组合。动态是指可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。下面的例子中,原始类是打印”hello word”,代理类在原始类方法执行之前打印一句”Welcome“:

import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    public class DynamicProxyTest{

        interface IHello {
            void sayHello();
        }

        static class Hello implements IHello {
            @Override
            public void sayHello() {
                System.out.println("hello world.");
            }
        }

        static class DynamicProxy implements InvocationHandler {

            Object originalObj;

            Object bind(Object originalObj) {
                this.originalObj = originalObj;
                return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),
                        originalObj.getClass().getInterfaces(), this);
            }

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("Welcome");
                return method.invoke(originalObj, args);
            }
        }

        public static void main(String[] args) {
            IHello hello = (IHello) new DynamicProxy().bind(new Hello());
            hello.sayHello();
        }
    }/*Output: Welcome hello world. *///:~

27.前端编译器—Javac
对于Javac,相信很多人都不陌生,我第一次接触Java时,敲一个Hello World 程序,用的就是Javac去编译的,把.java 文件转变为.class文件。Sun公司研发的Javac编译器是用Java语言编写的,这让Java程序员有种归属感。从Sun Javac 的代码来看,编译过程大致可以分为3个过程:
1).解析与填充符号表过程
(1)解析

<1>词法分析 将源代码的字符流转变为标记(Token)集合,单个字符串是程序编写过程的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为标记。 <2>语法分析 根据Token序列构造抽象语法树,抽象语法树(Abstract Syntax Tree, AST)是一种用来描述程序代码语法结构的树形表示法,语法树的每一个节点都代表着程序代码中的一个语法结构,包、修饰符、运算符、接口、返回值等都可以是一个语法结构。 (2)填充符号表: 符号表是一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到。 2).插入式注解处理器的注解处理过程 插入式注解处理器可以看作是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round,构成回环。 3).分析与字节码生成过程 (1)语义分析 <1>标注检查 标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。 <2>数据及控制流分析 对程序上下文逻辑更进一步验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值等。 <3>解语法糖 将泛型、变长参数、自动装箱/拆箱等还原到简单的基础语法结构。 (2)字节码生成 不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。 ![2019120001670\_6.png][2019120001670_6.png] 28.Java 语法糖的味道 语法糖(Syntactic Sugar),也叫糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是能够增加程序的可读性,减少代码出错的机会。 1).泛型与类型擦除 泛型的本质是参数化(Parametersized Type)的应用,也就是说操作的数据类型被指定为一个参数。C\#、C++的数据类型在编译期是切实存在的,是基于类型膨胀的真实泛型。而Java的数据类型只在源码出现过,到了编译期就被类型擦除变成了裸类型,是基于类型擦除的伪泛型。 2).自动装箱、拆箱与遍历循环 举个例子,源码中的int\[1,2,3\]在编译时,会自动装箱成Integer.valueOf(1),… 3).条件编译 根据boolean值的真假,编译器将会把分支中不成立的代码块消除掉 … 语法糖还包括内部类、枚举类、断言语句… 29.后端运行期编译器—即时编译器(JIT) HotSpot虚拟机是解释器和编译器并存的,当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会采用分层编译的策略。 ![2019120001670\_7.png][2019120001670_7.png] HotSpot内置了两个即时编译器,分别称谓Client Compiler 和 Server Compiler.。目前主流的HotSpot虚拟机中,默认采用解释器与其中一个编译器直接配合的方式,程序使用哪个编译器,取决于虚拟机运行模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用参数强制虚拟机运行哪个模式。 即时编译器主要针对的编译对象是“热点代码”,包括被多次调用的方法和被多次执行的循环体,判断一段代码是不是热点代码通过热点探测(Hot Spot Detection),有基于采样的热点探测和基于记数器的热点探测(目前被HotSpot虚拟机采用)。 即时编译器的编译过程(这里以Client Compiler为例),Client Compiler是一个典型的三段式编译器: 1).第一阶段 一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representation , HIR)。主要完成一部分的基础优化,比如方法内联、常量传播等; 2).第二阶段 一个平台相关的后端从HIR中产生地级中间代码表示(Low-Level Intermediate Representation,LIR),完成空值检查消除、范围检查消除等; 3).最后阶段 在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器; ![2019120001670\_8.png][2019120001670_8.png] 29.逃逸分析–最高大上的编译优化技术,没有之一 一般来讲,即时编译器产生的本地代码会比Javac产生的字节码更加优秀。HotSpot虚拟机的及时编译器在生成代码时采用的代码优化技术太多了。这里就看看逃逸分析。 逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其它优化手段提供依据的分析技术。逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义之后,它可能被外部方法所引用(方法逃逸)或被外部线程所访问(线程逃逸),如果确定对象逃逸到方法或线程,就可以采取一些优化措施: 1).栈上分配 把对象分配到栈上,对象所占用的空间就可以随着线帧出栈(方法结束)而销毁,垃圾收集系统的压力将会小很多; 2).同步消除 逃逸分析能确定一个变量不会逃逸出线程,无法被其它线程访问,那这个变量的读写肯定就不会有竟争,对这个变量实施的同步(线程同步是相对耗时的过程)也就可以消除掉; 3).标量替换 把Java对象拆散,程序真正执行的时侯将可能不1创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替; 30.Java内存模型 Java虚拟机是在操作系统之上的又一层抽象管理,统一管理资源和分配内存。那么Java的内存模型是怎样设计的呢?Java的内存模型与计算机的内存模型类似,先看看它们之间的原理图,计算机: ![2019120001670\_9.png][2019120001670_9.png] Java: ![2019120001670\_10.png][2019120001670_10.png] Java内存模型的主要目标是定义程序中的各个变量的访问规则,即在虚拟机中将变量储存到内存和从内存中取出变量这样的底层细节。Java内存模型规定了所有的变量都储存在主内存(Main Memory)中(虚拟机内存的一部分),每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。可以近似的理解主内存主要对应Java堆中对象实例数据部分,而工作内存对应虚拟机栈的部分。 同时,Java内存模型定义了8种原子操作来完成主内存与工作内存之间的交互: 1).lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态; 2).unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定; 3).read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用; 4).load(载入):作用于工作内存的变量 ,它把read操作从主内存中得到的变量值放入工作内存的副本中; 5).use(使用): 作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时执行这个操作; 6).assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作; 7).store(存储): 作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用; 8).write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中; 31.Java内存模型的特征和原则 特征: Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。 1).原子性:就是8种原子操作。还有synchronized关键字; 2).可见性:当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。实现手段有volatile、synchronized和final关键字; 3).有序性:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是有序的。可以通过volatile和synchronized关键字实现; 先行发生原则: Java语言中有一个“先行发生”(happend-before)的原则,它是判断数据是否存在竟争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则解决并发环境下两个操作之间是否可能存在冲突的所有问题。 1).程序次序规则(Program Order Rule):书写在前面的操作先行发生于书写在后面的操作; 2).管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作; 3).volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作; 4).线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作; 5).线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对次线程的终止检测; 6).线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; 7).对象终结规则(Finalize Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始; 8).传递性(Transitivity):如果操作A先行发生与操作B,操作B先行发生与操作C,那么操作A先行发生与操作C; 总之,时间先后顺序与先行发生原则之间基本没有太大的关系,所以在衡量并发安全问题的时侯,一切以先行发生原则为准。 32.Java的线程实现方式和线程调度 线程是比进程更加轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开, 线程实现方式: 各个线程既可以共享进程资源,又可以独立调度(线程是CPU调度的基本单位)。实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和试用用户线程加轻量级进程混合实现。 1).使用内核线程实现 内核线程(Kernel-Level Thread, KLT),就是直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操作调度器(Schedual)对线程进行调度,并负责将线程的任务映射到各个处理器上。不过,程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口—轻量级进程(Light Weight Process, LWP),轻量级进程与内核线程之间是1:1的关系,称为一对一线程模型: ![2019120001670\_11.png][2019120001670_11.png] 2).使用用户线程实现 广义上讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread, UT)。狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。这种进程(Process, P)与用户线程之间1:N的关系称为一对多的线程模型: ![2019120001670\_12.png][2019120001670_12.png] 3).使用用户线程加轻量级进程混合实现 将内核线程与用户线程一起使用的实现方式,用户线程完全建立在用户空间中,用户线程的系统调用要通过轻量级进程来完成。在混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系: ![2019120001670\_13.png][2019120001670_13.png] 线程调度: 线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。 总而言之,由于Java虚拟机的线程映射很大程度上受操作系统的影响,在Windows和Linux下,Sun JDK采用一对一的线程模型和抢占式的线程调度方式。 33.Java线程安全 什么是线程安全?Java界有一本著作《Java Concurrency In Practice》给出了解释:”当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的“。 从编程语言的角度上看,线程安全其实就是资源数据共享竟争的问题。Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。 线程安全的实现方法有3种: 1).互斥同步(也叫阻塞同步) 互斥同步 (Mutual Exclusion & Synchronization)最常见的一种并发正确性保障手段,同步是指多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个/一些线程使用,而互斥是实现同步的一种手段,互斥是方法,同步是目的。虽然简单高效,但是当阻塞或唤醒一个线程,都需要操作系统帮忙完成,这就需要从用户态转换到核心态中,消耗很多的处理器时间,造成性能下降; 2).非阻塞同步 基于冲突检测的乐观并发策略。就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果有共享数据争用,那就再采取其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此称为非阻塞同步(Non-Blocking Synchronized); 3).无同步方案 如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。所以,有些代码天生就是安全的,比如可重入代码(Reentrant Code)和线程本地代码(Thread Local Storage),最典型的架构是”生产-消费“模式; 为了更好的提高Java的高效并发,锁优化技术是一项重要的保证手段,在JDK的各个版本中,HotSpot虚拟机做了大量的锁优化,包括适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。虽然这些优化技术是虚拟机帮我们做的,对用户是屏蔽的,但是理解虚拟机的锁优化能帮助我们写出更高伸缩性的并发程序。 34.Java虚拟机(HotSpot)的全貌 ![2019120001670\_14.png][2019120001670_14.png] [2019120001670_1.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_1.png [2019120001670_2.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_2.png [2019120001670_3.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_3.png [2019120001670_4.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_4.png [2019120001670_5.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_5.png [2019120001670_6.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_6.png [2019120001670_7.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_7.png [2019120001670_8.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_8.png [2019120001670_9.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_9.png [2019120001670_10.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_10.png [2019120001670_11.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_11.png [2019120001670_12.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_12.png [2019120001670_13.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_13.png [2019120001670_14.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/jvm/2019120001670_14.png

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

相关推荐