[深入理解Java虚拟机]<自动内存管理>

时间:2022-12-10 13:08:37

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程序时将其所管理的内存划分为若干个不同的数据区。每个区域有各自的用途,以及创建和销毁时间。

[深入理解Java虚拟机]<自动内存管理>

  • 程序计数器:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖该计数器。
    • JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时间,一个处理器只会执行一条线程中的指令。因此,为了在线程切换时恢复到正确的执行位置,每个线程都需要一个独立的程序计数器。
    • 若线程在执行一个Java方法,则计数器纪录的是正在执行的虚拟机字节码指令的地址;若执行的是native方法,则计数器值为空。
    • 该内存区域是唯一一个未规定任何OutOfMemoryError情况的区域。
  • Java虚拟机栈:
    • 与程序计数器一样,也是线程私有的。
    • JVM栈描述的是Java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 每个方法从调用到执行完成的过程,就对应一个栈帧在JVM栈中入栈到出栈的过程。
    • 很多时候,我们经常会讲Java内存区分为Heap和Stack,这种分法比较粗糙,这里所指的栈其实就是上面介绍的局部变量表,因为这是程序员最关注的。
    • 局部变量表存放了编译器可知的各种基础数据类型、对象引用(注意这不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。)以及returnAddress类型。
    • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,该方法所需在帧中分配的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。
    • 在JVM规范中,对该区域规定了两种异常情况:
      • 若线程请求的栈深度大于虚拟机所运行的深度 --> *Error异常;
      • 若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堆中划分出来(如果Java堆中内存是绝对规整的,那么只需要简单地维持一个指针作为分界点指示器,这种方式称为“指针碰撞”。如果内存不是规整的,那JVM必须维护一个列表,纪录哪些内存块是可用的,这种方式称为“空闲列表”。heap是否规整由垃圾收集器是否带有压缩整理功能决定。)。
    3. [线程安全问题]:对象创建载JVM中是非常频繁的,即使仅仅是修改一个指针所指向的位置,在并发情况下也不是线程安全的。解决方案:1)对分配内存空间的动作进行同步处理,保证更新操作的原子性;2)把内存分配动作按线程划分在不同的空间之中进行。
    4. [初始化]:内存分配完成后,JVM需要将分配到的空间都初始化为零(不包括对象头)。
    5. [对对象进行必要的设置]:例如该对象是哪个类的实例,如何才能找到类的元数据信息、对象的hash码、对象的GC分代年龄等信息。这些信息存放在对象头(Object header)之中。
  2. 对象的内存布局:在HotSpot虚拟机中,对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
    1. 对象头:一部分用于存储对象自身的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁等;另一部分是类型指针,即对象指向它的类元数据的指针;另外如果对象是Java数组,那么对象头中还必须有一块用于纪录数组长度的数据,因为JVM可以通过普通Java对象的元数据信息确定Java对象的大小,但从数组的元数据中无法确定数组的大小。
    2. 实例数据:对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到JVM分配策略参数和字段在Java源码中的定义顺序的影响。HotSpot默认分配策略为longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers) [可以看出相同宽度的字段总是被分配到一起],在类型相同的情况下会按照定义顺序排。
    3. 对齐填充:该部分是非必然存在的,它仅仅起着占位符的作用。因为HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。
  3. 对象的访问定位:Java程序需要通过栈上的reference数据来操作堆上的具体对象。对象访问方式取决于JVM,主流的有使用句柄和直接指针指针两种。
    • 句柄访问:heap中将会划分出一块内存来作为句柄池,reference中存的就是对象的句柄地址。好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时只改变实例数据指针。
      [深入理解Java虚拟机]<自动内存管理>
    • 直接指针:reference中存储的直接就是对象的地址。那么heap对象的布局中就必须考虑如何放置访问类型数据的相关信息。好处是速度更快,节省了一次指针定位的时间开销。
      [深入理解Java虚拟机]<自动内存管理>

3. 实战:OutOfMemoryError异常

在JVM规范中,除了程序计数器外,虚拟机内存的其他几个运行时区域都可能OOM。

  1. Java堆溢出
    Java堆用于存储对象实例,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会产生OOM。
    • 首先要判断是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
    • 若是内存泄漏:可使用工具查询泄漏对象到GC Roots的引用链。
    • 若是内存溢出:从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况,尝试减少程序运行期的内存消耗。
  2. 虚拟机栈和本地方法栈溢出
    HotSpot中不区分虚拟机栈和本地方法栈。
    1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,抛*。
    2. 若虚拟机在扩展栈时无法申请到足够的内存空间,抛OOM。
  3. 方法区和运行时常量池溢出
    • 一段常量池OOM的代码:
      public class RuntimeConstantPoolOOM {
      public static void main(String[] args) {
      // use List to keep the reference of Constants, avoiding GC
      List<String> list = new ArrayList<>();
      int i = 0;
      while (true) {
      list.add(String.valueOf(i++).intern());
      }
      }
      }
  4. 本机直接内存溢出
    DirectMemory容量默认与heap最大值一样。

