深入理解Java虚拟机电子版完整免费版|百度网盘下载

编者点评:深入理解Java虚拟机电子版

深入了解Java虚拟机电子版,全书分为五个部分,分别介绍Java的技术体系、开发流程、虚拟机家族、手工编译JDK。了解这部分可以为学习JVM提供一个很好的指导,里面还有更多精彩内容,有兴趣的请下载

简介

《理解Java虚拟机:JVM高级特性和最佳实践》简介:作为一名Java程序员,你有没有想过深入了解Java虚拟机,却被它的复杂性和深奥性拒之门外?没关系,这本书尽量简单,

它可以让你轻松领略Java虚拟机的奥秘。本书是近年来国内唯一一本与Java虚拟机相关的专着,也是唯一一本同时从核心理论和实际应用两个角度讨论Java虚拟机的书籍。

不仅理论分析透彻,书中包含的典型案例和最佳实践也具有重要的现实意义。

本书分为五个部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立编译一个OpenJDK7,对理解后面的内容很有帮助。第二部分讲解JVM的自动内存管理,

包括虚拟机内存区域的划分原则以及各种内存溢出异常的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常用虚拟机监控调试工具的原理和使用方法。

第三部分分析虚拟机的执行子系统,包括类的文件结构以及如何在类中存储和访问数据;虚拟机的类创建机制和类加载器的工作原理及其对虚拟机的意义;

虚拟机字节码的执行引擎和它在执行代码时涉及的内存结构。第四部分讲解程序的编译和代码的优化,阐述泛型、自动装箱拆箱、条件编译等语法糖的原理;

讲解了虚拟机的热点检测方法,HotSpot的即时编译器,编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分讨论Java中高效并发的原理,

包括JVM内存模型的结构和操作; Java内存模型中原子性、可见性和有序性的实现;首次出现原则的规则和使用; Java语言中线程的实现原理;虚拟机的高效实现 并发做出的一系列锁优化措施。

各章内容介绍

第 1 部分(第 1 章)接近 Java

系统介绍了Java的技术体系、发展历程、虚拟机家族、JDK的动手编译。了解这部分可以为学习JVM提供很好的指导。

第二部分(第 2~5 章)自动内存管理

介绍Java的内存区域和内存溢出、垃圾收集器和内存分配策略、虚拟机性能监控和故障排除等与自动内存管理相关的内容,以及10多个经典的性能优化案例和优化方法;

第三部分(第6~9章)虚拟机执行子系统

深入分析虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎、多类加载及其执行子系统实战案例;

第四部分(第10~11章)程序编译和代码优化

详细讲解程序前后端的编译和优化,包括前端的可用性优化措施,如泛型的深入分析、主动打包和拆箱、有条件的编译等;以及后端的性能优化措施,如虚拟机的热点检测方法、HotSpot的即时编译器、提前编译器以及各种常见的编译时优化技术;

第 5 部分(第 12-13 章)高效并发

主要讲解Java中高并发的原理,包括Java的内存模型、线程和协程,以及线程安全和锁优化。

全书以实战为指导,通过大量案例分析和演示,结合实际生产环境解决各种Java技术问题的解决方案和技巧。

深入理解Java虚拟机电子版图片预览

《深入理解Java虚拟机》总结

每读一章,我就做个笔记,感觉很乱。看完这篇,总结了一下,对整个Java虚拟机有了一个系统的了解。

首先,Java程序可以“编写一次,随处运行”,因为有Java虚拟机作为容器。作为中间层,Java虚拟机接受我们向上编写的代码生成的字节码,

向机器提供可以直接执行的目标代码,这是Java“平台独立性”的基础。通过这个定义,我们知道任何可以编译字节码的语言都可以获得这种“平台无关性”,

也就是说,像一些类似Java的语言如Groovy、Scala等,因为也可以生成字节码,所以也可以由Java虚拟机执行,是平台无关的。所以Java虚拟机不仅仅针对Java语言,

