JAVA内存区域与内存溢出异常

JAVA内存区域与内存溢出异常

1. 程序计数器

它是一块较小的内存,可以看作是当前线程所执行字节码的行号指示器

每条线程都有一个单独的程序计数器,用于线程切换。

注意:如果正在执行本地(Native)方法,则计数器值为空(唯一一个在java虚拟机规范中没有规定OutOfMemoryError的情况)

2. JAVA虚拟机栈

他也是线程私有的,描述的是java方法执行的线程内存模型,每个方法执行的收,就会同步创建一个栈帧,用于存储局部变量表、操作数栈,方法出口等信息,生命周期与线程相同。通常所说的栈其实指的是这里的虚拟机栈,或者更多情况下是栈中的局部变量表。

局部变量表中存放了编译时期可知的基本数据类型(int,byte,float等)、对象引用类型、returnAdress类型。

这些数据类型以局部变量槽的存储空间来表示,其大小(槽的数量)在编译完成就已经确定,而在运行时刻不会改变其大小。

该区域会产生俩种异常:

  • StackOverFlow:线程请求的深度大于虚拟机栈所允许的深度。
  • OutOfMemeoryError如果栈可以动态扩展,当扩展时无法申请到足够内存时就会发生。

3. 本地方法栈

与虚拟机栈作用相似,差别是本地方法栈是为了虚拟机执行本地方法所服务的,而虚拟机栈是为了java方法(字节码)服务。

4. java堆

java堆是虚拟机所管理的的最大的一块内存,是所有线程共享的一块内存区域,此区域的唯一目的就是存放对象的实例。几乎所有的对象实例就是在这个地方分配内存的。(由于逃逸技术的日益强大,栈上分配,标量替换已经导致一些变化)

关于 Java 逃逸分析的定义:

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术根据 Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff 等大牛在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。

该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在次基础上,提出一种组合数据流分析法。

由于算法是上下文相关和流敏感的,并且模拟了对象任意层次的嵌套关系,所以分析精度较高,只是运行时间和内存消耗相对较大。1

我们了解了 Java 中的逃逸分析技术,再来了解下一个对象的逃逸状态。

1、全局逃逸(GlobalEscape)

即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

  • 对象是一个静态变量
  • 对象是一个已经发生逃逸的对象
  • 对象作为当前方法的返回值
2、参数逃逸(ArgEscape)

即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

3、没有逃逸

即方法中的对象没有发生逃逸。

针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化。

1) 锁消除

我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。

例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。

锁消除的 JVM 参数如下:

  • 开启锁消除:-XX:+EliminateLocks
  • 关闭锁消除:-XX:-EliminateLocks

锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。

2) 标量替换

首先要明白标量聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。

对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。

这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。

标量替换的 JVM 参数如下:

  • 开启标量替换:-XX:+EliminateAllocations
  • 关闭标量替换:-XX:-EliminateAllocations
  • 显示标量替换详情:-XX:+PrintEliminateAllocations

标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。

3) 栈上分配

当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。

从内存分配的角度来看,所有线程共享的java堆,可以分化出多个线程私有的缓冲区(Thread Local Allocation Buffer)TLAB,用于提升对象分配时候的效率。

java堆物理上可以是不连续的,但在逻辑上必须是连续的。

java堆可以设置成固定大小的,也可以是可扩展的(通过-Xmn,-Xms设定),如果堆未完成实例分配,并且堆无法扩展时,则会抛出OutOfMemoryError异常。

5. 方法区

与java区一样是所有线程所共享的一块内存区域,用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译编译后的代码缓存等数据。

到KDK6的时候 hotspot团队就有放弃永久代,采用本地内存来实现方法区。到了JDK7已经把放在永久代的字符串常量、静态变量移出。到了JDK8把永久代剩余的内容主要是(类型信息)移到元空间。

元空间
    很多开发者都在其系统中见过“java.lang.OutOfMemoryError: PermGen space”这一问题。这往往是由类加载器相关的内存泄漏以及新类加载器的创建导致的,通常出现于代码热部署时。相对于正式产品,该问题在开发机上出现的 频率更高,在产品中最常见的“问题”是默认值太低了。常用的解决方法是将其设置为256MB或更高。

JDK8 HotSpot JVM 将移除永久区,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace)。这意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用。

总结:
   PermGen空间状况:这部分内存空间将全部移除。JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。

Metaspace 容量:默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整

JDK8 HotSpot JVM 使用本地内存来存储类元数据信息并称之为:元空间(Metaspace);这与Oracle JRockit 和IBM J9很相似。如下图所示:

这里写图片描述

这将是一个好消息:意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用……但请等等,这么说还为时过早。在默认情况下,这些改变是透明的,接下来我们的展示将使你知道仍然要关注类元数据内存的占用。请一定要牢记,这个新特性也不能神奇地消除类和类加载器导致的内存泄漏

在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。

每个加载器有单独的存储空间。

省掉了GC扫描及压缩的时间。

当GC发现某个类加载器不再存活了,会把对应的空间整个回收。

6. 运行时常量池

运行时常量池是方法区的一部分,class文件中除了字段,方法、类型以外,还有一项信息是常量池表,用于存放编译时期生成的各种字面量的符号和符号引用,之后在类加载的时候存放到方法区的运行时常量区

1.符号引用(Symbolic References):

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2.直接引用:直接引用可以是

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

7. 直接内存

使用native直接分配对外内存,然后通过在堆中的一个DirectByteBuffer对象作为这块内存的引用进行操作,这样避免了在java堆和navtive中来回复制的开销。

参考:《深入理解java虚拟机》第三版-----周志明

https://www.cnblogs.com/javastack/p/11023044.html

https://www.jianshu.com/p/a6f19189ec62

https://blog.csdn.net/u012834750/article/details/70160594

https://www.cnblogs.com/shinubi/articles/6116993.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值