<服务>JVM

时间:March 12, 2017 分类:

目录:

概念

JVM是通过c++编写,解决了java的跨平台问题。

数据类型

JVM中,有两类数据类型,一种为基本类型,另一种为引用类型,基本类型的变量保存的是原始的值,基本运行在栈中,比如说int,long等,而引用数据类型变量保存的引用的值,,由两部分组成的,一部分是指针,另一部是引用的值,引用变量堆中,在栈中执行的时候从堆中取出,进行计算,使用完成后放回堆中,这样对象是共享,多线程的时候计算的时候会共享,但是会引起其他的问题。

基本类型包含:byte,short,int,long,char,double,Boolean 引用类型包含:类类型(对象),接口类型和数组

数组就是通过语言本身分配一块连续的内存,cpu的L1 cache一般会有64个位,如果数据如果在64个值以内,就能直接放入CPU中进行,而不放在内存中L1 cache是纳秒级别,而内存是1%毫秒级别,差了1000倍,做业务可能没啥区别,但是用于计算就差别很大了。

链式存储结构和顺序存储结构的区别

区别在于:

  1. 链表存储结构的内存地址不一定是连续的,但顺序存储结构的内存地址一定是连续的;
  2. 链式存储适用于在较频繁地插入、删除、更新元素时,而顺序存储结构适用于频繁查询时使用。

堆与栈

Linux有进程地址空间,运行的每个实例都会被划分到进程地址空间,也是有堆和栈,上边是堆,下边是栈,然后是全局的代码,最后是代码的指令等,具体的可以去查一下,这边主要讲JVM的堆与栈。

栈的特点

栈是运行时的单位,和线程绑定,解决的问题是程序运行的问题,即程序如何执行,如何处理数据,而堆是存储单位,用于存储对象,通过堆中对象的引用指针进行绑定栈中数据,解决的问题为数据放在哪,如何放置。

堆的特点

而Java的每一个线程都会有一个线程栈与之对应,因为不同的栈运行的业务逻辑不同,所以需要一个独立的线程栈,而堆是所有线程共享的。

堆和栈数据

栈因为是运行单位,里边存储的信息与当前线程或程序的相关信息,包括局部变量,程序运行状态,方法返回值等,而堆只负责存储对象信息。

因为栈先进后出,局部变量会在使用后进行销毁。

cpu在当前运行线程超过CPU线程的时候,CPU就会进行切换,CPU会根据线程的优先级进行处理,如果相同优先级,CPU就会给每个线程分配时钟,第一个线程执行超过时钟就会被挂起,并把当前线程进行快照,然后执行第二个线程,然后挂起第二个线程,然后恢复第一个线程快照,进行往返处理,快照的时候就会保存程序的运行状态,通过PC寄存器(JAVA计数器)。

而堆中的对象信息包括好多,对象头,对象锁,对象属性的数据等。

为什么的要有堆和栈

  1. 栈代表处理逻辑,而堆代表数据,进而隔离和模块化;
  2. 堆和栈进行分开,堆中的内容可以被多个栈共享,即多线程访问一个对象,这样一方面共享提供了一种有效的数据交换方式(例如共享内存),另一方面,堆中的共享常量和缓存可以被所有的栈访问,节省了空间
  3. 栈运行的时候需要保存系统运行的上下文,需要进行地址端的划分,由于栈只能向上增长,因此限制住栈的存储能力,而堆中的对象可以根据需求动态的增长,因此堆和栈的区分可以使栈实现动态的增长,只需要栈记住堆中的地址即可
  4. 对于面向对象,对象的属性就存放在堆中,对象的方法就存放在栈中

栈中存放的堆的引用在32位系统为4byte,在64为系统为8byte,基本类型因为不会增长,长度小,一般在1~8byte

JAVA中的参数传递

首先明确,JAVA中没有指针的概念,其次程序永远是运行在栈内,而参数的传递只能传递基本类型和对象引用,而不会是对象本身。

所以说,当栈调用对象的时候,获取的为堆中对象的值,处理完后返回给对象处理后的值,如果另一个栈再次调用就会是第一个栈处理后的值,这个是CPU的METI同步协议实现的类似锁的机制

一个程序没有堆可以,但是不能没有栈

在JAVA中可以通过-Xss来设置栈的大小,当栈中数据较多的时候就需要调大该值,否则会出现java.lang.StackOverflowError的异常,常见的出现这个异常是因为无法返回递归,因为此时栈中保存的信息都是方法返回的记录点。

对象的引用类型

强引用,软引用,弱引用和虚引用(在1.7和1.8中取消)

  • 强引用会造成内存泄露,因为在垃圾回收的时候不回收强引用
  • 软引用在内存不够的情况下进行回收,如果内存赋予则不会回收,当然如果是OutOfMemory的时候,软引用肯定是不存在的
  • 弱引用在堆不够的情况下,一定会被回收,存在时间为一个垃圾回收周期内