他从一开始就被明确地创建为具有这种可扩展性。 Android虚拟机其实是Java虚拟机的衍生。学习Java虚拟机对Android开发也有帮助。 Java虚拟机从以下几个方面支持Java:内存管理机制、类加载机制和优化。

内存管理

让我们先谈谈内存管理。内存管理是Java虚拟机在运行时如何管理程序如何划分内存区域,如何分配内存,以及在内存用完时如何回收。

内存区

我们先来说说内存区域的划分。 Java虚拟机将内存划分为很多数据区域,不同的区域有不同的用途和生命周期。我们经常直接接触到的就是运行时数据区,可以细分为:方法区、堆、虚拟机栈、

本机方法堆栈,程序计数器。在这些区域中,方法区和堆是所有线程共享的,可以被所有线程访问,而虚拟机堆栈、本地方法堆栈和程序计数器是线程隔离的。每个线程都有自己独立的区域。不共享。

程序计数器:相当于程序执行过程中的一个行号指示器,类似于操作系统中的ip,指向当前正在执行的虚拟机字节码地址。如果执行 Java 方法,计数器记录正在执行的虚拟机字节码指令的地址。如果是本机方法,则计数器为空

虚拟机栈:虚拟机栈是java方法的内存模型。每个线程在执行期间都会有自己的虚拟机堆栈。在运行过程中,被调用的方法被封装到栈帧中,然后栈帧存储在栈中。栈帧包含方法执行时的相关信息,包括方法使用的局部变量、操作数、动态链接等。

Native 方法栈:类似于虚拟机栈,只是它存储的是 Native 方法。

堆:堆是相对最大的一块内存,用于存储所有线程创建的类的对象实例。如果在方法调用中创建了一个对象,那么该对象实例会存放在堆中,然后对该对象的引用会存放在栈中,这样就可以得到方法对象了。对于内存回收,也就是堆内存的回收。

方法区:存放虚拟机加载的类的信息以及一些常量、静态变量等,这些内容一般是不可变的。

OOM 和 StackOverFlow

OOM 和 StackOverFlow 出现在运行时数据区。如前所述,虚拟机堆栈会将每个调用的方法封装为堆栈帧并存储。这些栈帧必须占用内存,栈的内存也是有限的。

如果很多栈帧还没有被释放,此时另一个栈帧来了。这个堆栈框架没有空间来容纳它。有两种情况。如果虚拟机堆栈不支持动态扩展,则会抛出 StackOverFlow 异常。如果支持动态扩展,

然后堆栈将请求扩展一些空间。当然,内存不是无限的。如果内存扩展太频繁以至于无法再扩展,就会抛出 OutOfMemory 异常。

此外,堆空间是有限的。由于创建的对象都是堆中分配的内存,如果堆中空间不足,没有足够的内存空间为新对象分配内存,此时也会抛出OutOfMemory异常。

内存分配与恢复

要创建一个对象,在堆中为这块内存分配一块内存。当对象不再使用时,占用的内存被回收并用于其他对象。回收内存,需要知道哪些对象会被回收,什么时候会被回收,以及具体的回收算法。

对象创建 - 生成

对象的创建过程非常简单。比如我新建一个对象,虚拟机找到这条指令后,会先检查new后面的参数是否能定位到常量池中某个类的符号引用,并检查该类是否已经加载.

如果没有,执行一个类加载(后面会详细介绍)。加载完成后,虚拟机在堆中为新对象分配一块内存。具体的分配其实是类加载后确定的。分配内存后,

然后,该对象的实例字段将被初始化为零值。最后会对对象进行一些设置,比如设置哈希码、世代年龄信息、对象属于哪个类等等。

这一系列工作完成后,就认为对象创建成功,然后会调用相关代码,按照我们的意愿进行一次初始化。

创建对象后,我们需要一个引用来保存它,以便我们可以使用它。引用放置在虚拟机堆栈帧的局部变量表中。引用有两种形式,一种是直接持有对象地址,另一种是持有句柄,句柄存放在堆中,

包含对象的地址,这是一种间接访问。直接访问速度快,当对象移动频繁时,间接访问更有优势。

