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

简单来说这里说的就是JVM的内存区域。

怎么理解JVM的内存区域呢?

答:因为JVM就是一个进程,既然是进程所有就有自己虚拟地址空间,只是他和普通的进程不一样,JVM需要做的事情是执行java代码,所以说他的虚拟地址空间划分和管理就是有自己独特的个性。但是总的来说他还是虚拟地址空间,尽管不一样但是也还是很类似的。

对JVM内存有了大致的认识以后接下来就可以详细来看看他其中都划分了哪些的区域。

这是总体的一张图

下面一个一个介绍

1.程序计数器

是一块较小的内存空间。主要的功能是用来标记当前线程所执行的字节码的位置。(书中原话:程序计数器是当前线程所执行的字节码的行号指示器),所以到底是记录的是当前线程执行到的字节码所在的地址行还是具体执行到哪个字节码了,我也很疑惑,但是总体来说他就是为了标记当前线程执行到了哪个位置。他是线程私有的数据区域,每个线程都有自己的程序计数器。

为什么java中的线程有自己的程序计数器呢而C++没有呢?

答:因为java代码都是由jvm来管理执行的,尽管他线程实现底层调用的是操作系统的方法实现的线程,但是我们在JAVA中写的线程的代码还是需要JVM来执行的,所以JVM就需要知道每次线程执行到了哪个位置,所以就有程序计数器。而在C++中直接使用的就是系统的创建线程的方法,他有自己寄存器之类的东西来记录线程上下文切换的东西,所以能够在切换后知道自己执行到哪里了,所以就不需要程序计数器了。

备注:如果线程执行的是Java的方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是netive方法的话,这个计数器值为空。并且程序计数器这个区域是java虚拟机规范中唯一一个没有OutOfMemoryError情况的区域。

2.虚拟机栈和本方法栈

在说这两个区域时,首先强调一个概念:函数栈帧的创建和开辟(这里就是使用的栈内存)。

虚拟机栈主要是JAVA方法执行的过程中创建栈帧的地方,描述的是Java方法执行的内存。虚拟机栈是线程隔离的,每个线程都有自己独立的虚拟机栈,所有线程一起使用JVM的虚拟机栈的总大小。在jvm的规范里面,对这个区域有两种异常。

1.虚拟机栈的StackOverflowError

若单个线程的请求栈深度大于虚拟机允许的深度,则会抛出StackOverflowError,JVM会为每个线程的虚拟机栈分配一定内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前的线程虚拟机栈的内存耗尽。典型的就是函数的递归调用

2.虚拟机栈的OutOfMemoryError

这个是异常指的是当前JVM的整个虚拟机栈的内存耗尽,并且无法在申请到新的内存时候抛出的异常,而不是单个线程的虚拟机栈。JVM没有提供设置整个虚拟机栈占用的内存的配置参数,虚拟机栈最大总内存大致等于:jvm进程占用的内存-最大堆内存-最大方法区内存-程序计数器内存(可以忽略不计)-JVM进程本身其他代码消耗的内存。当虚拟机栈能够使用的最大内存被耗尽以后,便会抛出OutOfMemoryError,可以通过不断的创建线程来模拟这种异常。

 

本地方法栈的功能和特点类型与虚拟机栈,也有线程的隔离的特点和抛出StackOverflowError和OutOfMemoryError异常。但是本地方法栈是jvm执行native方法时候创建的栈帧,而虚拟机栈服务的是JVM执行的java方法。如何去服务native方法?native方法使用什么语言实现?怎么组织像栈帧这种为了服务方法的数据结构?虚拟机规范并未给出强制规定,因此不同的虚拟机实可以进行自由实现,我们常用的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。

 

3.堆和方法区

堆:

堆是虚拟机管理的内存中最大的一块,主要用于存放对象(不是所有的对象都会分配在堆上),是垃圾管理器(GC)管理的主要的目标空间。他有如下的特点

1.从内存回收的角度来看,由于现在的收集器都采用分代收集的算法,所以Java堆可以进一步被划分为:新生代和老年代,并且在进一步细致划分的话,有Eden空间,From Survivor空间,To Survivor空间。为什么这么详细的划分,是为了更好的回收内存,或者更快的分配内存(这里后续降到垃圾回收的时候在细说)

2.线程共享的区域,每一个不同的线程都可以拿到堆上相同的对象

3.堆占用的内存并不要求物理连续,只需要逻辑连续即可

4.堆的生命周期随虚拟机的启动而创建

5.堆一般实现成可扩展内存大小,使用“-Xms”与“-Xmx”控制堆的最小与最大内存,扩展动作交由虚拟机执行。但由于该行为比较消耗性能,因此一般将堆的最大最小内存设为相等

