• 02-运行时数据区域


    0. review JVM

    JVM 是一台执行 Java 字节码的虚拟计算机,它拥有独立的运行机制,其运行的 Java 字节码也未必由 Java 语言编译而成。

    Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应的平台上的机器指令执行。每一条 Java 指令,Java 虚拟机规范中都有详细定义,如何取操作数,如何处理操作数,处理结果放在哪里。

    1. 程序计数器

    Program Counter Register 程序计数器(物理上通过“寄存器”实现)

    作用:记住下一条 JVM 指令的执行地址,可以看作是当前线程所执行的字节码的行号指示器。

    在 JVM 的概念模型里,〈字节码解释器〉工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令(然后翻译成机器码,继而交给 CPU 运行),它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

    如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。

    【小结】① 是线程私有的;② 不会存在 OutOfMemoryError 情况

    2. 虚拟机栈

    Java Virtual Machine Stacks (Java 虚拟机栈)

    2.1 概述

    • 每个线程运行时所需要的内存,称为“虚拟机栈”,它是线程私有的,生命周期与线程相同。
    • “虚拟机栈”描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
    • 每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;每个线程只能有一个“活动栈帧”,对应着当前正在执行的那个方法。

    局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

    这些数据类型在局部变量表中的存储空间以「局部变量槽(Slot)」来表示,其中 64 位长度的 long 和 double 类型的数据会占用 2 个变量槽,其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照 1 个变量槽占用 32 个比特、64 个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

    2.2 演示栈帧

    2.3 问题辨析

    1. 垃圾回收是否涉及栈内存

    2. 栈内存分配越大越好吗

    3. 方法内的局部变量是否线程安全

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
    • 如果是局部变量引用了对象,并逃离方法的作用范围(如:作为返回值返回),需要考虑线程安全。

    2.4 栈内存溢出

    在《Java 虚拟机规范》中,对这个内存区域规定了 2 类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛 StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

    1. 栈帧过多导致栈内存溢出

    2. 栈帧过大导致栈内存溢出

    HotSpot 虚拟机的栈容量是不可以动态扩展的,以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常 —— 只要线程申请栈空间成功了就不会有 OOM,但是如果申请时就失败,仍然是会出现 OOM 异常的。

    2.5 线程运行诊断

    2.5.1 案例:CPU占用过高

    1. 通过 top 定位哪个进程对 CPU 的占用过高

    2. 通过 ps H -eo pid,tid,%cpu | grep 进程id 进一步定位是哪个线程引起的 CPU 占用过高。

    3. 通过 jstack 进程id 找到有问题的线程,进一步定位到问题代码的源码行号

    2.5.2 案例:程序运行很长时间没有结果

    3. 本地方法栈

    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行的 Java 方法(也就是字节码)服务,而本地方法栈为本地方法(一般是指用其它语言如 C、C++ 或汇编语言等编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理)服务。

    「本地接口(Java Native Interface,JNI)」 的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序,Java 诞生的时候是 C/C++ 横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域(就是本地方法栈) 用于处理标记为 native 的代码(凡是带了 native 关键字的,说明 Java 的作用范围达不到了)。

    它的具体做法是在 「本地方法栈」 中登记 native 方法,在 Execution Engine 执行时加载 「本地方法库(Native Libraies)」,然后通过 「本地接口」 去调用。

    如:new Thread().start() 执行完这行代码,线程就开始运行起来了吗?不一定,Java 能做的部分就到调用 private native void start0(); 这儿了(该方法进的是 [本地方法栈],然后会通过 JNI 去调用底层 C/C++ 的类库),而具体线程运行还要看 OS 和 CPU 的调度。

    与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。

    4. 堆

    4.1 概述

    堆(Heap)是被所有线程共享(需要考虑线程安全的问题)的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。

    4.2 堆内存

    4.2.1 内存结构

    根据《Java 虚拟机规范》的规定,Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

    Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩展来实现的(通过参数 -Xmx 和 -Xms 设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

    4.2.2 内存溢出

    Java 堆是垃圾收集器管理的内存区域。

    禁用显式垃圾回收:

    4.3 堆内存诊断

    4.3.1 jps 工具

    查看当前系统中有哪些 Java 进程。

    4.3.2 jmap 工具

    通过 jmap -heap <tid> 查看线程在某一时刻堆内存占用情况。

    Console >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> jmap -heap 232
    Attaching to process ID 232, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.20-b23
    
    using thread-local object allocation.
    Parallel GC with 8 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 4267704320 (4070.0MB)
       NewSize                  = 89128960 (85.0MB)
       MaxNewSize               = 1422393344 (1356.5MB)
       OldSize                  = 179306496 (171.0MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 21807104 (20.796875MB)
       CompressedClassSpaceSize = 1073741824 (1024.0MB)
       MaxMetaspaceSize         = 17592186044415 MB
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 67108864 (64.0MB)
       used     = 8058472 (7.685157775878906MB) <<<<<<<<<<<<<<<<<<
       free     = 59050392 (56.314842224121094MB)
       12.008059024810791% used
    From Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    To Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    PS Old Generation
       capacity = 179306496 (171.0MB)
       used     = 0 (0.0MB)
       free     = 179306496 (171.0MB)
       0.0% used
    
    3080 interned Strings occupying 253008 bytes.
    
    Console >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> jmap -heap 232
    Attaching to process ID 232, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.20-b23
    
    using thread-local object allocation.
    Parallel GC with 8 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 4267704320 (4070.0MB)
       NewSize                  = 89128960 (85.0MB)
       MaxNewSize               = 1422393344 (1356.5MB)
       OldSize                  = 179306496 (171.0MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 21807104 (20.796875MB)
       CompressedClassSpaceSize = 1073741824 (1024.0MB)
       MaxMetaspaceSize         = 17592186044415 MB
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 67108864 (64.0MB)
       used     = 18544248 (17.68517303466797MB) <<<<<<<<<<<<<<<<<<
       free     = 48564616 (46.31482696533203MB)
       27.6330828666687% used
    From Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    To Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    PS Old Generation
       capacity = 179306496 (171.0MB)
       used     = 0 (0.0MB)
       free     = 179306496 (171.0MB)
       0.0% used
    
    3081 interned Strings occupying 253056 bytes.
    
    Console >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> jmap -heap 232
    Attaching to process ID 232, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.20-b23
    
    using thread-local object allocation.
    Parallel GC with 8 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 4267704320 (4070.0MB)
       NewSize                  = 89128960 (85.0MB)
       MaxNewSize               = 1422393344 (1356.5MB)
       OldSize                  = 179306496 (171.0MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 21807104 (20.796875MB)
       CompressedClassSpaceSize = 1073741824 (1024.0MB)
       MaxMetaspaceSize         = 17592186044415 MB
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 67108864 (64.0MB)
       used     = 2684400 (2.5600433349609375MB) <<<<<<<<<<<<<<<<<<
       free     = 64424464 (61.43995666503906MB)
       4.000067710876465% used
    From Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    To Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    PS Old Generation
       capacity = 179306496 (171.0MB)
       used     = 959984 (0.9155120849609375MB)
       free     = 178346512 (170.08448791503906MB)
       0.5353871841876827% used
    
    3067 interned Strings occupying 252064 bytes.
    

    4.3.3 jconsole 工具

    图形界面的多功能的监测工具,可以连续监测。

    命令:jconsole

    4.3.4 jvisualvm 工具

    垃圾回收后,内存占用仍然很高。

    5. 方法区

    5.1 定义

    5.2 位置变化

    方法区是 JVM 规范,JDK6 对方法区的实现称为“永久代”,JDK8 对方法区的实现称为“元空间”。

    5.3 方法区内存溢出

    该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    • 1.8 以前使用永久代实现方法区
      永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
      -XX:MaxPermSize=8m
      
    • 1.8 之后使用元空间实现方法区
      元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
      -XX:MaxMetaspaceSize=8m
      

    代码演示:

    5.4 运行时常量池

    运行时常量池(Runtime Constant Pool)是方法区的一部分。

    // 输入命令:javap -v HelloWorld.class
    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello World");
        }
    }
    

    Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是「常量池表(Constant Pool Table)」,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,并把里面的符号地址变为真实地址

    运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

    既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

    5.5 字符串常量池

    5.5.1 Quiz

    String s1 = "a";
    String s2 = "b";
    String s3 = "a" + "b";
    String s4 = s1 + s2;
    String s5 = "ab";
    String s6 = s4.intern();
    
    // 问
    System.out.println(s3 == s4); // false
    System.out.println(s3 == s5); // true
    System.out.println(s3 == s6); // true
    
    // ===========================
    
    String x2 = new String("c") + new String("d");
    String x1 = "cd";
    x2.intern();
    
    // 问:1. 打印结果如何?2. 如果调换了最后两行代码的位置呢?3. 如果是jdk6呢
    System.out.println(x1 == x2);
    

    https://blog.csdn.net/qq_43012792/article/details/107428828
    https://liuchenyang0515.blog.csdn.net/article/details/86583262

    // StringTable ["a", "b", "ab"] // hashtable 结构,不能扩容
    public class StringTest {
        // 常量池中的信息,都会被加载到运行时常量池中,这时,a/b/ab
        // 都还只是常量池中的符号,还没有变为 String 类型的对象
        // 执行到用到它的那行代码,才会创建对象 ↓ lazy~
        // ldc #2 会把 a 符号变为 "a" 字符串对象
        // ldc #3 会把 b 符号变为 "b" 字符串对象
        // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    
        public static void main(String[] args) {
            String s1 = "a";
            String s2 = "b";
            String s3 = "ab";
            String s4 = s1 + s2;
            // new StringBuilder().append("a").append("b").toString() {new String("ab")}
            System.out.println(s3 == s4); // false:一个在堆,一个在串池
            String s5 = "a" + "b"; // ldc #4
            // javac 在编译期间的优化,结果已经在编译期确定为 "ab"
            System.out.println(s3 == s5); // true
        }
    }
    

    演示字符串字面量也是延迟成为对象的:

    小结:

    • 常量池中的字符串仅是符号,第一次用到时才变为对象
    • 利用串池的机制,来避免重复创建字符串对象
    • 字符串变量拼接的原理是 StringBuilder(1.8)
    • 字符串常量拼接的原理是「编译期优化」

    5.5.2 空间位置

    JDK 1.8 测试程序加上如下参数 -Use...,会抛出堆溢出异常。

    5.5.3 垃圾回收

    5.5.4 调优

    底层是个 HashSet。

    1. 调整 -XX:StringTableSize=桶个数

    2. 考虑将字符串对象入池

    6. 直接内存

    直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

    • 常见于 NIO 操作时,用于数据缓冲区
    • 分配回收成本较高,但读写性能高
    • 属于 OS 管辖内存,故不受 JVM 内存回收管理

    6.1 NIO

    在 JDK 1.4 中新加入的 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据,如图所示。

    6.2 分配和溢出

    显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置 -Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OOM。

    6.3 回收

    ByteBuffer 的实现类内部(底层就是 Unsafe),使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。

    也可以反射获取 Unsafe 对象(因为直接获取不到)来直接完成 DirectMemory 的分配和回收。

    直接内存不受 JVM 管理,故只能用任务管理器方式查看 JVM 占用内存情况。

  • 相关阅读:
    表单实现仿淘宝搜索应用
    ASCII字符集
    HTTP状态码
    总结get和post区别
    总结Canvas和SVG的区别
    展示github中的页面(Github Pages)
    canvas的beginPath和closePath分析总结,包括多段弧的情况
    12. Integer to Roman
    13. Roman to Integer
    463. Island Perimeter
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/14784673.html
Copyright © 2020-2023  润新知