博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java虚拟机-第一篇-Java内存区域与内存溢出异常
阅读量:6258 次
发布时间:2019-06-22

本文共 7923 字,大约阅读时间需要 26 分钟。

hot3.png

运行时候数据区域

橙色代表java虚拟机运行时候的线程共享的数据区域,绿色代表了运行时候线程的数据隔离区域。

程序计数器

程序计数器占用一小块内存区域,可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作的时候就是通过改变这个程序计数器的值来选择下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖程序计数器来执行。

如果一个线程正在执行java方法,则这个计数器记录的正在执行的虚拟机字节码的指令地址,如果是一个native方法在执行,那么程序计数器的值为空(undifined)。此内存区域是唯一一个在java虚拟机规范中没有规定任何内存溢出情况的区域。

Java虚拟机栈

Java虚拟机栈是线程私有的,他的生命周期和线程生命周期一样,虚拟机栈描述的是Java方法执行的内存模型,每个方法执行的同时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等等信息。每个方法调用执行完的过程,对应着栈针在虚拟机栈中入栈到出栈的过程。

Java内存区域的划分主要分为堆内存和栈内存。所指的栈就是现在所说的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

Java虚拟机规范中规定了两种异常,如果线程请求的栈的深度大于了虚拟机规定的允许的栈的深度,将会抛出StackOverflowErro异常.当Java虚拟机动态扩展的时候无法申请到足够的内存时候,就会抛出OutOfMemoryErro异常。

本地方法栈

本地方法栈和Java虚拟机栈非常的类似,Java虚拟机栈是为虚拟机执行Java方法服务,本地方法栈是为Java虚拟机运行本地方法服务,他们产生的异常种类也是相同的。

Java堆

虚拟机管理内存的最大的区域,Java堆是被所有线程共同管理的一块内存区域,在虚拟机启动的时候创建。这个内存区域的主要目的就是存放Java对象实例,几乎所有的实例都是在堆中进行分配的。

从内存回收角度看,Java堆中还可以分为新生代和老年代。从Java内存分配角度看,线程共享的Java堆中可能划分为多个线程私有的分配缓冲区,不论如何划分,都与存放的内容无关,无论哪个区域,存储的都是对象的实例,无论怎么划分都是为了更好的分配内存和回收内存。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据。

在Hotspot虚拟机中,很多人把方法区当做是永久代,本质上不等价,这里主要是Hotspot虚拟机设计团队把GC分代收集扩展至方法区了,或者说使用永久代来实现方法区而已 ,这样垃圾收集器就可以像管理堆一样来管理方法区了。

Java虚拟机规范对方法区的限制是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定的大小和可扩展以外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是很少见的,这个区域的回收目标是对常量池的卸载和对类型的卸载,尤其是对类型的卸载,条件相当苛刻。

运行时常量池

用于存放编译生成的各种字面量和符号引用,这部分内容将在类加载侯进入方法区的运行时常量池中存放。Java语言并不要求常量一定只有在编译器才能产生,也就是并非预制入class文件中的常量内容才能进入方法区运行时常量池,运行期间产生的常量也可能放入常量池中。

对象的创建

虚拟机每次遇到一个new指令的时候,首先会监测这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化,如果没有,那必须执行相应的类加载过程。

在类加载检查通过以后,接下来虚拟机将为新生对象分配内存,对象所需要的内存大小将在这个类加载完成以后就可以完全确定,为对象分配内存就相当于从Java堆中划分一块内存出来。

对象内存的分配方式

如果Java堆中的内存是绝对规整的,所有用过的内存都放在一边,没有用过的内存放在另外一边,中间存放着一个指针作为分界点,当为对象分配内存的时候,移动指正向空闲一边移动和对象内存大小的相等的距离,这种分配方式就叫做指针碰撞。

如果Java堆中的内存不是规整的,已经使用的内存和空闲的内存相互交错的话,就不可以用指针碰撞方式分配内存,虚拟机必须就必须维护一个列表实例,记录哪些内存可用,在分配的时候从列表中选取一个足够大的空间分配给对象实例,并且更新记录表,这种分配方式叫做空闲列表。

虚拟机采用哪种方式分配内存的时候,取决于Java堆是否规整,Java堆是否规整又由所采用的垃圾收集器时候带有压缩整理功能决定。

对象分配内存的时候可能出现的问题

除了考虑如何划分空间之外,还学要考虑的问题是对象的创建在虚拟机中是否是非常频繁的行为,即使仅仅修改一个指正的行为,在虚拟机进行并发的情况下也不是线程安全的,可能出现正在给对象A分配内存的收,指针还没来得及修改,对象B又同时修改了原来的指针来分配内存。解决这个问题有如下两个方案:

一种是在对象分配内存的时候进行同步处理,另外一种就是对象在分配内存的时候按照线程划分在不同的空间中进行分配内存,即每个线程在Java堆中预先奉陪一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程需要分配内存就在哪个线程的TLAB上进行分配,只有当TALB用完的时候分配新的TLAB的时候才需要同步锁定。虚拟机是否采用TLAB,可以通过**-XX:+/-UserTLAB**参数设定。

内存分配完成以后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作可以提前至TLAB分配的时候进行,这一步骤操作保证了对象的实例字段在Java代码中可以不赋值初始值就可以直接使用。

接下来Java虚拟机还要对对象就行必要的设置,比如这个对象是哪个类的实例,如何才能找到类的原始数据星系,对象的hash码,对象的GC分带年龄等信息。这些信息都放在对象的对象头中。

经过上面的步骤以后,从虚拟机的角度看,一个新的对象就已经产生了,但是从Java程序的角度看,对象创建才刚刚开始,初始化还没执行,所有的字段都还是默认值,所以开始执行构造函数的初始化,从而按照程序员的思想进行初始化。

对象的内存布局

对象在内存中的存储可以分为三块区域,对象头,实例数据,对其填充。

对象头

对象头包括两个部分信息:

第一部分用于存储对象自身运行时候的数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等。这部份数据的长度在32位和64位的系统中的长度分别是32和64位,官方称为“Mark Word”。对象需要存储的运行时数据很多,已经超过了32位和64位的Bitmap结构所能存储的值,但是对象头信息是对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成了额一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会更具对象的状态复用自己的存储空间。

第二部分就是类型指针,即对象指向他类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,并不是所有的虚拟机都必须在对象数据上保留类型执行,换句话说,查找对象的元数据并不一定要通过对象本身。另外,如果对象是个数组,那么在对象头中还必须有一块用于记录数组长度的指针,因为虚拟机可以通过普通的Java对象的源数据信息确定Java对象的大小,但是数据的元数据无法确定数据的大小。

实例数据

实例数组部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型字段的内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来,这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。Java虚拟机的分配策略中相同宽度的字段总是会分配到一起,在满足这个条件的前提下,在父类中定义的变量会出现在子类之前,子类中较窄的变量可能会插入到父类的变量的空隙之中。

对齐填充

对齐填充部分并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用,由于Java中Hotspot VM的自动内存关系系统要求对象的起始地址必须是8字节的整数倍,换句话说,对象大小必须是8字节的整数倍,对象头正好是8字节的整数倍,因此当对象的实例数据没有对齐的时候,需要通过对齐填充来进行补全。

对象的访问定位

Java程序通过操作Java栈本地变量表中的reference数据来操作堆上的具体对象。 目前Java虚拟机对象访问方式有使用句柄和直接指针两种方式。

句柄访问:Java堆中会划分一块内存作为句柄池,存储对象的的句柄地址,句柄中包含了对象的实例数据与类型数据各自具体的地址信息。本地变量表中reference直接指到句柄池,句柄池总的指向类型数据的指针指向方法区的类型数据,指向实例数据的指针指向实例池中的对象的实例数据。

指针访问的方式:Java堆对象的内存布局必须考虑如何防止类型数据相关的信息。 Java本地栈变量表中的reference直接指向Java堆中的对象实例数据和对象实例数据类型的指针,对象类型实例数据的指针指向方法区中对象类型数据。

使用句柄访问的好处就是reference中存储的是稳定的句柄地址,在对象呗一定(垃圾收集是移动对象是非常普遍的行为)最会改变句柄中规定实例数据的执行,而reference本身不需要修改。

使用指针访问的最大的好处就是速度快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常的频繁,一次这类的开销积少成多侯也是一项非常可观的执行成本。

内存溢出异常

Java堆异常

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收继承消除这些对象,那么对象数量到达对打堆的容量的时候就会产生内存溢出异常。通过-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在内存出现溢出异常是Dump出当前的内存对转储快照以便于时候进行分析。

package basic;import java.util.*;import java.util.concurrent.ConcurrentHashMap;/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/public class Demo {    public static void main(String[] args) {       List list = new ArrayList();       while (true){           list.add(new Object());       }    }}

输出结果

java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid15000.hprof ...Heap dump file created [2314303873 bytes in 10.848 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space	at java.util.Arrays.copyOf(Arrays.java:2245)	at java.util.Arrays.copyOf(Arrays.java:2219)	at java.util.ArrayList.grow(ArrayList.java:242)	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)	at java.util.ArrayList.add(ArrayList.java:440)	at basic.Demo.main(Demo.java:18)	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)	at java.lang.reflect.Method.invoke(Method.java:606)	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

