JVM第二章Java内存区域与内存溢出异常笔记

237 阅读8分钟

【Java运行时数据区】 包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池、直接内存

【程序计数器】: 当前线程执行字节码的行号指示器,每条线程都有一个独立的程序计数器。 特点: 1.线程隔离性(线程私有)。 2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。 3.执行native本地方法时,程序计数器的值为空。因为是通过JNI方法调用C/C++方法,并没有相应的字节码,自然是空的。 4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。 5.是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。 了解: 线程按顺序执行为何还要计数器?java代码经过javac编译成class文件后,JVM通过解释器将按照顺序读取字节码翻译成机器代码进行操作。在多个线程的情况下,可能因为CPU时间片耗尽线程挂起,因此它通过程序计数器来记录某个线程的字节码执行位置,方便下次从挂起的地方继续执行。 【Java虚拟机栈】 描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。 特点: 1.线程私有。 2.规定异常有两种:1.线程请求的栈的深度(可以理解为空间单位)大于虚拟机所允许的深度,将抛出StackOverflowError异常;2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。 了解: 栈帧可以理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另一部分是操作数栈,用来存放操作数。 我们通常将的堆栈的栈就是虚拟机栈中局部表量表。 局部变量表:方法中定义的局部变量以及方法的参数就存放在这张表中,主要是基本数据类型、引用类型、returnAddress类型(函数返回地址)等。局部变量表所需的内存空间在编译期完全确定(double,long占2 slot空间单位,其它1 slot). 操作数栈:用来存放操作数;用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。 动态连接:简单理解是使对象能调用实例方法的过程称为动态链接。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。 方法返回地址:就是当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令。返回有两种:当方法执行异常退出(因为通过异常处理器执行不会保留程序计数器的值),方法执行完正常退出(返回程序计算器的值)。 Java 字节码指令的操作数存放在操作数栈中,当执行某条带 n 个操作数的指令时,就从栈顶取 n 个操作数, 然后把指令的计算结果(如果有的话)入栈。当我们说 JVM 执行引擎是基于栈的时候,其中的“栈”指的就是操作数栈。由于操作数栈是内存空间,所以字节码指令不必担心不同机器上寄存器以及机器指令的差别,从而做到了平台无关。 【本地方法栈】: 跟虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。也会抛出StackOverflowError异常,OutOfMemoryError异常。 【Java堆】: 在虚拟机启动时创建,用于存放对象实例,是Java虚拟机管理内存中最大的一块,所有线程共享。 通过-Xms设置初始堆大小,-Xmx设置可扩展的最大堆大小。是垃圾回收的主要区域。 现在收集器主要采用分代收集算法,因此堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。 堆无法扩展时,抛出OutOfMemoryError异常。 【方法区】 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;所有线程共享。处理不需要连续内存和可以固定扩展外,还允许不实现垃圾收集;但也可以收回这区域的回收目标主要是对常量池的回收和对类型的卸载, GC很少在这个区域执行。 【运行时常量池】 是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外, 还有一项是常量池(Const Pool Table),用于存放编译期生成的各种字面量和符号引用。 它具有动态性,并不要求常量必须在编译期间产生,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。 当方法区无法满足内存分配需求时,抛出OutOfMemoryError 【直接内存】 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 因为避免了在Java堆和Native堆中来回复制数据,提高了性能。 当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

【java对象创建及访问】 主要涉及Java栈、Java堆、方法区三个内存区域。 java创建对象,如:Object obj = new Object(); 引用变量obj存放在java虚拟机栈中局部变量表中,作为一个reference类型数据;而obj的实例则在java堆上。同时Java堆中还包含查找此对象信息的地址信息. 通过引用变量obj访问Java堆中的对象有两种方式: 句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量obj中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据(java堆)和类型数据各自的具体地址(方法区)。 优点:引用变量obj中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只要修改句柄中的实例数据指针,而不用修改引用变量obj的值。

引用访问:引用变量ojb中直接存储的就是对象堆地址。(sun hotspot采用这种) 优点:最大好处就是速度快,它节省了一次指针定位的时间开销。

【内存溢出异常】 内存溢出的目的时让我们在工作中遇到内存溢出时能根据异常信息快速判断时哪个区域内存溢出,判断可能是怎样的代码导致溢出从而分析结局。 Java堆溢出 通过分析堆快照,判断是内存溢出还是内存泄露,如果是内存泄露则再进一步判断泄露对象是怎么跟GC Roots关联导致垃圾收集器无法自动回收。然后通过泄露对象的信息和GC Roots引用链就比较准确找到泄露代码位置。 如果是内存溢出则表示内存中的对象确实必须存活那就检查虚拟机的堆参数(xmx与xms)对比物理内存看是否能调大,或者代码上检查某些生命对象是否持续时间过长,尝试减少程序运行期内存消耗。 Java虚拟机栈和本地方法栈溢出 一般保stackoverflowError异常,多线程的情况下则报outOfMemoryError。在虚拟机默认参数下,栈深度一般可达到1000-2000,正常方法调用没问题。如果是多线程过多导致,则考虑减少线程或更换64位虚拟机,自就是减少最大堆和减少栈容量换取更多线程。 运行时常量池溢出 常量池分配在方法区中,通过-XX:PermSize 和-XX:MaxPermSize限制方法区的大小从而限制常量池容量。一般报错误为:java.lang.OutOfMenoryError:PermGen space 方法区溢出。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。一般场景有使用GClib字节码增强外,常见的还有大量JSP动态生成。