• 《深入理解JVM》笔记 第2章 Java内存区域与内存溢出异常


    一、运行时数据区域

    PS:jdk1.8中内存区域有所不同,方法区变成了元数据区。这里为了更好的理解jdk1.7到jdk1.8的变化,仍然先去学习书中所讲的版本。作图工具:Office Excel

    1. 程序计数器

    可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行一个Native方法,这个计数器的值则为空(Undefined)。

    此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    2. Java虚拟机栈

    线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    局部变量表存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型。

    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,扩展时无法申请到足够内存,将抛出OOM

    3. 本地方法栈

    本地方法栈与虚拟机栈作用非常相似。Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机所用到的Native方法服务。

    HotSpot直接把本地方法栈和虚拟机栈合二为一。

    4. Java堆

    虚拟机启动时创建,唯一的目的就是存放对象实例。Java堆是垃圾收集管理的主要区域,因此很多时候也被称作“GC”堆。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续。可以是固定大小,也可以是可扩展的,主流虚拟机按照可扩展来实现,通过-Xmx 和 -Xms控制。无法扩展时会抛出OOM。

    Java虚拟机规范描述:所有的对象实例和数组都要在堆上分配。(实际上因为栈上分配、标量替换优化技术的存在,不那么绝对了)

    注意:线程共享的Java堆中可能分出多个线程私有的分配缓冲区。

    5. 方法区

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。HotSpot曾选择把GC分代收集扩展至方法区,或者说用永久代实现方法区,图个省事。但是这样更容易遇到内存溢出问题,所以已经有放弃永久代并逐步改用Native Memory来实现方法区的规划。 JDK1.7,已经把原本放在永久代的字符串常量池移出。

    6. 运行时常量池 Runtime Constant Pool

    运行时常量池是方法区的一部分,Class文件中常量池(存放编译期生成的字面量和符号引用)在类加载后进入方法区的运行时常量池存放。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

    并非只有Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,比如String的intern()方法。

    常量池无法再申请到内存时,会抛出OutOfMemoryError异常。

    ps:jdk1.7中,常量池已经改为存放在堆中,这是官网原话:

    In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.

    后面两句大意是:这个变化需要我们调一下堆的大小。它对大多数程序没啥影响,但是加载了很多class文件的大型项目或者大量使用到String.intern()这个方法的项目会有一些重要的变化。

    7. 直接内存 Direct Memory

    直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,也可能导致OOM异常。

    JDK1.4中新增了NIO,引入了一种基于通道与缓冲区地IO方式,它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合显著提高性能,因为避免了在Java堆中和Native堆中来回复制数据。

    直接内存受本机总内存(包括RAM和SWAP区或者分页文件)大小以及处理器寻址空间的限制。

    各内存区域总和大于物理内存限制,会报OutOfMemoryError。

    二、HotSpot虚拟机对象探秘

    1. 对象的创建

    这里讨论的是普通java对象,不包括Java数组和Class对象,创建流程如图(作图工具为visio)

    分配内存的方式有两种:

    • 指针碰撞(Bump the pointer)
    • 空闲列表(Free List)

    使用哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由采用的垃圾收集器是否带有压缩整理功能决定。

    在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞;

    在使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

    如何解决并发创建?对象先尝试在TLAB创建,此时不存在线程安全问题。但是TLAB都很小,一般只占Eden区的1%,所以在TLAB很可能创建不了。这时候才会去Eden创建对象。

    可以通过 -XX:+/-UseTLAB参数 来控制是否使用TLAB。还可以通过首选项 -XXTLABWasteTargetPercent设置TLAB占Eden大小。

    2. 对象的内存布局

    3. 对象的访问定位

    Java程序需要通过栈上的reference数据来操作堆上的具体对象。具体访问方式和虚拟机的实现有关。主要有两种方式:

    • 使用句柄访问,那么Java堆中会划分句柄池,reference中存储的就是句柄地址(优点,reference本身不会因为对象被移动而需要修改,缺点是多了一次指针定位的执行开销)
    • 使用直接指针,Java堆对象需要考虑放置访问类型数据的相关信息,refenrence存储的直接就是对象地址

    HotSpot使用的第二种方式,即reference存储直接指针。

  • 相关阅读:
    Codeigniter 控制器的继承问题
    laravel 安装
    js preventDefault() 方法
    jquery 获取$("#id").text()里面的值 需要进行去空格去换行符操作
    HDU_1394_线段树
    Codeforces_723_D
    Codeforces_723_C
    Codeforces_723_B
    Codeforces_723_A
    HDU_4456_二维树状数组
  • 原文地址:https://www.cnblogs.com/jdbc2nju/p/16028852.html
Copyright © 2020-2023  润新知