JVM基础和内存区域剖析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础和内存区域剖析相关的知识,希望对你有一定的参考价值。

参考技术A

概念

特点

先进后出

虚拟机栈:用户描述Java方法执行的内存模型

栈帧:虚拟机栈中的栈元素(用于支持虚拟机进行方法调用和方法执行的数据结构)包括局部变量表、操作数栈、动态链接、方法出口

1.主要存储:

2.数据过多会导致OutOfMemoryError异常

< JDK 1.8

>=JDK1.8

和永久代的区别:

1.存储位置不同,永久代物理上是堆的一部分,和新手代,老年代地址是连续的,而元空间属于本地内存;

2.存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中;

3.相当于永久代的数据被分到了堆和元空间中

直接内存:避免native空间和java堆中来回进行复制

虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包含常量池)都在堆上分配内存,当对象无法再该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。可通过 -Xmx –Xms 参数来分别指定最大堆和最小堆

GC主要管理区域,可以通过-Xmx和Xms来设置最大和最小值,也可以通过-XX:NewSize -XX:MaxNewSize设置年轻代初始大小

超出空间大小会抛出OutOfMemoryError异常

新生区

新生区分为两个部分:伊甸区(Eden space)和幸存者区(survivor space)

伊甸区(Eden space)

大部分对象都会在Eden区诞生,并且一段时间不使用就会被GC回收

幸存者区(survivor space)

存在一段时间还在使用的对象会进入survivor区,survivor区包含一个相对的From区和to区,两者来回copy,回收没用的对象,用来延长对象的生命周期。

老年区(old FullGC)

经过多次GC仍然存在的对象会移动到老年区中,若老年区也满了,则会产生MajorGC(Full GC),对老年区进行内存清理(STW),若老年区执行了Full GC之后发现还是无法进行对象的报错,那么就会产生OOM异常”OutOfMemoryError“

问题:空间不连续,浪费空间

复制算法有2块一样大小的空间,情况对象时将可用的对象移动到to区里,复制算法内存空间连续

问题:要用2块空间,所以内存的模型from和to非常的小

只用一块空间,先进行标记无用对象,然后整理内存空间地址,最后清除

它只有一条GC线程,且就像前面说的,它在运行的时候需要暂停用户程序(stop the world)STW

它有多条GC线程,且它也需要暂停用户程序(stop the world)STW

它有一条或多条GC线程,且它需要在部分阶段暂停用户程序(stop the world),部分阶段与用户程序并发执行

serial(用于新生代,采用复制算法)、serial old(用于年老代,采用标记/整理算法)

parNew(用于新生代,采用复制算法)、Parallel

Scavenge(用于新生代,采用复制算法)、Parallel

old(用于年老代,采用标记/整理算法)

concurrent mark sweep[CMS] (用于年老代,采用标记/清除算法)

JVM基础--JVM内存区域模型

  一提到Java,我们第一直觉就是Java语言。其实Java不仅仅是一种编程语言,它还是由一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并且广泛应用于嵌入式系统/移动终端/企业服务器/大型机等场合。从广义上讲,类似于JRuby等运行在JVM上的语言及其相关的程序都属于Java技术体系中的一员。但是,一般咱们从传统上理解,sun官方所定义的Java技术体系包括:Java程序设计语言/各个平台上的JVM虚拟机/class文件/Java API类库/还有第三方Java类库。我们把JVM JavaAPI 和Java程序设计语言这三部分称为JDK。

  简单介绍了Java之后,就进入今天的主题,jvm内存区域模型。相对于c或者c++来说,Java的自动内存机制可以每次new对象写配对的delete/free代码。不容易出现内存溢出和内存泄漏的情况。同样,有利肯定就有弊处。一旦出现内存泄漏或者内存溢出的情况,排查问题也是一项棘手的工作。

  JVM在执行Java程序时,会把Java虚拟机管理的内存划分不同的数据区域。每个区域都有自己的用途。如下图所示:

这张图就表示了Java虚拟机运行时数据区。下面我们就来一一介绍着不同区域的内容。

程序计数器

  程序计数器program counter register , 是一块较小的内存空间。我们可以把它当前线程所执行的字节码的行号指示器。

而在虚拟机的概念模型中,字节码解释器,是通过改变这个程序计数器的值,来选去下一条需要执行的字节码指令。一些基础功能,比如分支/循环/跳转/异常处理/线程恢复等,就是得需要这个计数器才能完成。

  Java虚拟机中的多线程就是 通过线程轮流切换分配处理器执行时间 的方式来实现。在任何一个时间内,一个处理器也只能执行一个线程中的指令。所以,线程之间是来回切换的,为了线程切换后能恢复到正确的执行位置。所以每条线程都配置了一个专用的独立的 程序计数器。这样,各个线程的程序计数器之间不会相互干扰,独立存储。我们称之为“线程私有”内存。

Java虚拟机栈

Java虚拟机栈 VM stack,就是执行Java方法的内存模型。这个也是线程私有的,生命周期跟线程一致。

每个Java方法被执行的时候,都会创建一个“栈帧” ,用于存储 局部变量表 和 操作数栈 和 动态链接 和局部变量区 等信息

每个Java方法从被调用 到 执行完 这个过程,就对应着一个栈帧虚拟机栈中 从入栈 到 出栈的过程。

下图是关于一个“栈帧”的示意图:

其中值得注意的是,栈帧中的局部变量表:局部变量区所需要的内存空间,是在编译期间就完成分配的。当进入一个方法是,这个方法需要在帧中分配多大的局部变量空间是完全确定的。方法在运行期间,是不会改变局部变量表的大小。

本地方法栈

本地方法栈 native method stack ,与上面介绍的虚拟机栈基本类似。区别之处在于:上面的虚拟机栈  为执行Java方法的内存模型,而本地方法栈 是为native方法也就是操作系统提高的方法 服务的。有的虚拟机(如sun Hotspot虚拟机),就直接把本地方法栈和Java虚拟机栈合并在一起。

Java堆

java堆Java heap是Java虚拟机中内存最大的一块,是被所有线程共享的一块内存区域,随着Java虚拟机的启动而创建。

该区域的唯一目的就是 存放对象实例。所有的对象实例以及数组都是在堆上进行分配。随着JIT编译器的发展与逃逸分析技术的逐渐的发展,所有对象均在堆上进行分配也变得不那么绝对。

  Java堆是 垃圾收集器管理的主要区域。因此也被称为 GC堆 : 从内存回收的角度来看,由于现在收集器基本都是采用的 分代收集算法,所以 Java堆还可以细分为:新生代Young generation 和 老年代old generation。从内存分配的角度来看,线程共享的Java堆中可能会划分出多个线程私有的分配缓冲区 也就是thread local allocation buffer ,TLAB

  但是,不管是如何划分,不管是哪个区域,都是与存放的内容没有关系,存储的依旧都是对象实例,这点是不会变化的。

  现在我们来介绍一下,Java堆的内存大小。JVM的大小是通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms是Java虚拟机启动时申请的最小内存,默认时操作系统物理内存的1/64,但是 大于1G,-Xmx 是Java虚拟机可以申请的最大内存,默认时操作系统的物理内存的1/4 同样也是小于1G。默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可以通过"-XX:MinHeapFreeRation=" 这个参数来制定这个比例;当空余堆内存大于70%时,JVM会减少heap的大小到-Xms指定的大小,可以通过"-XX:MaxHeapFreeRation="来指定这个比例。对于运行系统,为了避免在运行时频繁调整Heap的大小,通常把-Xms和-Xmx的值设置成一样的。

  上文介绍了关于Java堆的分法,我们在这就着重介绍一下关于新生代和老年代。堆的新生代和老年代的默认比例时1:2。新生代主要时 存储新创建的对象 和 尚未进入老年代 的对象。老年代则是存储经过多次新生代GC(MinorGC)仍然存活的对象。

  新生代:

    • 程序新创新的对象都是从新生代中分配内存
    • 发生在新生代中的GC 称为MinorGC
    • 新生代由 Eden Space 和 两块大小相同的survivor space(一般称为S0 和 S1 或 From 和 To)来构成
    • 通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation 来调整Eden Space 以及 Survivor Ration的大小,默认比例为8:1:1

  老年代:

    • 用于存储经过多次新生代GC仍然存活的对象,比如 缓存对象
    • 注意:新建的对象也可能直接进入老年代:
      • 大对象:通过启动参数“-XX:PretenureSizeThreshold=1024字节(默认为0)”,设置c
      • 大的数组对象,且 数组中没有饮用外部对象
    • 发生在老年代中的GC 称为Major GC / full GC
    • 老年代所占内存大小是-Xmx减去-Xmn的值

下面是以视图的方式来展现对象在Java堆中的移动:

对象在堆中的移动:

  1. 绝大多数刚被创建的对象会存放在Eden区
  2. 在Eden区执行了第一次GC之后,存活的对象会被移动到一个Survivor空间
  3. 在Eden区执行GC之后,存活的对象会被堆积在同一个Survivor空间
  4. 当一个Survivor空间饱和之后,依旧存活的对象会被移动到另一个Survivor空间,之后会清空已经饱和的Survivor空间
  5. 在以上步骤重复几次后,依然存活的对象,则会被移动到老年代。一般是默认15次,但是我们可以通过选项“-XX:MaxTenuringThreshold=n” 控制经历n次GC后移动老年代

方法区

  方法区 method area,其实方法区也可以称为永久代permanent generation .大家可以参考上面的图,老年代左侧有个perm区。这个就是方法区/永久代。

  用于存储虚拟机加载的类信息 常量 静态变量 即时编译器编译后的代码等数据,是各个线程共享的内存区域。

  默认最小内存为16MB和最大值64MB;可以通过参数 “-XX:PermSize”和“ -XX:MaxPermSize”这两个参数限制方法区的大小。

 

以上介绍的是JVM内存区域模型的基本部分,也是最常用的部分。我们明白了虚拟机中的内存是如何划分的,对于一些代码和操作会引起内存溢出异常,是哪一部分还需要我们继续学习和研究。虽然Java有垃圾收集机制,但是内存溢出异常也是会有的。我们常遇到的就有一些Java堆溢出 虚拟机栈和本地方法溢出 还有方法区和运行时常量池溢出等,这些都是常遇到的。但是Java中的垃圾收集机制还是会避免一些内存溢出异常的出现,读者也可以查询相关的资料进一步了解。

    

 

以上是关于JVM基础和内存区域剖析的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM之内存区域

java基础 --- Java内存结构

java基础 --- Java内存结构

java_基础JVM内存模型和垃圾回收机制

分享:关于 JVM 内存的 N 个问题和定位工具

JVM系列-- 关于JVM 内存的 N 个问题