JVM相关概念和异常类型

coolrainman 2019-06-25

转载请注明出处 http://www.paraller.com
原文排版地址 点击获取更好阅读体验

概述

  • 基本类型、对象的引用在栈内存中操作,
  • 对象类型在堆内存中操作,
  • 特点:栈容量小,速度快;堆是容量大,速度慢

内存区域分为六块(jvm所管理的内存)

方法区 & 永久代 (method area & PermGen ):
  • 线程共享,用于储存已被虚拟机加载的类信息,常量,静态变量,及时编译期编译后的代码等数据
  • 规范中将其描述为堆的一个逻辑部分,英文名NOn-heap ,与java heap区分开来
  • 不规范别名(permanent gerneration)"永久代",除hotspot虚拟机外没有这个概念
  • 和java heap一样不需要连续的内存及可以选择固定大小或扩展,还可以选择不进行垃圾收集
  • 但垃圾收集效果难以令人满意:针对常量池的回收以及类的卸载(十分苛刻)

堆(Heap):
  • jvm管理的内存最大的一块,被所有线程共享,启动时创建
  • 目的:存放对象实例,所有实例对象和数组在这里分配内存
  • 随着JIT编译期的发展和逃逸分析技术的成熟.所有对象都分配在堆上已经不是那么"绝对"了,(不懂)
  • 该区域是垃圾收集器的主要管理区域,所以也称为GC堆
  • 既可以是固定大小,也可以是扩展的,一般都是扩展(-Xms -Xmx)
  • heap可分为young heap & old heap
  • Young区保存绝大多数刚实例化的对象,当该区被填满时,触发局部GC,局部GC会将Young+区清空,仍被引用的对象将被移到Old区。
  • 当Old区再被塞满,就会触发FullGC,回收最后能回收的空间

虚拟机栈(VM stack):
  • 线程私有;为执行java方法服务,java方法执行的内存模型,每个方法被执行时候同时创建一个Stack frame,
  • 用于储存局部变量表,操作数栈 ; 动态链接,方法出口等信息,方法被执行完结对应入栈出栈
  • 局部变量表:用于储存基本数据类型,对象引用(不等同于对象本身,可能是指向对象的引用指针)reference和returnAddress类型
  • 64位的long和double会占据2个局部变量空间,其余数据类型只占用一个
  • 局部变量表的内存空间在编译期就已经分配完成

本地方法栈(native method stack):
  • 发挥的作用与jvm stack相同,但是为虚拟机使用到的Native方法服务,hotspot虚拟机将两个合而为一

程序计数器(program counter register):
  • 当前线程 所执行的字节码的 行号指示器,线程私有内存,互不影响,独立储存
运行时常量池:
  • 是方法区 & 永久代的一部分;具备动态性,不要求常量一定只有编译器才能产生

对象的访问定位

java程序通过栈上的reference数据操作 堆上的 具体对象

虚拟机栈中reference数据称为引用
reference类型的数据中储存的数值代表的是另外一块内存的起始地址,就称这块内存是代表着引用
除了 被引用和不被引用两种状态 ;JDK1.2之后引入了四种新的概念:强引用、软引用、弱引用、虚引用

主流的方式有两种,直接指针和使用句柄

  • 直接指针: java堆中划出一块内存作为句柄池,reference保存句柄地址;句柄中保存了对象实例数据与类型数据各自的具体地址信息。优点是reference中储存的是稳定的句柄地址
  • 访问句柄: reference中保存着对象的具体地址。优点是快速。

方法区 & 永久代:

主要存放类信息,通过CGLib动态构造类会造成OOM异常,比如jsp的构造

判断对象死活

引用计数法

每当有一个地方引用+1 ,计数器为0说明没有对象引用它 , 存在两个无用对象相互引用的情况

可达性分析算法