当堆无法分配对象内存且无法再扩展时,会抛出OutOfMemoryError异常。但是一般来说,堆无法分配对象时会进行一次GC(GC详细后面在说),如果GC后仍然无法分配对象,才会报内存耗尽的错误。

方法区:

方法区,也称非堆(Non-Heap),又是一个被线程共享的内存区域。其中主要存储加载的类字节码、class/method/field等元数据对象、static-final常量、static变量、jit编译器编译后的代码等数据,。另外,方法区包含了一个特殊的区域“运行时常量池”,它们的关系如下图所示:

下面对上述的总结进行解释

1.要使用一个类,首先需要将其字节码加载到JVM的内存中。至于类的字节码来源,可以多种多样,如.class文件、网络传输、或cglib字节码框架直接生成

2.class/method/field等元数据对象:字节码加载之后,JVM会根据其中的内容,为这个类生成Class/Method/Field等对象,它们用于描述一个类,通常在反射中用的比较多。不同于存储在堆中的java实例对象,这两种对象存储在方法区中。

备注:元数据就是用来描述数据的数据。比如age,name就是一个元数据,age用来描述年龄,name用来明书姓名。上述中的class描述一个类。method用来描述一个方法。

3.static-final常量、static变量:对于这两种类型的类成员,JVM会在方法区为它们创建一份数据,因此同一个类的static修饰的类成员只有一份;

4.jit编译器的编译结果:以hotspot虚拟机为例,其在运行时会使用JIT即时编译器对热点代码进行优化,优化方式为将字节码编译成机器码。通常情况下,JVM使用“解释执行”的方式执行字节码,即JVM在读取到一个字节码指令时,会将其按照预先定好的规则执行栈操作,而栈操作会进一步映射为底层的机器操作;通过JIT编译后,执行的机器码会直接和底层机器打交道。如下图所示:

备注:JIT编译器会将字节码文件编译成可以直接执行的机器码文件。进行优化的。

运行时常量池:

运行时常量池是方法区的一部分。我们了解到类的字节码在加载时会被解析并生成不同的东西存入方法区。类的字节码中不仅包含了类的版本、字段、方法、接口等描述信息,还包含了一个常量池。常量池用于存放编译期间生成的所有字面量和符号引用(如字符串字面量),在类加载时,它们进入方法区的运行时常量池存放.一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池当中。

备注:注意类的常量池和运行时常量池。class的常量池必须符合Java 虚拟机的规范,不然不会被识别,但是对于运行时常量池,java 虚拟机规范并没有做任何要求,各个不同的虚拟机提供商可以根据自己的需要来实现这个内存区域(运行时常量池)。

备注:符号引用和直接引用的区别?

答:符合引用就是正常的变量名称,变量引用。直接引用就是对应的对象的地址.

举例:

String s="hehhe";
System.out.println(s);//符合引用
System.out.println(OX123);//直接引用

备注:运行时常量池是方法区中一个比较特殊的部分,具备动态性,也就是说,除了类加载时将常量池写入其中,java程序运行期间也可以向其中写入常量。比如下面的例子

1 //使用StringBuilder在堆上创建字符串abc,再使用intern将其放入运行时常量池
2 String str = new StringBuilder("abc");
3 str.intern();
4 //直接使用字符串字面量xyz,其被放入运行时常量池
5 String str2 = "xyz";

方法区是属于哪个区域呢?是以哪种形式实现的呢?这里我觉得有必要说一下,因为jdk1.8的方法区已经和上图描述不太一样了。

答:在之前的JDK1.7之前,HotSpot的方法区的实现是在jvm堆中的,堆中划分了一个永久代区域来当做方法区使用存储的是方法区存储的数据。HotSpot使用GC分代来实现方法区的内存回收。可以使用如下的方法来调节方法区的大小

-XX:PermSize
方法区初始大小
-XX:MaxPermSize
方法区最大大小
超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen

但是使用堆中的永久代来实现方法区是不太好的,容易出现内存溢出的问题。所以在JDK1.8开始对于方法区的实现进行了优化。

在JDK1.8中已经不存在永久代的结论了,方法区被元空间代替,并且方法区不在堆中实现了,而是从堆中移除来,使用本地方法区实现(本地方法区其实也是虚拟地址空间的一部分,属于堆外内存)。

下面给出大致的方法区,运行时常常量区,字符串常量区,常量区的变化做个解释

4.直接内存

直接内存区域并不是JVM运行时的数据区域的一部分,也不是JAVA 虚拟机规范中定义的内存区域。但是这部分内容也会被频繁的使用,原因是在JDK1.4中新加入NIO类。引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过堆上的DirectByteBuffer对象对这块内存进行引用和操作。

未完待续...

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值