JVM调优:JVM的内存模型

Java虚拟机的模型

JVM(java virtual Machine) 是jre的一部分,是java语言具有跨平台性的实现途径,JVM有自己完善的硬件架构如处理器、堆栈、寄存器等,还有相应的指令系统。JVM主要功能是对程序加载的内存分配、内存回收等工作。

 

JVM被分为三个子系统:类加载子系统、运行时数据区(栈、堆、方法区)、执行引擎(垃圾回收机制)。

JVM主要掌握:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区这5个部分组成。

程序计数器

是一个较小的内存空间,可以把它看着当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是正在执行的哪一条的指令地址。

但是:当执行的是本地方法是,程序计数器的值为空

程序计数器作用

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码流程的控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的模式下,程序计数器用于记录当前线程的执行位置,从而当线程切换回来的时候能从上一次的执行位置开始执行。

程序计数器的特点

  1. 具有一块较小的内存空间。
  2. 线程私有,每条线程都有一个程序计数器。
  3. 是唯一不会出现在OutOfMemoryError的内存区域
  4. 生命周期随着线程的创建而创建,随着线程的结束而结束。
  5. 线程私有。

java虚拟机栈(JVM stack)

虚拟机栈和线程时紧密联系的,每创建一个线程时就会创建一个java栈,故java栈和程序计数器一样是线程私有的,每个java栈中又包含多个栈帧,每调用一个方法时就会往栈中创建并压入一个栈帧,每一个方法的调用和最终的返回结果就对应着栈帧的进栈和出栈。栈帧是用于存储该调用方法运行中所需要的一些信息。这些信息包括:局部变量表(基本的数据类型、引用类型、变量、return address的变量)、操作数栈、动态链接、方法出口信息等。当这个方法执行完毕后,这个方法所对应的栈帧出栈,并释放内存空间。

关于java的内存空间分为堆栈的问题:这个说法是不正确的,java 的堆可以这么理解,但是这里的栈只能代表java虚拟机中的局部变量表的部分。真正的java虚拟机栈是由栈帧组成,而每个栈帧由:局部变量表、操作数栈、动态链接、方法出口等。

JVM栈的特点

  1. 局部变量被创建是在方法被执行的时候,随着栈帧的创建而创建。而且局部变量表的大小就被确定下来了,在创建时只需要分配事先规定的大小即可。此外,在方法表的大小不会被改变的。
  2. Java虚拟机有两种异常:StackOverFlowError和OutOfMemoryError。 ①:StackOverFlowError:若java虚拟机栈的内存大小不允许动态的扩展.那么当线程请求栈的深度超过当前java虚拟机栈的最大深度时,就会抛出这个异常。②:OutOfMemoryError:若java虚拟机栈的内存大小允许动态扩展,但是在线程请求栈时,内存用完了,无法再动态扩展就会扔出这个异常。
  3. JVM栈也是线程私有的,每一个线程都有各自的JVM栈,而随着线程的创建而创建,随着线程的死亡而死亡。

本地方法栈

本地方法栈和JVM栈一样,实现的功能类似,只不过本方法区是本地方法运行的内存模型。本地方法被运行时。在本地方法栈会创建一个栈帧。用于存放该本地方法的局部变量,操作数据栈、动态链接、出口等;执行完毕后相应的栈帧也会出栈,释放占用内存。也会抛出StackOverFlowError和OutOfMemory异常。

堆(heap)是存储java实例或者对象的地方,是GC的主要区域,同样是线程共享的内存区域。

堆的特点

  1. 线程共享,整个JVM只有一个堆,所有的线程都能访问同一个堆。而程序计数器,JVM栈,本地方法栈都是一个线程对应一个。
  2. 在虚拟机的启动时创建
  3. 垃圾回收的主要场所
  4. 可以进一步分为:新生代,老年代。新生代可被分为:Eden、FromSurvior、ToSurvior。不同的区域存放具有不同的生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,效率更高。
  5. 堆的大小既可以固定也可以扩展,但是主流的虚拟机堆大小是可以扩展的,因此线程请求分配内存,但堆已满,并且内存已满无法扩展时,就会抛出OutOfMemoryError异常。

实例

i1和i2都new的一个Integer对象,两个对象的内存地址都放在堆中,并且占用的两个地址;a和b的值都等于1,但是在类加载进方法区中时,这些常量都会放在常量池中,所有a和b都是指向的1这个常量的地址,所以a==b是true,i1==i2是false。

 

方法区

JVM中定义方法区是堆的逻辑部分;方法区是用于存储类结构信息的地方,包含有:常量池、静态变量(static)、构造函数等类型信息,这些类信息都是由在类加载器从类文件中提取出来的;方法区同样存在垃圾回收,因为用户自定义加载器加载的一些类同样会成为垃圾,JVM会回收一个未被引用类型所占的空间;方法区是线程共享的。

方法区的特点

  1. 线程共享:方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的,整个JVM只有一个方法区
  2. 永生代:方法区中的信息,一般需要长期的存在,而且它又是堆的逻辑分区,因此用堆的划分方法,将方法区称为老年代。
  3. 内存回收效率低:方法区中的信息,一般长期存在,回收一遍内存后可能只有少量信息无效,对方法区的内存回收的主要目标:对常量的回收和对类型的卸载。
  4. JVM规范对方法区要求比较宽松:和堆一样,允许固定大小,也允许可扩展大小,还允许不实现垃圾回收。

堆内存内部结构

堆被划分为新生代和老生代,新生代的对象特点是朝生夕死(短命鬼),老生代相对来说就是存活时间周期比较长的。新生代又被划分为Eden区,和Survior区。Eden:Survior1:Survior2=8:1:1的比例,新创建的对象都会一开始就放在新生代中的Eden中,如果新生代容量满后,新生代就会发生minor gc,minor gc会清除eden、s0、s1中的垃圾。

 

 