垃圾回收算法

按照基本回收策略分

引用计数

引用计数是一个很古老的算法,对象有一个引用,就会增加一个计数,删除或减少一个引用则会减少一个技术,垃圾回收时,只收集计数为0的对象,次算法的最大问题就是无法解决循环应用的问题

标记清除

标记清楚先给内存中的对象做标记,然后将未标记的进行删除,但是此算法会造成内存碎片,需要暂停应用,如果堆过于大,就可能会暂停好几秒。

复制

复制是把内存划分为两个区域,而每次只使用一个区域,垃圾回收的时候遍历当前使用的区域,把正在使用的对象复制到另一个区域,在复制的过程中进行整理,不过缺点就是需要两倍的内存空间。

标记整理

标记清楚先给内存中的对象做标记,然后将标记过的进行压缩和整理

堆中内存现在eden区,然后是s0和s1区,如果都没有被YGC回收,就会到old区,一般只有20%到old区,还有的方法区

按照系统线程分

串行收集

使用单线程处理所有的回收工作,因为无需多线程进行交互,容易实现,而且效率还比较高,但是局限性也明显,无法使用多处理器的优势,适合在单处理器上使用,在100M的小数据量也可以使用

并行收集

使用多线程处理垃圾回收,速度快,效率高,理论上CPU约多,效率越高

并发收集

前两者在垃圾回收的过程中会暂停运行环境,只有垃圾回收程序在运行,因此系统会有明显的暂停

垃圾回收会面临的问题

所有的垃圾回收会有一个root object,去栈中获取那些对象正在被使用,除了栈之外还有运行的寄存器,所以就以栈还有寄存器为起点,查找堆的对象,以及堆中对象对其他对象的引用,逐渐的扩展到基本类型或null,以对象的根节点形成一个对象树,如果栈中多个引用就是多个对象树,对象树上的都是系统运行需要的,就不会被回收,而剩余的未被引用的就会被回收,最简单的栈就是执行的main函数,回收的方式就是标记清楚的方法

如何处理碎片

对象的存活时间不一定,如果不进行内存碎片的处理,就会造成无法分配较大的内存空间,所以要使用标记整理或者复制的方法

如何解决同时存在的对象创建和对象回收问题

垃圾的回收线程是回收内存,而程序运行线程则是消耗内存的,一个回收内存,一个分配内存,从这点看两者是矛盾的,因此在现有的垃圾回收的过程中,要进行垃圾回收前需要停止应用(停止内存分配),然后进行垃圾的回收,回收完成后再继续应用,这种方式实现是最直接的,进而解决两个的矛盾。

但是这样也有弊端,当堆的空间持续增大的时候,垃圾回收的过程消耗的时间也会增大,一些相对要求很高的应用,暂停时间过长会对应用产生影响,当堆大于几个G的时候,就很有可能超过这个限制,在这种情况下,垃圾回收会成为系统的一个瓶颈,所有有了并发回收算法,使用这种算法,垃圾的回收线程运行的同时程序线程也正常运行,这样解决了暂停问题,但是需要在新生对象的同时回收对象,就会造成了算法的复杂度提高,并且系统的处理能力也会降低,并且碎片的问题还需要解决。

分代回收

虚拟机中划分的年轻代,年老代和持久带,持久带主要存放的就是Java的类的类信息,与垃圾收集要收集的Java对象关系不大,年轻代和老年代的划分对垃圾收集的影响还是比较大的。

年轻代

所有的新生成的对象首先都是放在年轻代,年轻代的目标就是尽快的收集生命周期短的对象,新生代分三个区,一个是Eden区,两个Survivor区,大部分对象在Eden区生成,当Eden区满后还存活的对象将被复制到Survivor区,当这个Survivor区满了会复制存活对象到另一个Survivor区,如果另一个Survivor区也满了,就会把第一个复制到另一个Survivor区并且此时还存活的对象复制到年老区,这两个Survivor是对称的,并没有先后关系。另外年轻代是复制回收的算法

年老代

在年轻代中经历了N次了垃圾回收后仍存活的对象,就会放到年老代,因此可以认为年老代存放的都是生命周期比较长的对象

为什么会有分代回收

所以就有分代回收,对不同生命周期的对象使用不同的回收方式,在Java程序会产生大量的对象,这些对象与业务相关,比如http请求中的session对象,线程socket连接,这些对象都与业务挂钩,因此生命周期长,还有一些临时的变量,这些生命周期就短,例如String对象,这些使用一次就被回收。

