JVM 内存模型与GC机制

摘要:最近在压测的时候遇到了OutOfMemoryError错误,发现是jvm内存超限,虽然过程是数据库瓶颈导致线程阻塞,垃圾回收不及时所导致,但当时解决问题的时候还是采用了一个治标不治本的法子:使用-Xms -Xmx调整jvm堆查占用内存,后面发现没解决根本为题,无论将 -Xmx调整到多大,只是增大缓存,最终还是会被塞满,报OutOfMemoryError错误。

因此,在解决该问题后有特意了解了下jvm的内存结构和回收机制

一、jvm内存模型

  • 程序计数器
  • java虚拟机栈
  • 本地方法栈
  • java堆
  • 方法区
  • 运行时常量池
1、程序计数器

其实在了解程序计数器之前多线程执行连贯性问题对我有些困扰,总不理解为啥多线程各个指令、数据不会窜线。
程序计数器,是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,运行的程序已经是字节码了,而字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
每一个线程都有独立的程序计数器,属于每个线程的"私有内存",以此也可以来确保线程命令不会"窜线"。

2、java虚拟机栈

简单理解,就是java的方法,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame ①)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈内存了方法还包括里面包含的局部变量,如数据基本类型(int ,char....),也包方法中所含对象的地址(注意,此处是存对象地址,对象本身存在java堆中)。
当方法出栈时该部分内容会被释放。

3、本地方法栈

这个概念我也没太明白,目前理解就是和java虚拟机栈一样,有点区别就是使用native修饰的方法会放到这个栈中,望大神指教,指正。

4、java堆

java堆是占用java虚拟机最大空间的一块内存,可以粗暴点理解就是存对象的地方,Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的
唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。套用下Java 虚拟机规范:所有的对象实例以及数组都要在堆上分配。但也并非绝对(做判断题要注意)。
这块内存分配就是我上文中所说使用-Xms和-Xmx来调节和实现拓展的。-Xms是最小占用空间,-Xmx是最大占用空间,可浮动。-Xmx默认占用物理内存的1/4,到如两者相等则为固定值。

5、方法区

包括常量、类变量、静态变量、即时编译器编译后的代码等数据。属于多线程恒共享数据。可以理解为jvm不停,这里面的数据就会一直存在。真真的全局变量

二、直接内存

这一块怕说不明白,我引用下:
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?
在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区
域之间的关联关系,如下面的这句代码:
Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的=具体地址信息,如下图所示。


JVM 内存模型与GC机制_第1张图片
image

如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

JVM 内存模型与GC机制_第2张图片
image

这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

三、GC机制

GC 垃圾回收,主要针对区域为java堆。回收未被有效引用的无用对象。
此处主要梳理下回收流程,回收详解及算法分析可可参考下文章:
https://www.cnblogs.com/xiaoxi/p/6486852.html

java堆包含NewSpace和OldSpace两部分,且NewSpace区域分为Eden区、From区、To区。三者大小固定。
当有新对象进入时,会率先存入Eden区,from区和to区是两个相对的概念。
步骤 :
1、当有新对象产生,会存入到Eden区,但到Eden区存不下该对象时,此时jvm执行一次Minor GC ,会将所有无用对象回收,将有用的对象放在to区,新增的对象放在Eden区,此时From 区为空

2、当新对象继续产生,Eden区再装满,在执行一次Minor GC,回收无用对象,将Eden中有用的对象和‘1’中To去中有用的对象 存入‘1’的From区。此时‘1’中的From区和To区角色切换。新的From区为空

3、重复1、2,直到执行一次Minor GC时newSpace中的To区装不下依然有用的对象,此时所有老对象被放入OldSpace中。

4、进入OldSpace的几种情况

  • 如3所描述,当执行Minor GC时,newSpace中的To区存不下还存活的对象时,会把所有存活对象放入OldSpace
  • 当新建的对象太大时,直接存到OldSpace
  • 当对象存活年龄较大时(可通过已经挺过多少次Minor GC来计算),存入OldSpace
  • 动态判断年龄,如某个年龄的对象已经超过newSpace中To区的一半时,将大于等于改年龄的对象放入OldSpace

5、再次重复以上过程,当出现OldSpace也存不下还存活的对象时,则对整个java堆执行一次Full GC,检查当前java堆所有对象使用情况,并回收无用对象。若此时还不能给新对象分配内存,或者按规矩要存入OldSpace的对象无法存入时,则报出OutOfMemoryError异常

所以针对最原始的问题,当碰到OutOfMemoryError异常时,一味加大java堆内存也是不可取的,这样可以减少执行Minor GC的次数,如果该被释放的对象一直不被释放,最终还是会导致java堆被撑满,碰到此类情况应着重排查内存泄漏。但有时候java堆内存太小会使程序内存“周转”不过来,也会导致内存溢出,此时可以适当增大jvm内存上限值。

你可能感兴趣的:(JVM 内存模型与GC机制)