虚拟机栈和本地方法栈溢出

由于在Hotspot虚拟机中并不区分本地方法栈和虚拟机栈,因此栈容量只需要由-Xss参数设置就可以。

如下代码

/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/public class Demo {    public static void main(String[] args) {        test();    }    public static void test(){        test();    }}

输出结果

Exception in thread "main" java.lang.StackOverflowError	at basic.Demo.test(Demo.java:19)	at basic.Demo.test(Demo.java:19)	at basic.Demo.test(Demo.java:19)

Java虚拟机栈包括了两种异常:

如果线程请求的栈的深度大于了虚拟机允许的栈的深度的最大值,那么将会出现StackOverflowError异常。

如果虚拟机扩展栈的时候如果申请到足够的内存的话,则抛出OutOfMenmoryError异常。

在本地测试中,使用-Xss参数介绍栈内存容量,结果抛出StackOverflowError异常,异常出现时输出堆栈的深度相应缩小。

定义了大量的本地变量,增大了此方法中本地变量表的长度,结果还是抛出StackOverflowError异常。异常时输出栈堆深度相应缩小。

在单线程情况下,无论是栈帧太大还是讯积极容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

方法区和运行时常量池溢出

运行时常量池是方法区的一部分,因此这两个区域的测试只能放到一起测试。

String.intern()方法是一个native方法,他的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则将这个string对象加入到常量池中,并返回此string对象的引用。

/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/public class Demo {    public static void main(String[] args) {        //使用list保持常量池的引用,避免Full GC回收常量池的行为        List 
list = new ArrayList
(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } }

Java1.7以前会发生内存溢出异常,Java1.7以后就不会发生异常了。因为1.7以前intern如果字符串是首次出现,则intern方法会把字符串添加到常量池中,返回此常量池的应用,否则直接返回常量池的引用。

请继续看

package basic;import java.util.*;import java.util.concurrent.ConcurrentHashMap;/** * @program: demo * @description: demo * @author: lee * @create: 2018-09-10 **/public class Demo {    public static void main(String[] args) {        //使用list保持常量池的引用,避免Full GC回收常量池的行为        String str1 = new StringBuilder("计算机").append("软件").toString();        System.out.println(str1.intern()==str1);        String str2 = new StringBuilder("计算机").append("软件").toString();        System.out.println(str2.intern()==str2);    }}

输出结果

truefalse

为什么会出现这样的情况呢,原因是在Java1.6以前,intern方法会把首次遇到的字符串实例复制到永久代中并且返回永久代中的字符串实例引用,而创建对象又是在Java堆中创建的,所以必然不是同一个引用。在Java1.7以后,intern的实现不会再复制实例,只是在常量池中记录首次出现的字符串的实例,因此intern 返回的是引用和由stringbuilder创建的那个字符串实例是同一个值。第二次的时候intern返回了第一个值的应用,stringbuilder又新生成了一个对象,所以对象实例是不同的。

方法区用于存放Class相关的信息,如类名,访问修饰符,常量池,字段描述,方法描述等等。对于这个区域的测试基本思路就是产生大量的类去填充方法区,直到方法区溢出。

常用参数

-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M -vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了

-Xms128m JVM初始分配的堆内存

-Xmx512m JVM最大允许分配的堆内存,按需分配

-XX:PermSize=64M JVM初始分配的非堆内存

-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配

转载于:https://my.oschina.net/jiansin/blog/2222078

你可能感兴趣的文章
HTML5本地存储-localStorage如何实现定时存储
查看>>
LAMP之Centos6.5安装配置Apache(二)
查看>>
Tomcat集群
查看>>
shell脚本中输出带颜色字体实例分享及chrony时间同步
查看>>
简单计时
查看>>
面试心得
查看>>
linux系统时间同步,硬件时钟和系统时间同步,时区的设置
查看>>
CentOS下载包格式说明
查看>>
VMware Vsphere 6.0安装配置 二安装vcenter server程序
查看>>
关于CISCO asa5510防火墙端口映射配置
查看>>
2012年6月美国最佳虚拟主机提供商TOP12性能评测
查看>>
monkey详细介绍之二
查看>>
两列布局之左边固定宽度,右边自适应(绝对定位实现方法)
查看>>
4,gps信号与地图匹配算法
查看>>
python print的用法
查看>>
之字形打印矩阵
查看>>
我的世界之电脑mod小乌龟 —— 方位上的操作 lua函数集
查看>>
游戏方案
查看>>
在 Linux 下搭建 Git 服务器
查看>>
StackExchange.Redis Client(转载)
查看>>