在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆进行回收,花费时间会很长,同时,因为每次回收都需要遍历所有存活的对象,对于生命周期长的遍历就是无意义的,所以垃圾分代回收采用把不同生命周期的对象放在不同代上,不同代采用最适合它的垃圾回收方式进行回收。

持久带

持久代用于存放静态文件,如Java的类,方法,持久代对垃圾回收没有显著的影响,但是有些应用可能会动态的生成或者调用一些class,例如Hibernate等,在这个时候就需要设置一个比较持久的持久空间来存放这些运行过程中新增的类,持久代大小通过-Xx:MaxPermSize=进行设置,一般128~256M就可以。

什么时候触发垃圾回收

可以设置阈值进行垃圾的回收,另外就是Scavenge GC和Full GC

Scavenge GC

当新的对象生成的时候,在Eden申请空间失败的时候就会触发GC,对Eden区域内进行GC,清除非存活对象,并且把存活的对象移动到Survivor区,然后整理整个Survivor区,这种GC是在Eden区进行,并不会影响到老年代,以为大部分对象都是从Eden区开始,同时Eden区一般不会很大,所以Eden去的GC会频繁的进行,所以必须采用速度快效率高的算法,为Eden区尽快的腾出空间来

Full GC

对整个堆进行整理,包括年轻代,年老代和持久带,所以比Scavenge GC要慢,因此要尽量的减少这一Full GC的次数,在JVM调优的时候,很大的一部分工作就是对FUll GC的调节,一般在年老代被写满,持久代被写满,或者调用System.gc()被显示调用,上一次GC之后栈中各域分配策略的动态变化等。

垃圾收集的算法

分为串行收集器和并行的收集器,并发收集器

串行的就可以通过-XX:+UseSerialGC打开

并行为-XX:+UseParallelGC打开,年老代如果不指定,默认是使用单线程串行进行垃圾回收,可以使用-XX:+UseParalledlOldGC打开,使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理器数量的几分之一。

也可以指定垃圾回收的过程中最大暂停时间,通过-XX:+MaxGCPauseMillis=来进行指定,单位为毫秒,此值的设定可能会减少应用的吞吐量

吞吐量为垃圾回收的时间与非垃圾回收时间的比值,通过-XX:+GCTimeRatio=来进行设定,公式为1/(1+N),例如-XX:GCTimeRatio=19,表示5%的时间用于垃圾回收,默认情况为99,即1%的时间用于垃圾回收

并发收集器在垃圾回收的时候只暂停较少的时间,适合响应时间要求比较高的应用,通过-XX:+UseConcMarkSweepGC打开,在回收的过程中如果写入新的对象,在回收未结束,新的对象写满完,就会造成失败导致整个堆的回收,可以通过-XX:CMSlnitatingOccupancyFraction=来指定还有多少堆时开始执行并发收集。

总结一下

  • 串行处理器适用于数据量比价小,100M,单处理器下并且对响应时间无要求的应用,缺点是只能用于小型应用
  • 并行处理器适用于对吞吐量有高要求的,多CPU,响应时间无要求的中,大型应用,例如hadoop,缺点就是垃圾回收的过程响应时间较长
  • 并发处理器适用于对响应时间有高要求的,多CPU,对响应要求有较高要求的大中型应用,例如web服务器,应用服务器等

常见配置的汇总

堆设置

  • -Xms初始堆大小
  • -Xmx最大堆大小 一般这两个值大小相等,因为初始增长的时候会有一定的影响业务
  • -XX:NewSize=n设置年轻代大小 如果应用程序中会大量产生新的对象,并且生命周期较短
  • -XX:NewRaitio=n设置年老代和年轻代的比值,如果设为3,表示老年代与年轻代的比值为3:1
  • -XX:SurvivorRaino=n设置年轻代中Eden区与Survivor区的比值,注意这两个Survivor区的大小肯定是一样的,如果设为3,带表Eden:Survivor=3:2,一个Survivor则为1,当然Eden越大,回收的频率就会降低,延迟的时间就会增大
  • -XX:MaxPermSize=n设置持久代的大小 收集器设置
  • -XX:+UseSerialGC串行收集器
  • -XX:+UseParallelGC并行收集器
  • -XX:+UseParalledlOldGC年老代并行收集器(建议并行模式下一直开启)
  • -XX:+UseConcMarkSweepGC并发收集器 垃圾回收统计信息
  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename 并行收集器设置
  • -XX:ParallelGCThreads=设置并行收集器收集时使用的CPU核数,并行收集线程数
  • -XX:MaxGCPauseMillis=设置并行收集最大的暂停时间
  • -XX:GCTimeRatio=设置垃圾回收时间占程序运行时间的最大百分比 并发收集器设置
  • -XX:+CMSlncrementalMode设置为增量模式,适用于单CPU的情况
  • -XX:ParallelGCThreads=设置并发收集器年轻代收集方式为并行收集,并且使用的CPU核数