哪些对象被回收? ——可访问性分析算法

选择回收哪些对象,虚拟机有很多算法,常用的有引用计数法和可达性分析算法。引用计数的思想是给每个对象设置一个值来统计它被引用的次数。只要有对该对象的引用,就将这个数字加一。

这样,如果一个对象没有任何引用,则引用计数为零,并且该对象被标记为“可回收”。但是这有一个很严重的bug,就是如果我有两个不再使用的对象,但是它们相互引用,那么它们的引用计数永远不会为零,所以它们不会被回收。

现在大部分虚拟机都使用“可达性分析算法”,明显比引用计数法高很多。这个想法是使用一些特定的对象作为 GC Roots,然后从这个节点向下查找对其他对象的引用。

如果一个对象没有到 GC Roots 的引用链,它可以被收集。在Java虚拟机中,指定为GC Roots的对象有:

虚拟机堆栈中引用的对象

方法区静态属性引用的对象

方法区中常量引用的对象

JNI 引用的对象

所以我们在日常开发过程中遇到的内存泄漏很大一部分是应该被回收的对象被GC Roots无意中引用了,比如被static等静态字段引用的对象,所以他可以不被回收

回收算法? ——各种混搭

知道了哪些对象要被回收,下一步就是如何回收它们。垃圾回收算法有很多种,常见的有标记清除法、标记排序法、复制算法、分代收集等。目前的虚拟机基本都是分代收集的,

与其他算法合作。这些算法就不一一介绍了,有兴趣的可以去查一下。

具体:根据对象的生命周期,将内存分为新生代和老年代。在新生代中,由于每次回收大量对象,比较频繁,所以使用了复制算法。另一方面,老年代收集对象相对较少,不那么频繁,而且对象一般较大,因此使用标记清除或标记排序算法。

回收过程? ——双标

具体的回收过程是当GC时发现一个对象是可回收的时,会第一时间进行标记。这是第一个标记。然后它将过滤以查看是否需要执行对象的 finalized() 方法。如果有的话,会被放入队列中,

之后,虚拟机分别处理这个队列中的对象,依次调用它们的finalized()方法,这是对象复活的唯一机会。之后,将再次统一标记。如果这次标记成功,则该对象将被视为死亡,并立即被回收。

GC 的时间安排? ——动态年龄确定

虚拟机将堆分成两个区域进行内存回收,新生代和老年代。新一代分为1个Eden区和2个Survivor区。每次分配内存,如果对象比较大,直接进入老年代。否则,先进入伊甸区和幸存者区,

还为每个对象设置了年龄值。之后,将在某个安全点定期对其进行检查。对于新生代的对象,可回收的对象会被回收,剩下的对象会被复制到另一个Survivor区。在这个过程中,年龄值会加一。这个过程称为 Minor GC,

是属于新生代的GC。当一些对象的年龄值比较大时,会被移到老年代。当然,在此之前,我们会先检查老年代的剩余空间是否足够移动。如果不能满足,则会对老年代进行一次GC,称为Full GC。而这时候检查对象是否可以被GC,也就是GC的时机,

它通常被称为“安全点”。此时勾选不会影响程序的正常运行。

灵活控制——四引号

GC的过程大致是这样的。我们知道Java中有四种引用,分别是强引用、软引用、弱引用和虚引用。这四个引用的区别在于GC的过程:

强引用:直接通过类名新建一个对象,所以直接创建的对该对象的引用称为强引用。被强引用的对象一般不会被回收。

软引用:软引用持有的对象只有在“不回收就会发生内存溢出”时才会被回收

弱引用:弱引用持有的对象将在每次 GC 时被回收

虚引用:没有定时功能,只是一个标记,目的是为了在对象被回收的时候做一些系统通知

类加载机制

Java 平台独立性的基石是字节码。在Java虚拟机中,有一个类文件的概念。一般情况下,每个类都会生成一个类文件,其内容就是字节码。虚拟机执行字节码,

其实加载类的是类文件。 Android中有两种虚拟机,Dalvik虚拟机和ART虚拟机。它们属于Java虚拟机的派生,区别有二:

Java虚拟机基于栈架构,DVM和ART基于寄存器架构

Java 虚拟机执行字节码,DVM ART 不同。 DVM会将class文件重新打包成dex文件并执行dex字节码。 ART 会进一步转化为本地机器码,在 DVM 的基础上执行。

类加载就是加载每个类,类对应的类就是类文件,所以对类文件结构有一个大概的了解是很有必要的。

类文件结构

任何类文件都对应一个唯一的类或接口的定义信息。但类或接口不一定要在类文件中(比如通过类加载器动态加载)。一个类文件是一组二进制流,

category里面虽然有相关信息,但是排列得很紧凑,并且严格规定了第1位和第2位是什么,主要包括magic numbers、常量池等数据信息。

这部分内容看起来还是很枯燥的,只关注一个部分。比如前4个字节是幻数,幻数的唯一作用就是判断文件是否可以被虚拟机接受。

比如有一段叫做常量池入口,这很重要。常量池是类文件结构中与其他项目关联度最高的数据类型,相当于一个资源池。通过这个常量池入口,可以得到常量池信息。具体来说,常量池存储两种类型:文字和符号引用。

Literal:它是一个文字,例如文本字符串。

符号引用:包括三种常量灯:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

它们的作用是在虚拟机运行时通过常量池入口在常量池中找到对应的符号引用,从而找到被引用的类或方法。

类加载机制

类生命周期的7个阶段:

加载验证准备解析初始化使用卸载

其中,验证、准备、分析三个步骤可以合并为一个链接

所以类加载的过程就是加载链接初始化

加载时机——按需加载

虚拟机没有指定类加载过程什么时候开始,只是明确了类加载的生命周期是固定的。但有些特别的是“初始化”。当我们需要使用一个类时,我们必须“初始化”,

而摆在他面前的其他步骤自然是要调用的。因此,可以概括为:加载、验证、准备、解析,这个过程是不确定的,由不同的虚拟机自己控制,可能不知道什么时候进行。

但是当我们需要使用一个类时,它必须从加载开始到初始化结束立即执行,才能使用。

那么你什么时候需要这个课程?以下常见情况:

新建一个对象,或者调用一个类的静态字段或静态方法

反射调用一个类

必须先加载父类,再加载子类

主类在虚拟机刚启动时执行

这些案例都是对类的主动引用。

加载过程——五个步骤

如前所述,类加载过程是类生命周期的前五个步骤:

加载:

通过完全限定名获取定义该类的二进制字节流

将这个字节流表示的静态存储结构转化为方法区的运行时数据结构

在内存中生成一个类对象来表示这个类,作为这个类在方法区的各种数据访问入口

因为加载的过程不限制具体的来源,衍生了很多新的东西,比如读取Jar包,从网络加载类等。

这适用于简单的类。对于数组,不是通过类加载器加载,而是由虚拟机直接创建,然后递归加载数组中引用的类。

验证:验证是链接过程的第一步。目的是保证Class文件的字节流中包含的信息满足当前虚拟机的要求,不会危及虚拟机本身的安全。有四种主要的身份验证类型:

文件格式验证:字节流是否符合Class文件格式规范

元数据验证:语义分析,符合语言规范

字节码验证:分析数据流以确定语义是否合法且合乎逻辑。

符号引用验证:验证符号引用的有效性

准备工作:正式为类分配内存并设置初始值。类变量在方法中分配,初始值设置为类变量而不是实例变量。

Resolve:将常量池中的符号引用替换为直接引用。如前所述,符号引用只是通过名称和其他信息指出引用的方法或类。那么这里的符号引用实际上是转换为直接引用,

是对方法区域类的引用。直接引用类似于指针,所以这个过程可以理解为从名称到地址的翻译。

初始化:前面是加载和链接的过程,这里是类加载过程的最后一步。所谓初始化阶段,就是真正执行类中编写的代码。比如实例变量初始化和构造函数。初始化阶段也可以理解为调用类的构造函数的过程。

加载的工具 - 类加载器

