Java基石——Java内存之JVM虚拟机

一、前言

Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。具体划分为如下5个内存空间

  • 栈:存放局部变量

  • 堆:Java中的所有对象都存储在这个里面,包括之后谈到的GC策略,JVM虚拟机都是在这。

  • 方法区:被虚拟机加载的类信息、常量、静态常量等。

  • 程序计数器(和系统相关)
  • 本地方法栈
1.1、栈
  • 线程私有,生命周期和线程相同
  • 栈由一系列帧组成(因此Java栈也叫做帧栈)
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用创建一个帧,并压栈

解释:

Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

栈是存放线程调用方法时存储局部变量表,操作,方法出口等与方法执行相关的信息,栈大小由Xss来调节,方法调用层次太多会撑爆这个区域。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)

(2)虚拟机栈可以动态扩展,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

1.2、堆

堆内存和程序开发密切相关,应用系统对象都保存在Java堆中所有线程共享Java堆

对分代GC来说,堆也是分代的GC管理的主要区域

现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。如果堆是分代的,那堆空间应该是下面这个样子:
在这里插入图片描述

上图是JVM内存模型的基本结构,分为堆内存和方法区内存,在之后的文章中再进行详解。

1.3、方法区

方法区保存装载的类信息,类型的常量池,字段,方法信息,方法字节码,通常和永久代(Perm)关联在一起。