minor gc垃圾清除的流程:当新生代容量满了,那么就会发现minor gc,那么eden中的存活的对象全部要转移到s0或者s1中(因为s0或者s1始终会有一个区是始终保持是空),这里假如s0是空的,eden存活的对象全部转移到s0中后,再把s1中存活的对象也转移到s0,那么s1保证是空的;每次幸存的对象每minor gc一次,年龄就会增加一岁,等待到达某个值时(这个值是自己设定的,也有默认值),会将对象转移到老年代。

 

eden和Survior,eden占的比例是80%,因为年轻代中的对象80%都是会死的;老年代的内存空间会更大。如果老年代都满了会发生major GC或者Full GC,

major GC只是回收老年代的垃圾,一切程序都会正常运行;Full GC是回收新生代、老年代中的垃圾,但是它会暂停所有线程等待清理垃圾(线程挂起)。

JVM垃圾回收机制判断对象是否是垃圾的方法:①:引用计数器,一个对象被引用就+1,没有被引用就是垃圾,但是无法解决循环引用的情况。

  ②:根集GC Roots,就是可达性分析,一个对象能不能在根集中直接的或者间接的到达,则说明该对象没有对废弃。

 

老年代用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也 有可能直接进入老年代,主要有两种情况:

  1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字 节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。

  2、大的数组对象,且数组中无引用外部对象。老年代所占的内存大小为-Xmx对应的值减 去-Xmn对应的值。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError异常。

 

JVM内存常用配置参数的配置

JVM中的新生代、老年代、Eden区等参数都可以自己配置,因为在很多情况下,我们自己的程序的服务的压力不一样,可以针对性配置高性能的应用,解决一些程序承受力的问题。

-Xms

  JVM启动时申请的heap的初始空间值,默认:操作系统物理内存的*1/64但是小于1G。默认堆内存大于70%时,JVM会减少heap的大小到-Xms指 定的大小。

Server端JVM最好将-Xms和-Xmx设为相同 值,避免每次垃圾回收完成后JVM重新分配内存,也可以减少垃圾回收次数;开发测试机 JVM可以保留默认值。(例如:-Xms4g)

-Xmx

  JVM启动时申请的heap的最大值,默认值为物理内存的1/4但小于1G,默认当空余堆内存小于 40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个。(例如:-Xmx4g)

jvm这么一个配置的意思:-Xms5M -Xmx20M -XX:+PrintGCDetails - XX:+UseSerialGC:heap初始内存5M,最大内存20M。

-Xmn

  Java Heap Young区大小。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小(相 对于HotSpot 类型的虚拟机来说)。持久代一般固定大小为64m,所以增大年轻代后,将会 减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。(例如:- Xmn2g)

-Xss

  Java每个线程的Stack大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大 小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生 成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验 值在3000~5000左右。(推荐:小系统:-Xss126K,大系统:-Xss256K)。

-XX:PermSize

持久代(方法区)的初始内存大小。(例如:-XX:PermSize=64m)

-XX:MaxPermSize

持久代(方法区)的最大内存大小。(例如:-XX:MaxPermSize=512m)

-XX:+MaxTenuringThreshold=10

垃圾的最大年龄,代表对象在Survivor区经过10次复制以后才进入老年代。如果设置为 0,则年轻代对象不经过Survivor区,直接进入老年代。

配置例子:这个配置例子还是算比较适用的

tomcat中的配置内存参数可以放在 catalina.sh或者catalina.bat的第二 行: set JAVA_OPTS=%JAVA_OPTS% -server -Xms1800m -Xmx1800m -Xmn600m - XX:PermSize=512M -XX:MaxPermSize=512m -Xss256K -XX:+PrintGCDetails

 

内存的泄露与溢出

内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内 存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者 说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就 是内存不够用,此时就会报错OOM,即所谓的内存溢出。

内存泄露的分类:

  1.常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导 致一块内存泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发 生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所 以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷, 导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在构函数 中却没有释放该内存,所以内存泄漏只会发生一次。

  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内 存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对 于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终 耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

 

内存溢出的原因:

  1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

  2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

  3.代码中存在死循环或循环产生过多重复的对象实体;

  4.使用的第三方软件中的BUG;

  5.启动参数内存值设定的过小;

内存溢出的解决方案

  第一步,修改JVM启动参数,直接增加内存。

  第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

  第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

  第四步,使用内存查看工具动态查看内存使用情况

排查方式

  1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一 次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前, 数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有 可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询,尤其大的 系统。

  2.检查代码中是否有死循环或递归调用。

  3.检查是否有大循环重复产生新对象实体。

  4.检查List、Map等集合对象是否有使用完后,未清除的问题。List、Map等 集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

总结

  1. 方法区、堆是所有线程共享的内存区域;而程序计数器、本地方法区、虚拟机栈都是线程私有。
  2. 栈相关:①:每个线程都会包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用。对象是放在堆中的。②:这个栈中的数据都是这个线程私有的其他线程不能访问。③:方法的形参方法调用完后从栈中进行回收。④:引用的对象地址,引用完成之后,栈空间会被立即释放,堆中的空间会等待回收。
  3. 堆相关:存储在堆中的全部是对象,每个对象包含与之对应的class信息;JVM只有一个堆区被所有线程共享,堆区中不存在基本类型的对象引用,只存放对象本身。
posted @ 2019-03-18 20:42  轻抚丶两袖风尘  阅读(219)  评论(0编辑  收藏  举报