首页 > 代码库 > [深入理解Java虚拟机]<阅读笔记>

[深入理解Java虚拟机]<阅读笔记>

Overview

  • 走近Java:介绍Java发展史

第二部分:自动内存管理机制

程序员把内存控制的权利交给了Java虚拟机,从而可以在编码时享受自动内存管理。但另一方面一旦出现内存泄漏和溢出等问题,就需要了解一些底层的知识来进行错误排查。

  • 自动内存管理机制:介绍内存是如何划分的。
  • 垃圾收集器与内存分配策略:分析垃圾收集算法
  • 虚拟机性能监控与故障处理工具
  • 调优案例分析与实战

第三部分:虚拟机执行子系统

  • 类文件系统:介绍Class文件结构的各个组成部分。
  • 虚拟机类加载机制:介绍类加载过程的各个阶段。
  • 虚拟机字节码执行引擎
  • 类加载及执行子系统的案例与实战

第四部分:程序编译与代码优化

Java程序从源码编译成字节码和字节码编译成本地机器码两个过程,加起来就等同于一个传统编译器所执行的编译过程。

  • 早期(编译器)优化:分析泛型、主动拆箱和装箱、条件编译等多种语法糖的前因后果。
  • 晚期(运行期)优化:介绍虚拟机的热点探测方法、HotSpot的即时编译器等等。

第五部分:高效并发

  • Java内存模型与线程
  • 线程安全与锁优化

走近Java

  • Java技术体系:
    • Java程序设计语言
    • 各种硬件平台上的Java虚拟机
    • Class文件格式
    • Java API类库
    • 第三方Java类库

  其中,Java语言 + JVM + API类库 = JDK(Java Development Kit)。JDK是支持Java程序开发的最小环境。

自动内存管理机制

Java内存区域与内存溢出异常

1. 运行时数据区域

JVM在执行Java程序时将其所管理的内存划分为若干个不同的数据区。每个区域有各自的用途,以及创建和销毁时间

技术分享

 

  • 程序计数器:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖该计数器。
    • JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时间,一个处理器只会执行一条线程中的指令。因此,为了在线程切换时恢复到正确的执行位置,每个线程都需要一个独立的程序计数器
    • 若线程在执行一个Java方法,则计数器纪录的是正在执行的虚拟机字节码指令的地址;若执行的是native方法,则计数器值为空。
    • 该内存区域是唯一一个未规定任何OutOfMemoryError情况的区域
  • Java虚拟机栈:
    • 与程序计数器一样,也是线程私有的
    • JVM栈描述的是Java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 每个方法从调用到执行完成的过程,就对应一个栈帧在JVM栈中入栈到出栈的过程
    • 很多时候,我们经常会讲Java内存区分为Heap和Stack,这种分法比较粗糙,这里所指的栈其实就是上面介绍的局部变量表,因为这是程序员最关注的。
    • 局部变量表存放了编译器可知的各种基础数据类型、对象引用(注意这不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。)以及returnAddress类型。
    • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,该方法所需在帧中分配的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。
    • 在JVM规范中,对该区域规定了两种异常情况:
      • 若线程请求的栈深度大于虚拟机所运行的深度 --> StackOverflowError异常;
      • 若JVM栈可以动态扩展(当前大部分JVM都可以动态扩展,只不过JVM规范中也运行固定长度的JVM栈),若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 本地方法栈:与JVM栈作用十分类似,主要区别在于JVM栈未JVM执行Java方法(也就是字节码)服务,而本地方法栈则为JVM所使用到的Native方法服务。
  • Java堆:对大多数应用而言,heap时JVM所管理的内存中最大的一块。
    • heap对所有线程共享,在JVM启动时创建
    • 此内存区域的唯一目的就是存放对象实例,几乎所有对象都在这里分配内存。
    • heap是垃圾收集器管理的主要区域。从内存回收的角度来看,由于现在的收集器基本都采用分代收集算法,所以Java堆还可以细分为新生代和老年代
    • 根据JVM规范,heap可以处理物理上不连续的内存空间,只要逻辑上是连续的即可。可以是固定大小的,也可以是可扩展的。
    • 若堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛OutOfMemoryError异常。
  • 方法区:同样是多个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 虽然JVM规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-heap。
    • JVM对方法区的限制非常宽松,除了和heap一样不需要连续内存和可选固定大小或可扩展外,还可以选择不实现垃圾收集
    • 该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
    • 可能抛OutOfMemoryError异常。
  • 运行时常量池:是方法区的一部分
    • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放。
    • 运行时常量池相对class文件常量池的一个重要特征是具备动态性。即可以在运行期间将新的常量放入池中。最常见的就是String类的intern()方法。
  • 直接内存
    • 直接内存并不是JVM运行时数据区的一部分,也不是JVM规范中定义的内存区域。
    • 但这部分内存也被频繁地使用,也可能导致OutOfMemoryError。
    • NIO类:引入了一种基于通道(Channel)与缓存区(Buffer)的I/O方法,可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。(这样能显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。)
    • 总之,直接内存的分配不受到Java堆大小的限制,但仍然受到本机总内存的限制,仍可能抛OutOfMemoryError。

2. HotSpot虚拟机对象探秘

  1. 对象的创建:虚拟机在遇到一条new指令时:
    1. [类加载检查]:首先检查该指令的参数是否能在常量池中定位到一个类的符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过。
    2. [为新生对象分配内存]:对象所需内存的大小在类加载完成后便可完全确定(后续会介绍如何确定)。为对象分配内存

 

[深入理解Java虚拟机]<阅读笔记>