主流方式;以GC Roots对象为起点,根据是否有引用链可达判断;哪些可以称为引用链:

  • 虚拟机栈中引用的对象
  • 本地方法栈中 引用的对象
  • 方法区 & 永久代中 类静态属性引用的对象
  • 方法区 & 永久代中 常量引用的对象

PS:对象如何在被标记回收的时候逃脱: 覆写finalize方法,将this赋值给某个类变量或者对象的成员变量

垃圾收集算法

  • 标记-清除算法:标记出所有需要回收的对象,然后统一回收 ; 缺点:效率和碎片空间多
  • 复制算法:将可用内存分为大小相等的两块,一块内存用完了之后,将存活的对象复制到另一块上,对原来的内存全部清理 ; 优点:高效,少空间碎片 ; 缺点:内存缩小了一半
  • 标记-整理算法:标记存活的对象往一端移动,然后将边界外的内存回收
  • 分代-收集算法:商业虚拟机主流算法,将java堆分成新生代和老年代,然后用不同的收集算法,新生代用复制算法,老年代标记整理就好

异常时定位 哪些区域异常

内存泄露 (Memory Leaking):

指某些对象不再被应用程序使用,而垃圾收集器(Garbage Collector)却没能识别它们是“不再使用的”。如果那些不使用的对象占用堆(heap)空间足够大,使得应用程序无法满足下一次内存分配需求,就会导致OutOfMemoryError错误

内存溢出 (OutOfMemory):

内存不够用了,原因有几种,内存泄露只是其中一种。

堆区域异常

一般是 OutOfMemoryError :先确定是内存溢出还是内存泄露(垃圾回收处理机制)
内存泄露:通过工具查看泄露对象到GC Roots的引用链,找到无法回收的原因
内存溢出:调到物理内存,判断是否对象生命周期过长

StackOverflowError:

线程请求的栈深度大于虚拟机栈所允许的最大深度;单线程的时候,无论是栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是这个异常

OutOfMemoryError: PermGen space:
  • JVM需要加载一个新类的定义,而永久代(PermGen)的空间不足——已经有太多的类存储在那里了。一个可能的原因是:你的应用程序或服务器使用了太多的类,当前的永久代(PermGen)大小无法满足需求。
  • 可能是内存泄漏(堆无法分配空间)
  • 虚拟机在扩展栈时无法申请到足够的空间
  • 多线程导致的内存溢出,在不能减少线程数和更换64位虚拟机的情况下(windows对每个进程分配的内存是有限制的,32位的为2G),只能通过减小最大堆和减小栈容量来换取更多的线程
Permanent Generation Leak - 永久代(PermGen)到底有没有可能会内存泄漏?

它保存了Java类定义,并且这些类定义是不会变成“无用”的,是吗?事实上,它们是可以变成“无用”的。以一个部署到应用程序服务器的Java web程序来说,当该应用程序被卸载的时候,你的EAR/WAR包中的所有类都将变得无用。只要应用程序服务器还活着,JVM将继续运行,但是一大堆的类定义将不再使用,理应将它们从永久代(PermGen)中移除。如果不移除的话,我们在永久代(PermGen)区域就会有内存泄漏。

Leaking Threads - 线程内存泄漏

类加载器(classloader)泄漏的一个可能的场景就是通过运行的线程(而内存泄漏)。当你的程序,或者你的程序使用的第三方库(我经常遇到这种情况,比如Quartz)开启了一些长时间运行的线程。一个例子:一个用于周期性执行代码的计时器(timer)线程。

如果不解决该线程预期的生命周期问题,我们直接会遇到麻烦。当你程序的任何一部分启动一个线程的时候,你要确保它不会比程序活得还要久。在典型的情况下,开发者要么不知道自己有责任处理好这个问题,或者忘了写清理(clean-up)的代码。

