深入理解Java虚拟机-第二章、Java内存区域与内存溢出异常

参考链接

第二章、Java内存区域与内存溢出异常

一、第一章一些知识点

1、为什么java可以一次编写,到处运行?

Java代码编译之后的,不是能被硬件直接运行的代码,而是一种"中间码"-字节码,然后不同的硬件平台上安装不同的虚拟机,由JVM把字节码再翻译成所对应的硬件平台可以执行的代码。

2、什么是JDK、JRE

JDK

什么是JDK? java程序设计语言、java虚拟机、java API类库三部分称为JDK。绝大多数的java程序员只接触java程序设计语言、java API类库,而不知java虚拟机运行原理。

JRE

什么是JRE?java API类库中的javaSE API和java 虚拟机称为JRE(java runtime Environment)。java平台版本有javaee、javase、javame,其中javase 为(Java Platform,Standard Edition)标准版本。

二、运行时数据区域

在这里插入图片描述

1、程序计数器(线程私有,可以看成当前线程的行号指示器)

程序计数器是一块较小的内存空间,它可以看成当前线程的行号指示器。每个线程都有一个独立的程序计数器。程序计数器中存放下一条指令存放的地址,字节码解释器工作是就是通过改变这个计数器值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖这个计数器来完成。

2、虚拟机栈(线程私有)

虚拟机栈也是线程私有的,虚拟机栈是用来描述java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

可能回报StackOverflowError和OutOfMemoryError错误

局部变量表

存放了编译器可知的8种基本数据类型、对象引用。

3、本地方法栈(线程私有)

与虚拟机栈作用类似,虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈为虚拟机使用的是Native方法服务。(native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,其他方法运行在虚拟机栈中,本地方法运行在本地方法栈中,native方法主要用于加载文件和动态链接库,与本地平台有关,可移植性不太高。)

本地方法栈也可能抛出StackOverflowError和OutOfMemoryError错误。

4、Java堆(线程共享)

java堆(java Heap)是虚拟机所管理的内存最大一块,是被所有线程共享的一块内存区域。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例和数组都在堆上分配内存,堆也是垃圾收集器管理的主要的区域。

Java堆是垃圾收集器管理的主要区域,因此又被称为“GC堆”。现在的收集器基本都使用分代收集算法,所以堆还可以细分为:新生代和老年代,再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

堆的大小可以通过-Xmx和-Xms控制,当堆中没有内存完成实例分配,就会抛出OutOfMemoryError异常。

5、方法区(线程共享)

方法区与堆一样是各个线程共享的内存区域,但不是描述java方法执行的内存模型,而是用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。编译期生成的各种字面量和符号的引用在类加载后进入方法区的常量池中存放。

上面说到的类信息包括:类的版本、字段、方法、接口等

对于习惯在HotSpot虚拟机上开发、部署程序的开发 者来说,很多人都愿意把方法区称为"永久代",本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC
分代收集扩展到方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾回收器就可以像管理Java堆一样管理这部分内存。

当方法区无法满足内存分配需求的时候,将抛出OutOfMemoryError异常。

二、HotSpot对象探秘

1、对象的创建

java程序语言创建一个对象仅仅使用一个关键字new,而在虚拟机中,虚拟机碰到一个new指令,对象的创建过程包括以下几步:

  • 1、先检查这个类是否已被加载,没有则执行相关的类加载过程。首先先检查这个指令的参数是否能在常量池中定位到这个类的符号的引用,并且检查这个负荷引用代表的类是否被加载、解析、和初始化过,如果没有,那先执行相应的类加载过程。

  • 2、在类加载通过后,虚拟机为新生对象分配内存。类加载完成后为对象分配的内存大小就确定了。

    (1)、如果内存是规整的,已使用的内存在一边,未使用的在另一边,中间是分界指针。为对象分配内存就是将指针想空闲区域移分配空间大小的位置。该情况称为指针碰撞
    (2)、如果内存空间不规则,空闲和使用空间交错。那么需要一个表进行记录空间使用情况。分配空间时,在表中取一块足够大的内存进行分配,然后更新该表。这种情况称为空闲列表

  • 3、内存分配完后,虚拟机需要把内存的初始值都置为0,对象头中记录类的信息、对象哈希码,使用TLAB时,该操作可以在分配TLAB时进行。这样保证在程序执行时不给变量赋初值可以直接使用数据类型对应的0值。接下来虚拟机会对对象进行一些设置。如该对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象GC时处于的年龄带等,都会记录在对象头中。

  • 4、以上的步骤都完成后,虚拟机层面上对象已经完成,但是程序层面上创建对象才刚刚开始。init方法还没有执行。所有初始值还都是0,需要按程序员的意愿全部初始化后,一个对象才算真正完成。

选择哪种分配方式由Java堆是否规整决定,而java堆是否规整又取决于垃圾收集器是否带有压缩整理功能,因此,在使用Serial、ParNew等带Compact过程的收集器时,采用指针碰撞。而在使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

存在的问题:内存分配不安全

对象在虚拟机中创建是一个非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发的情况下也并不是线程安全的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

解决方案
  • 对内存分配动作进行同步,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • 每个线程预分配一小块内存,这样哪个线程要分配内存满就在自己那个线程中分配。这块内存被称为本地线程分配缓冲。

2、对象在虚拟机中的内存布局

对象在内存中的存储分为3块区域:对象头,实例数据, 对齐填充。

对象头:HotSpot虚拟机的对象头包含两部分信息:

对象头详解

  • 存储对象自身运行时的数据。如哈希码,GC分代年龄,线程持有的锁(一个对象上可能有锁,其他线程再去获取这个对象的锁可能就要看对象头是否已经有锁),锁状态,偏向线程ID等。这部分空间在32位和64位虚拟机上分别对应32bit和64bit。官方称为“mark word”。
    它被设计为非固定的数据结构,在不同的状态下会复用自己的空间。例如32位的虚拟机里,如果对象处于未被锁定的状态下,那么mark word的32bit可以分别用于:25bit存储哈希码,4位存储对象分代年龄,2位存储锁状态,1位固定为0.
    下图显示的是在32位虚拟机上中,在对象不同状态的时候,mark word各个比特位区间的含义。
    在这里插入图片描述
  • 存储对象的类型指针,及指向类元数据的指针。虚拟机通过这个指针来确定该对象是哪个类的实例。
    但是并不是所有对象都需要存储类型指针,换言之并不需要通过对象本身查找它的元数据。另外如果对象是一个java数组,还对象头还需要存储数组的大小,因为普通对象虚拟机可以通过元数据确定对象的大小,但是数组不行。
实例数据

对象的实例数据是对象真正存储的有效信息,继承下来的父类的字段和子类定义的字段都会被记录下来。字段存储的顺序取决于(FieldAllocationStyle)参数和程序中定义的顺序。虚拟机默认的顺序是把宽带相同的变量放在一起。然后父类放在子类之前。

对齐填充

对齐填充不是必须的,只是起一个占位符的作用。由于虚拟机规定内存存储是8字节的整数倍,所以空间有空闲的用填充补齐。

3、对象的访问定位

java对象访问,是使用栈上的reference数据指向堆上的对象进行访问的。进行访问的方式一般有两种:1.句柄访问。2直接指针。

  • 通过句柄访问:如果使用句柄访问,java堆上会分配一个句柄池,reference此时存储的句柄池的地址。句柄池里存放着指向实例数据的指针和方法区上数据类型数据的指针。该方法的好处是reference的值稳定,当对象移动时(垃圾回收器工作时经常使对象移动),只改变实例对象的指针,reference的值不用变。

在这里插入图片描述

  • 直接指针访问:此时reference中存储的是实例的地址。而实例中有指向方法区上对象类型数据的指针。该方法的好处是,节省了一次指针定位消耗的时间开销。由于java对象的访问很频繁,积累下来的时间也是很大的成本。hotspot 虚拟机使用的改种方式。

在这里插入图片描述

四、内存溢出异常如何分析

虚拟机内存中几个数据区域除了程序计数器,方法区、虚拟机栈、本地方法栈、堆四个地方均可能发生内存溢出(OutOfMemoryError)。OutOfMemoryError简称OOM,OOM发生时如何判断是哪个区域的内存溢出?什么样的代码可能会导致这些区域内存溢出?溢出异常该如何处理?

1、Java堆溢出

先设置运行vm参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+HeapDumpOnOutOfMemoryError
其中: -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常Dump出当前的内存转储快照。
-Xms20M -Xmx20M 堆的最小值 堆的最大值

在这里插入图片描述


public class HeapOOM 
{
	static class OOMObject {}
	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<OOMObject>();
		while(true)
		{
			list.add(new OOMObject());
		}
	}
}

在这里插入图片描述
打开MemoryAnalyzer对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的。到底是内存泄漏还是内存溢出。
如果所内存泄露,可以进一步通过工具查看泄漏对象到GC Roots的引用链,于是就能得到泄漏对象是通过怎么样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们的,从而可以比较准确定位处泄漏代码的位置。
如果不存在泄露,也就是内存中的对象必须存活着,那就应该检查虚拟机的堆参数,与物理内存比较是否还可以设置大。

file–open–heap dump,可以查看不同对象占堆中的大小比例。
  点击Dominator Tree可以进一步查看大对象,它们是最有可能内存泄露的地方。
在这里插入图片描述

2、虚拟机栈和本地方法栈溢出分析

在hotspot虚拟机上,不区分虚拟机栈和本地方法栈。所以使用-Xoss(设置本地方法栈)参数无效,栈容量只由 -Xss设置。

栈上存在两种异常:

  • 1、线程申请的栈的深度大于虚拟机允许的大小,导致StackOverflowError异常。
  • 2、虚拟机扩展栈时申请不到足够的内存,导致的OutOfMemoryError异常。
/*
 * VM args: -Xss128k
 * */
public class JavaVMStackSOF{
	private int stackLength = 1;
	private void stackLeak() {
		stackLength++;
		stackleak();
	}
public static void main(String[] args) {
	JavaVMStackSOF sof = new JavaVMStackSOF();
	try {
		sof.stackLeak();
	} catch (Exception e) {
		e.printStackTrace();
	}
	}
}

书上说:实验表明,在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

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

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串就返回字符串常量池中该String对象,否则将此String对象的包含的字符串添加到常量池中,再返回引用。
在JDK1.6及之前,由于常量池在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制常量池大小。

/*
*/
public class RuntimeConstantPoolOOM
{
	public static void main(String[] args)
	{
		List<String> list = new ArrayList<>();
		int i = 0;
		while(true)
		{
			list.add(String.valueOf(i++).intern());
		}
	}
}

会报OutOfMemoryError,后面跟的提示信息是“PermGen space”,说明常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

在JDK1.6中,intern()方法会把首次出现的字符串复制到永久代中,返回的也是永久代中这个字符串实例的引用,而在JDK1.7中,intern()实例不会再复制实例,只是在常量池中记录首次出现的实例的引用。

4、方法区

方法区中存放Class相关信息:如类名、访问修饰符、常量池、字段描述、方法描述。在一些框架中如Spring会使用cglib(CGLIB 是一个强大的,高性能,高质量的Code生成类库,开源项目)的字节码技术动态生产很多代理类。这些代理类需要足够的方法区去装载,否则容易出现OutOfMemoryError异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值