[深入理解Java虚拟机]<自动内存管理>的更多相关文章

  1. 深入理解JAVA虚拟机 自动内存管理机制

    运行时数据区域 其中右侧三个一起的部分是每个线程一份,左侧两个是所有线程共享的. 程序计数器(Program Counter Register) 英文名称叫Program Counter Regist ...

  2. 深入理解java虚拟机,内存管理部分

    1,对象回收前会调用finalize()方法,尝试自救,只能调用一次 2,上面横向对比c++的析构函数,但是java有良好的内存管理,而且try/catch做得比较好 3,回收一个常量,1,对象的实例 ...

  3. &lbrack;翻译&rsqb;理解Unity的自动内存管理

    当创建对象.字符串或数组时,存储它所需的内存将从称为堆的*池中分配.当项目不再使用时,它曾经占用的内存可以被回收并用于别的东西.在过去,通常由程序员通过适当的函数调用明确地分配和释放这些堆内存块.如 ...

  4. 深入java虚拟机学习 -- 内存管理机制

    前面说过了类的加载机制,里面讲到了类的初始化中时用到了一部分内存管理的知识,这里让我们来看下Java虚拟机是如何管理内存的. 先让我们来看张图 有些文章中对线程隔离区还称之为线程独占区,其实是一个意思 ...

  5. 深入理解JAVA虚拟机(内存模型&plus;GC算法&plus;JVM调优)

    目录 1.Java虚拟机内存模型 1.1 程序计数器 1.2 Java虚拟机栈 局部变量 1.3 本地方法栈 1.4 Java堆 1.5 方法区(永久区.元空间) 附图 2.JVM内存分配参数 2.1 ...

  6. JAVA之自动内存管理机制

    一.内存分配 1.JVM体系结构 2.运行时数据区域 3.内存分配二.内存回收 1.垃圾收集算法 2.垃圾收集器三.相关参考一.内存分配JVM体系结构 在了解自动内存管理的内存分配之前,我们先看下JV ...

  7. Java虚拟机的内存管理

    众所周知,Java程序员写的代码是没有办法控制Java对象的内存释放的,完全有JVM暗箱操作. 虽然程序员把内存的释放的任务都交给了Java虚拟机,但是并不代表Java程序就不存在内存泄漏. 反而,某 ...

  8. 深入理解java虚拟机【内存溢出实例】

    通过简单的小例子程序,演示java虚拟机各部分内存溢出情况: (1).java堆溢出: Java堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收 ...

  9. 深入了解Java虚拟机和内存管理

    1.java程序的执行过程      java源文件->解析器->class文件->java类加载器->java运行时数据区->执行引擎 2.我们接下来看一下java运行 ...

随机推荐

  1. Jmeter学习笔记TWO

    使用非GUI模式运行Jmeter脚本并自动生成测试报告 命令:jmeter -n -t tougu.jmx -l result.jtl -e -o /tmp/ResultReport 这个命令是用于执 ...

  2. service里面弹出对话框

    如何在service里面弹出对话框先给一个需求:需要在service里面监听短信的接收,如果接收到短信了,弹出一个dialog来提示用户打开. 看看效果图:(直接在主桌面上弹出) service中弹出 ...

  3. JSON value

    JSON values can be: A number (integer or floating point) A string (in double quotes) A Boolean (true ...

  4. 第五百八十三天 how can I 坚持

    今天去看了个电影,日本动漫,第一次在电影院看日本动漫,<你的名字>,挺经典的.存在爱情吗?什么是爱情,哎.什么是人. 好像有点感冒了呢,说过自己很久没感冒后,往往就会感冒,这到底是怎么回事 ...

  5. 使用线程新建WPF窗体&lpar;公用进度条窗体&rpar;

    使用线程新建窗体 项目中需要一个公用的进度条窗体.大家知道在wpf中,有两个线程,一个是UI线程,另一个是监听线程(一直监听用户的输入).如果我们后台有阻塞UI线程的计算存在,那么界面上的比如进度条什 ...

  6. 持续集成Jenkins&plus;sonarqube部署教程

    1 引言 1.1 文档概要 本文主要介绍jenkins,sonar的安装与集成,基于ant,maven构建.用一个例子介绍jenkins的编译打包部署,代码检查.最后集成jenkins.(现阶段只是简 ...

  7. 【C&num;】组件分享:FormDragger-窗体拖拽器

    适用:.net2.0+ winform项目 介绍: 类似QQ.迅雷等讲究UI体验的软件,都支持在窗口内多处地方拖动窗口,而不必老实巴交的去顶部标题栏拖,这个组件就是让winform也能这样随性拖拽,随 ...

  8. Game Theory

    HDU 5795 || 3032 把x个石子的堆分成非空两(i, j)或三堆(i, j, k)的操作->(sg[i] ^ sg[j])或(sg[i] ^ sg[j] ^ sg[k])是x的后继 ...

  9. whu暑期集训&num;1

    题号:SGU123----SGU131 Problem A: 题意:求斐波那契的前N项和.. 做法:直接模拟,注意得用long long Problem B: 题意:给定一个封闭的多边形,求一个点在不 ...

  10. Solr 访问 403 错误

    把 Solr 基础环境搭建好后访问发现会出现 403 错误: 解决方法: 找到自己 Tomcat 目录下的 solr ,找到  ...\solr\WEB-INF\web.xml,然后把 169 - 1 ...