1.4、程序计数器(和系统相关

每个线程拥有一个PC寄存器,在线程创建时,指向下一条指令的地址,执行本地方法时,PC的值为undefined

1.5、本地方法栈

为线程私有,功能和栈内存非常类似。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息

二、什么是JVM?(Java Virtual Machine,Java虚拟机)

​ 那就不得不谈谈Java程序的跨平台特性,主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机负责将字节码文件(.Class文件)解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。

​ JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。
在这里插入图片描述

JVM = 类加载器(classloader) + 执行引擎(executionengine ) + 运行时数据区域(runtime data area)

首先Java源代码文件被Java编译器编译为字节码文件,然后JVM中的类加载器加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM中的运行时数据区(内存)会用来存储程序执行期间需要用到的数据和相关信息。

因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

三、JVM内存模型及GC策略

1、Java虚拟机规范,JVM将内存划分为:New(年轻代)、Tenured(年老代)、Perm(永久代)

New(年轻代)和Tenured(年老代)属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm(永久代)不属于堆内存,由虚拟机直接分配,但可以通过**-XX:PermSize -XX:MaxPermSize**等参数调整其大小。

  • New(年轻代)

    • Eden:Eden用来存放JVM刚分配的对象
    • Survivor1
    • Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。
  • Tenured(年老代)

  • Perm(永久代)用于存放静态文件,如今Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代大小通过-XX:MaxPermSize=进行设置。

    那么看了这个模型介绍,问题君有很多问题,年轻代只设置一个Eden区行不行?
    如果只设置一个Eden区,那么每进行一次MinorGC(YGC),存活的对象就会被送入老年代,老年代很快被填满,就会触发MajorGC,因为MajorGC之前会先进行一次MinorGC,所以也可以看做是发生了Full GC,Full GC消耗的时间要远远大于Minor GC,这样会增加系统停顿时间。那么,你们也许会想,可以增加老年代的空间,减少Full GC的频率,可以是频率虽然降低了,但是由于老年代存储的对象太多,一旦发生Full GC(FGC),单次GC的时间增加了,系统停顿时间依然很长。反之,单次GC时间减少,但是GC频率增加了,结果还是一样的。
    所以,我们减少Full GC的方案只有一个:减少往老年代发送的对象,进而更慢地触发Full GC。所以,我们才需要Survivor区来进行筛选,只要经历了16次Minor GC还能在年轻代存活的对象才有资格送往Tenured(年老代)。

    设置一个Eden区和一个Survivor区行不行?为什么要建立两个Survivor区?建立多个Survivor区可以吗?
    我们已经知道了必须需要设置Survivor区,如果只要一个Survivor区,MinorGC流程是:
    把new对象放在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会移动到Survivor区,同时Eden区被清空。
    然后继续new对象放在Eden区中,一旦Eden满了,又触发一个Minor GC,此时,Eden和Survivor中各有一些存活的对象,尤其是Survivor区发生了GC后,剩余存活的对象肯定不是紧密排列的,如果此时把Eden区中存活对象强行移动到Survivor区中,由于这Survivor区域的对象不是连续的,所以会产生内存碎片。

    ​ 建立两个Survivor区,分别是Survivor1和Survivor2,MinorGC流程如下:把new的对象放在Eden区中,一旦Eden满了,触发一次Minor GC,Eden区中存活的对象会送到Survivor1中,Eden被清空;再次new对象放入Eden区中,Eden又满了,触发Minor GC,Eden和Survivor1中存活的对象放入到Survivor2中,然后Survivor1和Eden清空,然后Survivor1和Survivor2交换角色,Survivor2变成了Survivor1,作为下一次GC的主要目标,Survivor1变成了Survivor1,存放下一次GC存活的对象。如此循环往复,对象头的分代年龄达到16次,则会被送到Perm(年老代)中。有两个Survivor的好处就是:永远有一个Survivor区是空的,另外一个Survivor1区无碎片。如果Survivor细分为更多块,每一块的空间会比较小,很容易导致Survivor满,对象的分代年龄增加的越快,导致送往老年代的对象越多,所以两个Survivor区是经过权衡后的最佳方案。

2、GC策略

从上面不难看出,GC回收应该有很多种的的策略,那么我们采用的主流策略是是什么呢?

JVM里的GC(Garbage Collection)的算法有很多种,如分代收集,标记清除,标记整理算法等等,我们这里主要讨论一下常用的GC策略。

2.1分代收集

2.1.1、内存申请、对象衰老过程

①、内存申请过程

  1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;
  2. 当Eden空间足够时,内存申请结束。否则到下一步;
  3. JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
  4. Survivor区被用来作为Eden及Tenured的中间交换区域,当Tenured区空间足够时,Survivor区的对象会被移到Tenured区,否则会被保留在Survivor区;
  5. 当Tenured区空间不够时,JVM会在Tenured区进行major collection;
  6. 完全垃圾收集后,若Survivor及Tenured区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";

②、对象衰老过程

  1. 新创建的对象的内存都分配自eden。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。对象在young generation里经历了一定次数(可以通过参数配置,一般参数为16次)的minor collection后,就会被移到old generation中,称为tenuring。

  2. GC触发条件

    GC类型触发条件触发时发生了什么注意查看方式
    YGCeden空间不足清空Eden+from survivor中所有no ref的对象占用的内存 将eden+from sur中所有存活的对象copy到to sur中 一些对象将晋升到Tenu中: to sur放不下的 存活次数超过turning threshold中的 重新计算tenuring threshold(serial parallel GC会触发此项)重新调整Eden 和from的大小(parallel GC会触发此项)全过程暂停应用 是否为多线程处理由具体的GC决定jstat –gcutil gc log
    FGCTenured(年老代)空间不足 Perm(永久代)空间不足 显示调用System.GC, RMI等的定时触发 YGC时的悲观策略 dump live的内存信息时(jmap –dump:live)清空heap中no ref的对象 permgen中已经被卸载的classloader中加载的class信息 如配置了CollectGenOFirst,则先触发YGC(针对serial GC) 如配置了ScavengeBeforeFullGC,则先触发YGC(针对serial GC)全过程暂停应用 是否为多线程处理由具体的GC决定 是否压缩需要看配置的具体GC

2.1.2、分代收集

现在比较常用的是分代收集,对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。

​ 年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消 亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

​ 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor1(此时,Survivor2是空白的,两个Survivor总有一个是空白的); 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor2; 当Survivor2也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

​ 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 FGC。可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
​ JVM(采用分代回收的策略),用较高的频率对年轻的对象(young generation)进行YGC,而对老对象(tenured generation)较少(tenured generation 满了后才进行)进行Full GC。这样就不需要每次GC都将内存中所有对象都检查一遍。

GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS(特别是动态生成类,当然permgen space存放的内容不仅限于类)的话,就很可能出现PermGen Space错误。

2.2、标记清除算法

分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象,如下图。缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片
在这里插入图片描述

2.3、标记整理算法

标记-整理算法:先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存,如下图。
在这里插入图片描述

四、JVM性能调优

1、JVM调优目标

​ JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟。程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu负载过高、请求延迟、系统吞吐量(tps)降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。这里有几个比较重要的指标:

  • 内存占用:程序正常运行需要的内存大小。
  • 延迟:由于垃圾收集而引起的程序停顿时间。
  • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

当然,和CAP原则(一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统。但是设计者考虑用户体验,保证高可用的情况下,即在此情况下获得了CA系统)一样。同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标程序cpu负载、请求时迟、系统吞吐量等,从中可能出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃。使得程序在正常运行的前提下,因此需要对JVM进行调优。

2、JVM调优工具

(1)调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。

①系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。

②堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。

③GC日志:程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化。

④线程快照:顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息,更详细的使用和分析网上有很多例,这篇文章写到这里已经很长了就不过多叙述了,贴一篇博客供参考:http://www.cnblogs.com/kongzhongqijing/articles/3630264.html

⑤堆转储快照:程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。

3、JVM调优总结
3.1、JVM参数
参数含义实例
-Xms初始堆大小,默认物理内存的1/64-Xms512M
-Xmx最大堆大小,默认物理内存的1/4-Xms2G
-Xmn新生代内存大小,官方推荐为整个堆的3/8-Xmn512M
-Xss线程堆栈大小,jdk1.5及之后默认1M,之前默认256k-Xss512k
-XX:NewRatio=n设置新生代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4-XX:NewRatio=3
-XX:SurvivorRatio=n年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8-XX:SurvivorRatio=8
-XX:PermSize=n永久代初始值,默认为物理内存的1/64-XX:PermSize=128M
-XX:MaxPermSize=n永久代最大值,默认为物理内存的1/4-XX:MaxPermSize=256M
-XX:ParallelGCThreads设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。-XX:ParallelGCThreads=4
3.2、小结建议

​ JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。

​ 那么JVM的配置比如年轻代、年老代应该配置多大最合适呢?答案是不一定,调优就是找答案的过程,物理内存一定的情况下,年轻代设置越大,年老代就越小,Full GC频率就越高,但Full GC时间越短;相反年轻代设置越小,年老代就越大,Full GC频率就越低,但每次Full GC消耗的时间越大。建议如下:

  • -Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。

  • 新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。

  • 老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。

  • 方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7只要差不多能装下启动时和后期动态加载的类信息就行。代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:

  • 避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。

  • 避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。

  • 当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。

  • 可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
    避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。

  • 尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入年老代,如果不能及时返回结果可以适当采用异步处理的方式等。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值