否则,如果应用程序卸载后,线程还在继续运行,它通常将维持一个到web应用程序的classloader的引用,即我们所说的contextclassloader。这也就意味着,所有卸载掉的应用程序仍然保存在内存中。怎么解决?如果是你的程序开启了新线程,那么你就应该在卸载的时候关闭它们,这可以通过使用一个servlet context listener来实现。如果是第三方库开启的新线程,你应该搜索它的关闭线程的接口,如果没有的话,就上报一个bug吧。

Leaking Drivers - 驱动内存泄漏

另一个典型的内存泄漏原因是由数据库驱动造成的。我们在和Plumbr一起发布的demo程序中遇到了这种内存泄漏情况。它是一个与Sprint MVC一起发布的、代码稍微修改过的Pet Clinic程序。让我们关注一下当这个应用程序部署到服务器上的时候,发生了什么:

  • The server creates a new instance of java.lang.Classloader and starts to load the application’s classes using it.
  • 服务器创建一个java.lang.Classloader的新实例,并用它来加载程序的类。
  • Since the PetClinic uses a HSQL database, it loads the corresponding JDBC driver,org.hsqldb.jdbcDriver
  • 由于PetClinic使用了HSQL数据库,所以它会加载相应的JDBC驱动,即org.hsqldb.jdbcDriver
  • This class, being a good-mannered JDBC driver, registers itself with java.sql.DriverManager during initialization, as required per JDBC specification. This registration includes storing inside a static field of DriverManager a reference to an instance of org.hsqldb.jdbcDriver.
  • 这个JDBC驱动类会在初始化的时候将它注册到java.sql.DriverManager中(正如JDBC规范所要求的那样)。这个注册过程包括了存储org.hsqldb.jdbcDriver的一个实例的引用到DriverManager的一个静态域中

现在,当从服务器上卸载应用程序的时候,java.sql.DriverManager仍将持有那个引用,无论在HSQLDB库,或者在Spring framework中,都没有代码可以移除它!正如上面解释的那样,一个jdbcDriver对象将持有一个到org.hsqldb.jdbcDriver类的引用,从而持有用于加载应用程序的java.lang.Classloader的一个实例的引用。这个classloader现在仍然引用着应用程序的所有类。在我们那特殊的demo应用程序中,在程序启动的时候,需要加载将近2000个类,占用约10MB永久代(PermGen)内存。这就意味着需要5~10次重新部署,才会将默认大小的永久代(PermGen)塞满,然后就会触发java.lang.OutOfMemoryError: PermGen space错误并崩溃。

怎样解决此问题?一个可能的办法就是写一个servlet content listener,用于在应用程序关闭的时候,从DriverManager反注册HSQLDB驱动。这个方法很直接,但是请记住——你需要在使用该驱动的每一个应用程序中都这么写。

Conclusion

你的应用程序遇到java.lang.OutOfMemoryError: PermGen space错误的原因很多,究其根本原因,大多数是由于object或程序的class loader加载的类的引用已经无用了导致的。对此类问题,你需要采取的补救措施都非常相似,即,首先,找出引用在哪里被持有;其次,给你的web应用程序添加一个关闭的hook,或者在应用程序卸载后移除引用。你要么通过servlet context listener,要么通过第三方库提供的API来实现这一点。

扩展:直接内存:本机直接内存

非jvm运行时数据区;新加入了的NIO类,引入了"Channel"与"Buffer"的I/O方式

通过本地函数库分配一个堆外内存,然后通过java堆的的对象实例buffer作为这块内存的引用进行操作,因为避免了再java heap and native heap之间

进行复制操作,显著的提高性能,当对于jvm的各个内存总和大于直接内存会报错

句柄:简而言之数据的地址需要变动,变动以后就需要有人来记录管理变动,(就好像户籍管理一样),因此系统用句柄来记载数据地址的变更

参考网站

Presenting the Permanent Generation

[[原创](翻译)什么是Java的永久代(PermGen)内存泄漏](http://ju.outofmemory.cn/entr...

相关推荐