读书笔记:《深入理解Java虚拟机:JVM高级特性与最佳实践(第三版)》
一、第一部分 走近java
第一章:走近java
世界上并没有完美的程序,但我们并不因此而沮丧,因俄日写程序本来就是一个不断追求完美的过程。
1.1 概述
Java很牛逼,用的人很多。
1.2 Java技术体系
从广义上来讲,Kotlin、Clojure、JRuby、Groovy等运行于JVM上的语言机器程序都属于Java技术体系的成员。
如果仅从传统意义上来看,JCP(Java Community Process,就是人们常说的Java社区)官方定义的Java技术体系包括了以下几个组成部分:
- Java程序设计语言
- 各种平台上的JVM的实现
- Class文件格式
- Java类库API
- 来自商业机构和开源社区的第三方Java类库
JDK = Java语言 + JVM + Java类库
第一章还有很多内容,但我个人认为没有记录的必要,仅作了解就好。如果后续真的需要,可以再认真阅读。
二、第二部分 自动内存管理
第二章:Java内存区域与内存溢出异常
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
2.1 概述
对于Javaer来说,JVM的自动内存管理看似很美好,但是其中有个很大的问题,就是JVM为程序员屏蔽了硬件与平台的差异,可以使得程序员更加专注于业务而非底层的细节。但是这种屏蔽并不是毫无代价的,它牺牲了一些硬件相关的性能特性。比如一个程序,很可能在100人使用的情况下没有任何问题,但是在10000人使用的情况下可能就会非常缓慢甚至崩溃。要满足10000人的使用,肯定需要更高规格的硬件支持,但是在绝大多数情况下,硬件性能无法等比例的提升程序的性能和并发能力,甚至可能对程序性能没有任何提升。如果不了解虚拟机诸多技术特性的运行原理,就无法写出适合虚拟机运行和自动优化的代码。
2.2运行时数据区
根绝《Java虚拟机规范》的规定,JVM所管理的内存将包括以下几个区域:
- 程序计数器 Program Counter Register
- 虚拟机栈 VM Stack
- 方法区 Method Area
- 堆 Heap
- 本地方法栈 Native Method Stack
其中方法区和堆是线程共享的,其他三个区域是线程私有的。
2.2.1 程序计数器
程序计数器是一块较小的内存空间,是在《Java虚拟机规范》中唯一没有规定任何OOM(OutOfMemoryError,后续简称OOM)情况的区域。
它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是时序控制六的治时期,分支、循环、顺序、跳转、异常处理、线程恢复等功能都是依赖计数器实现的。
如果一个线程正在执行Java方法,那么这里存放的就是当前正在执行的字节码指令的地址;如果正在执行的时一个Native方法,那么这里的值应为空(Undefined)
2.2.2 虚拟机栈
虚拟机栈也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程的内存模型。每当一个方法被执行的时候,JVM都会同步创建的一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完成的整个过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存储了方法中用到的所有局部变量(包括方法参数、方法中声明的局部变量)和返回地址(指向了一条字节码指令的地址,可以理解为方法条用处的下一行代码)。
在《Java虚拟机规范》中,对这个内存区域规定了两种异常状况:
- 如果线程请求的栈深度超过了JVM所允许的深度,将抛出SOF(StackOverflowError,后续简称SOF)
- 如果JVM栈容量可动态扩展,当栈扩展时无法申请到足够的内存会抛出OOM异常
- 如果JVM栈容量不能动态扩展,那么在申请失败时就会抛出OOM异常
2.2.3 本地方法栈
本地方法栈与虚拟机栈所发ui的作用是非常相似的,区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则视为本地方法服务。
2.2.4 Java堆
堆是虚拟机所管理的最大的一块内存,在虚拟机启动时创建,被所有线程共享。
所有的对象和数组都存放在堆中。堆也是垃圾回收器工作的区域。
堆的布局,基于分代回收理论,大致可以分为新生代和老年代。其中新生代又可以分为Eden区、幸存者FROM区、幸存者TO区。
注:这里的分区并不是《Java虚拟机规范》中的强制性规定,而是基于垃圾回收算法的工作模式而区分的。关于垃圾回收器和算法,会在后面的章节中提到。
根据《Java虚拟机规范》的规定,堆可以处于在物理上不连续的内存空间中,但是其在逻辑上应当被视为联连续的。
堆既可以是固定大小的,也可以是可扩展的,这取决于JVM的具体实现。不过主流的JVM实现都是可扩展的,通过-Xmx和-Xms参数来设定。
如果在堆中没有足够的内存完成实力分配,并且堆也无法继续扩展,则会抛出OOM异常。
2.2.5 方法区
方法区也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
《Java虚拟机规范》堆方法区的约束非常宽松,除了跟堆一样不需要连续的内存和可以选择固定或者可扩展大小外,甚至可以选择不实现垃圾回收。
如果方法区无法满足新的内存分配需求时,将抛出OOM异常。
2.2.6 运行时常量池
运行时常量池是方法区的一部分。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用。
这部分内容将在类加载后方到方法区的运行时常量池中。
常量并不一定是在编译期产生,运行期间也可以将新的常量方如常量池,比如String.intern()方法。
运行时常量池是方法区的一部分,自然也受到方法区内存的限制,当常量池无法申请到内存时会抛出OOM异常。
特:JDK7以后,字符串常量池被移到堆中,并不在方法区,垃圾回收器对堆和方法区会区别对待,所以字符串常量池的表现和运行时常量池有时会不一致
2.2.7 直接内存
直接内存并不是运行时数据区的一部分,也不是《Java虚拟机规范》定义的内存区域。
但是这部分内存也被频繁的使用,并且也可能导致OOM。
在jdk1.4中新加入了NIO(new Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的IO方式。
它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在堆和堆外内存中来回复制数据。
一般在运维配置JVM参数时,会根据实际内存去设置-Xmx等参数信息,但是有可能会忽略掉直接内存,从而导致各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OOM。

浙公网安备 33010602011771号