深入理解JAVA虚拟机 自动内存管理机制
运行时数据区域
其中右侧三个一起的部分是每个线程一份,左侧两个是所有线程共享的。
程序计数器(Program Counter Register)
英文名称叫Program Counter Register。如果翻译为程序寄存器更加合理。
因为这块内存区域很小,功能也类似于寄存器。所以还是翻译寄存器比较靠谱。
在虚拟机的概念模型中(仅仅是概念模型,虚拟机可以通过一些更高效的方式来实现),字节码解释器会通过改变程序计数器的值来选取下一条需要执行的字节码指令。
因为CPU是要轮转的,在切换回来之后,Java能够找到下一条要执行的指令。
每一个线程会有一个独立的程序计数器。
线程执行Nativan方法时,计数器记录为空(Undefined)
唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域
线程私有,生命周期跟线程一样。
Java虚拟机栈(Java Virtual Machine Stacks)
线程私有,生命周期跟线程一样。
用于描述Java方法执行的内存模型:
一个栈帧代表一个方法的执行内存模型:入栈和出栈分别代表方法被调用和方法执行完成。
一个栈帧包含:局部变量,操作数栈,动态链接,方法出口等。
重点说局部变量:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(Object reference)和字节码指令地址(returnAddress类型)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
因为所有类型占用的内存大小都是可知的,所以局部变量的内存空间是在编译器完成分配的,运行期不会改变局部变量表的大小。
如果线程请求的栈深度(个人理解:方法调用的深度)大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩展(个人理解:虽然局部变量表的空间在编译器就固定了,但是其他几个可能会动态扩展),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks)与Java虚拟机栈所发挥的作用非常类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
同样可能会抛出StackOverflowError和OutOfMemoryError异常。
Java堆(Java Heap)
Java堆(java heap)是Java虚拟机所管理的内存中最大的一块
它是被所有线程共享的一块内存区域,在虚拟机启动时创建
几乎所有的对象实例都存放在这里。之所以说几乎,是因为:在java虚拟机规范中描述:所有的对象实例都要在堆上分配。但是由于JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致"所有的对象实例都要在堆上分配"不那么绝对了。
Java堆是垃圾收集管理的主要战场。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的 磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。(通过-Xmx和-Xms控制)
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area)
方法区存储的是类的一些基本信息,如类的常量池、静态变量,即时编译器编译后的代码等。【标准说法是:类及其父类的全限定名(java.lang.Object没有父类)、类的类型(Class or Interface)、访问修饰符(public, abstract, final)、实现的接口的全限定名的列表、常量池、字段信息、方法信息、静态变量、ClassLoader引用、Class引用】
可以这样理解,如果定义了一个类,其中有一个静态变量,那么在new这个类的时候,静态变量会放在方法区,实例的其它部分会放在Java堆;如果又new了这个类,那么方法区的静态变量不变,在Java堆里面会有一块新的内存放实例。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用(如编译器生成的static final常量字面量,方法表中的方法名索引 描述符索引 符号引用等),这部分内容将在类加载后存放到常量池中。
运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)
我的理解:运行时动态扩展常量池可以把一些不经常变化的数据缓存起来,和String的缓存池一样,不知道String的缓存是不是存放在这里。
直接内存(Direct Memory)
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
对象访问
对象访问会涉及到Java栈、Java堆、方法区这三个内存区域。
如下面这句代码:
Object objectRef = new Object();
假设这句代码出现在方法体中,"Object objectRef" 这部分将会反映到Java栈的本地变量中,作为一个reference类型数据出现。而"new Object()"这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定。另外,在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型存储在方法区中。
reference类型在java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过那种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针。
句柄访问方式:java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
指针访问方式:reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。
这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式(直接指针访问)。
java堆溢出
实验:
实验代码:
- public class OutOfMemoryTest {
- static class OOMObject{
- }
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- List list = new ArrayList();
- while(true)
- {
- list.add(new OOMObject());
- }
- }
- }
设置JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
错误信息:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid568.hprof ...
Heap dump file created [27885730 bytes in 0.090 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at jvm.OutOfMemoryTest.main(OutOfMemoryTest.java:16)
使用MemoryAnalyzer查看dump:
OOM(OutOfMemory)是最长江的内存溢出。
java.lang.OutOfMemoryError: Java heap space代表堆内存溢出。
这种情况,一般要确认是内存泄漏(Memory Leak)还是内存溢出(Memory OverFlow)。
内存泄漏:对象的使用导致没有被GC。
内存溢出:内存不足。需要调整JVM参数。
虚拟机栈和本地方法溢出
在Hotspot中不区分虚拟机栈和本地方法栈,所以调整他们两个的参数都是-Xss。
经过我的实验,我发现,当由于方法嵌套循环调用导致的溢出,只会跟-Xss的配置有关,没有线程请求的栈深度(个人理解:方法调用的深度)大于虚拟机所允许的深度这种说法。至少我将-Xss调整到100M也没有发现。
测试代码:
- package jvm;
- public class StackOverFlowTest {
- private long stackLenth = 0;
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- StackOverFlowTest s = new StackOverFlowTest();
- try {
- s.testStack();
- } catch (Throwable e) {
- System.out.println(s.stackLenth);
- e.printStackTrace();
- }
- }
- private void testStack()
- {
- long l = 999999999999999L;
- stackLenth ++;
- testStack();
- }
- }
在10M的时候,报错信息:
java.lang.StackOverflowError
at jvm.StackOverFlowTest.testStack(StackOverFlowTest.java:21)
在100M的时候,报错信息:
java.lang.StackOverflowError
at jvm.StackOverFlowTest.testStack(StackOverFlowTest.java:21)
ps:即使我把内存调整到2G,stackLenth还在增加。证明不是因为达到了"线程请求的栈深度"而导致的StackOverflowError。
作者的结论:
在单线程模式下,不论是栈帧太长,还是虚拟机栈容量太小,都只能抛出StackOverflowError。
在多线程模式下,通过不断地建立线程到时可以抛出OutOfMemoryError异常。
运行时内存池异常和方法区内存异常
运行时常量池存储在方法区中。所以他们的异常都是一样的。方法去的jvm参数如:-XX:PermSize=10M -XX:MaxPermSize=10M 注意,和上面不一样,这里是等号。
方法区的内存溢出报错为:java.lang.OutOfMemoryError: PermGen space
深入理解JAVA虚拟机 自动内存管理机制的更多相关文章
- [深入理解Java虚拟机]<自动内存管理>
Overview 走近Java:介绍Java发展史 第二部分:自动内存管理机制 程序员把内存控制的权利交给了Java虚拟机,从而可以在编码时享受自动内存管理.但另一方面一旦出现内存泄漏和溢出等问题,就 ...
- 深入java虚拟机学习 -- 内存管理机制
前面说过了类的加载机制,里面讲到了类的初始化中时用到了一部分内存管理的知识,这里让我们来看下Java虚拟机是如何管理内存的. 先让我们来看张图 有些文章中对线程隔离区还称之为线程独占区,其实是一个意思 ...
- JAVA之自动内存管理机制
一.内存分配 1.JVM体系结构 2.运行时数据区域 3.内存分配二.内存回收 1.垃圾收集算法 2.垃圾收集器三.相关参考一.内存分配JVM体系结构 在了解自动内存管理的内存分配之前,我们先看下JV ...
- java虚拟机(一)——内存管理机制与OOM异常
一 java内存区域与内存溢出异常(OOM) 1)运行时数据区域划分 1.程序计数器(Program Conuter Register) 程序计数器是一块较小的内存空间,它是当前线程执 ...
- 深入理解JVM(一) -- 自动内存管理机制
Java运行时数据区域分为:程序计数器,虚拟机栈,本地方法栈,Java堆,方法区,运行时常量池,直接内存,结构如下: 1.程序计数器: 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示 ...
- 深入理解java虚拟机,内存管理部分
1,对象回收前会调用finalize()方法,尝试自救,只能调用一次 2,上面横向对比c++的析构函数,但是java有良好的内存管理,而且try/catch做得比较好 3,回收一个常量,1,对象的实例 ...
- 深入理解Java虚拟机(自动内存管理机制)
文章首发于公众号:BaronTalk 书籍真的是常读常新,古人说「书读百遍其义自见」还是很有道理的.周志明老师的这本<深入理解 Java 虚拟机>我细读了不下三遍,每一次阅读都有新的收获, ...
- 【深入理解Java虚拟机】自动内存管理机制——垃圾回收机制
Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生 ...
- 【深入理解Java虚拟机】自动内存管理机制——内存区域划分
Java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.C/C++程序员既拥有每一个对象的所有权,同时也担负着每一个对象生 ...
随机推荐
- 第四章 SpringCloud之Eureka-Client实现服务(Jpa,H2)
1.pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...
- 使用 Supervsior 守护进程
概述 一般来说,在终端开启的服务,如果退出终端的话,就会自动关闭服务.这个时候需要守护这个服务的进程. Supervisor 是一个用 Python 写的进程管理工具,可以很方便的用在 UNIX-li ...
- 阶段3 2.Spring_09.JdbcTemplate的基本使用_2 JdbcTemplate的概述和入门
先看这张图 1.spring中的JdbcTemplate JdbcTemplate的作用: 它就是用于和数据库交互的,实现对表的CRUD操作 如何创建该对象: ...
- 阶段3 2.Spring_07.银行转账案例_8 基于接口的动态代理回顾
创建 Producer 生产者的类 创建标准.这标准就是接口 创建消费者的类 以前的方式,客户直接找生产厂家 代理商出现以后,怎么去联系代理商 动态代理 第一个参数是ClassLoader 第二个参数 ...
- 阶段3 2.Spring_06.Spring的新注解_5 spring的新注解-PropertySource
数据库的链接 次数是写死的 新建配置文件 定义成员变量 value注解实现 与配置文件的key对应 PropertySource 要想让spring去读取这个配置文件 resource编译后都跑到了. ...
- django 如何传递id 参数
urls.py 注意这里的bid
- 问题:anaconda: command not found
打开Terminal 1.使用命令:sudo apt install vim 安装vim文本编辑器2.使用命令:vim ~/.bashrc 修改环境变量 3.在文本最后添加命令:export PATH ...
- Redis 入门 3.1 热身
3.1 热身 1. 获得符合规则的键名列表 KEYS pattern pattern 支持 glob 风格通配符格式 语言 字符组 ? 匹配一个字符 * 匹配任意个(包括0个)字符 [] 匹配括号间的 ...
- TCP/IP笔记——TCP特点、首部格式、滑动窗口
这次总结一下TCP相关的知识. TCP主要特点 面向连接:在通信前必须建立连接(只是逻辑上存在,而不是物理连接) 只能有两个端点:即只能一对一通信(所以通常p2p是用UDP实现的) 提供可靠交付服务: ...
- Vue-cli 鼠标监听事件之滚动条
<template> <div class="scroll"> <div class="scroll-div-outer&quo ...