如前所述,“加载”过程的第一步是通过类的全限定名获取类的二进制字节流。这个过程是在虚拟机外部的工具的帮助下执行的,即类加载器。每个类都有一个类加载器。两个类是否相同,不仅要比较它们自己,还要比较它们的类加载器。

类加载器可以分为三类:

启动类加载器:它是用 C++ 编写的,是虚拟机的一部分。它是一个非常基本的加载器,用于加载 Java 目录中 lib 中的类。

扩展类加载器:可供开发者使用

应用程序类加载器:也称为系统类加载器,它加载用户在类路径上指定的类。我们通常使用这个。

具体的加载逻辑称为“父代理模型”,即首先有一个根加载器“启动类加载器”,其下有一个子类“扩展类加载器”,其下为“应用类加载器”和最后是“自定义类加载器”。具体流程:

接收到加载请求的类会先将请求委托给父类加载,每个加载器都是这样。这最终会将请求传递给根节点的“启动类加载器”。之后,如果父加载器可以加载,则直接加载。否则,请求将被再次传递。

虚拟机优化

Java 的编译时间是一个非常不确定的过程。因为Java有很多编译期,所以有前端编译期、后端编译器和静态提前编译器。前端编译时负责将.java转换成简单的.class,

后端编译器负责将字节码转换为机器码,例如 JIT。静态提前编译器将 .java 直接翻译为本机机器代码,例如 AOT。因此,编纂时期无法准确分类,只能大致分为“早”和“晚”。

早期优化

早期可以概括为前端编译器将.java转为.class的过程。这一阶段的优化也可以称为编译期优化

这个阶段其实和其他语言的编译期优化类似,无非就是词法分析、语法分析、语义分析,然后是一些语言级别的优化。例如,语法糖、注释处理和字符串连接。 Java中的语法糖不多,但是相当实用,比如类型擦除、自动拆箱、装箱等。注解在编译时进行了优化,并且只在运行时有效。再举一个例子,大家都知道String和StringBuilder和StringBuffer的区别。据说每次用“+”链接两个字符串,都会创建一个新的String,会消耗大量内存。事实上,这种说法并不完全正确。如果只是串联,哪怕是换行,编译器如果识别出来,就会为我们优化,也就是把它们当成一个String对象。只有在极少数情况下,例如循环结构中频繁链接的字符串,才会出现刚才提到的问题。

运行时优化

运行时优化,众所周知的如 JIT 和 AOT。之所以这样分离虚拟机,是为了增加虚拟机的可扩展性,也就是说普通的前端编译期只接受Java。并且后端编译器可以接受像 Groovy 这样的语言。同时,JIT 和 AOT 极大地优化了编译的性能,因此被选为 Android 中 Java 虚拟机使用的编译器。

我们先说JIT,它将字节码转换为机器码,也就是DVM使用的编译器。它的特点可以类比。比如,你要背一首诗,你要在我面前背,还要重复好几遍,你必须背很久,才能一口气读出来。通过JIT,我可以让你根据书读一个单词,背一个句子。这使它易于携带。但 JIT 不一定比普通解释器慢很多。在JVM中,JIT是针对hot code的,针对这些代码进行JIT编译。因此,JIT 就编译本身的翻译过程而言是比较慢的,执行起来比较快。举个例子,如果只要求你总结意思,背几行诗,那么你最好尽快记住这些页面。对于热门的诗句,如果你能一眼看懂一个句子,那么速度是相当快的。

我们来谈谈 AOT。 AOT 就是直接将.java 转换成本机机器码。以上面的例子为例,在我给你的这首古诗里,你其实已经背了一部分,所以现在你可以背诵一小部分,所以速度很快,但代价是你需要准备提前,所以占用脑容量大。

在 Android 中,之前的 DVM 使用 JIT,而当前的 ART 使用 AOT。具体不同的是,DVM编译的时候,安装过程比较快,占用空间小,但是执行比较慢。另一方面,AOT 的安装过程缓慢,占用大量空间,但执行速度很快。

阅读剩余
THE END