典型的配置

一下配置主要针对分代垃圾回收

堆大小设置

年轻代的设置非常关键,JVM中堆的大小受三方面限制,操作系统位数,系统可用虚拟内存,物理可用内存,32位一般就1.5~2G了,64位则是没有限制

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k代表最大可用内存3550MB,初始内存3550MB,年轻代大小2GB,每个线程堆栈128k(默认为1MB)

对于年轻代sun公司推荐是堆的3/8,持久代大小一般为64MB

java -Xmx3550m -Xms3550m -Xss128k -XX:NewSize=4 -XX:SurvivorRaino=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

代表年轻代和年老代的比值为4:1,年轻代和Survivor的比值为4:1:1,持久代大小为16m,垃圾最大的年龄为0(年轻代不进入subvivor直接进入年老代,对于年老代的应用可以调高效率。如果设置为一个较大的值,年轻对象会在subvivor中复制,增加在年轻代的存活时间,增加被回收的概率)

回收器的选择

以吞吐量为目标的

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParalledlOldGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

  • -XX:+UseAdaptiveSizePolicy是为了并行收集器会自动选择年轻代大小和相应的Subvivor的比例,以达到目标系统规定的最低响应时间或收集频率,建议并行一直打开 响应时间优先的

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=20 -XX:+UseParNewGC

  • -XX:+UseParNewGC年轻代设置为并行收集,在1.7自动设置

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

  • -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存进行压缩整理,会产生碎片,设置多少次GC后进行内存空间压缩和清理
  • -XX:+UseCMSCompactAtFullCollection打开对老年代的压缩,可能会影响性能

调优的总结

年轻代大小的选择 响应时间优先的应用,尽可能设大,直到接近系统的最低的响应时间限制,这样年轻代收集的频率小,减少达到年老代的对象 吞吐量优先的应用,尽可能设大,因为对响应的时间没有要求,垃圾收集可以并行进行 年老代大小的选择 响应时间优先的应用,年老代使用并发收集器,所以大小需要谨慎设置,需要考虑并发会话率和会话持续时间的参数,如果堆设置的小,会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式,如果堆大就会有较长收集时间,一般需要参考以下数据

  1. 并发垃圾收集信息
  2. 持久代并发收集次数
  3. 传统GC信息
  4. 花在年轻代和年老代回收上的时间比例,减少年轻代和年老代花费的时间,一般会提高效率 吞吐量优先的选择 一般吞吐量优先的应用一般都有一个较大的年轻代和一个较小的年老代,尽可能回收掉大部分短期对象,减少中期对象,而年老代尽可能存放长期存活的对象

调优工具

jconsole,VisualVM和jProfile

  • jconsole为jdk自带,功能简单,在系统有一定的负荷的情况下使用,对垃圾回收算法有详细跟踪
  • VisualVM为jdk自带,功能强大但是复杂
  • jprofile则为付费软件

如何调优

内存的释放情况,集合类检查,对象树

堆信息查看

堆的大小和使用的堆的情况,如果堆的大小只是波动则没什么问题,如果是堆的使用一直在增长就可能会产生内存的泄露,当然这种增长可能是因为用户过多导致的堆内存使用过多导致,需要通过负载均衡器来显示并发的请求,可以查看堆空间大小分配,垃圾回收情况,堆内类,对象信息,正常的情况下会是[c标志的对象多,如果不是该标志可能为内存泄露

内存泄露检查

一般可以理解为系统资源在错误使用的情况下导致使用完毕的资源无法进行回收,从而导致新的资源分配请求无法完成引起的系统错误

年轻代堆空间被占满 java.lang.OutOfMemoryError:Java heap space 产生的情况可以根据垃圾回收前后的情况对比,根据对象的引用情况分析(常见可能出问题的为集合对象引用)

持久代被占满 java.lang.OutOfMemoryError:PermGen space spring框架就会大量占用持久代,解决的办法就是调大持久代

栈内存溢出 java.lang.StackOverflowError 一般就是递归没有返回,或者循环调用造成,再就是栈确实不够了

线程堆栈满 Fatal:Stack size too small 增加栈内存大小

系统使用内存被占满 java.lang.OutOfMemoryError:unable to create new native thread 这可能需要降低栈的内存大小,以便产生更多的线程,并且评估代码是否需要减少线程数

检查的命令为jmap和jstat等,详情可以看一下http://517sou.net/archives/jvm-monitoring-tools

  • jmap -heap 堆信息
  • jmap -histo 类信息

  • jstat -gcutil pid

  • jstat -l 进程ID