【总结】JVM模型

  • JVM的每个实例都有一个它自己的方法域一个堆,运行于JVM内的所有的线程都共享这些区域
  • 虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中
  • 当程序运行的时候,JVM把程序初始化的所有对象置于
  • 每个线程创建的时候,都会拥有自己的程序计数器Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态
  • 本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现


组成部分:
  • 执行引擎
    • 它的行为由指令集决定,每条指令规范说明当JVM执行字节码遇到指令时候,它的实现应该做什么
    • JVM支持248个字节码,每个字节码执行基本的CPU运算,比如:把一个整数加到寄存器
    • 指令包含一个单字节的操作符,还有0个或多个操作数,提供操作所需的参数和数据,需要指令没有操作数,仅由一个单字节的操作符构成
  • JVM寄存器
    • pc: Java程序计数器
      • 每个线程一旦被创建就拥有了自己的程序计数器
      • 线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义
    • optop: 指向操作数栈顶端的指针
    • frame: 指向当前执行方法的执行环境的指针
    • vars: 指向当前执行方法的局部变量区第一个变量的指针
  • JVM栈
    • 局部变量区
      • 每个Java方法使用一个固定大小的局部变量集,它们按照与vars寄存器的字偏移量来寻址
      • 局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址
      • 虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈指令,也提供了把操作数栈中的写入局部变量指令
    • 运行环境:在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉
      • 动态链接
        • 运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class文件代码在引用要调用的方法和要访问的变量时使用符号
        • 动态链接符号形式的方法调用翻译成实际方法调用,装载必要的类解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址
        • 动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码
      • 正常的方法返回
        • 如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值
        • 执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去
      • 异常捕捉
    • 操作数区
      • 机器指令只从操作数栈中操作数,对它们进行操作,并把结果返回到
      • 选择栈结构的原因是: 在只有少量寄存器或非通用寄存器的机器(如 Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果
      • 每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置
      • 本地方法栈
        • 当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈
        • 在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同
  • 所有的数据程序都在运行数据区存放
    • Stack栈
      • 栈也叫栈内存,是Java程序的运行区,是在线程创建时创建它的生命期是跟随线程的生命期线程结束栈内存也就释放
      • 栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据数据集当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则
      • 栈帧中主要保存3类数据
        • 本地变量(Local Variables):包括输入参数输出参数以及方法内的变量
        • 栈操作(Operand Stack):记录出栈入栈的操作
        • 栈帧数据(Frame Data):包括类文件方法等等
    • Heap堆内存
      • 一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的
      • 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存,以方便执行器执行
      • 堆内存分为三部分
        • Permanent Space 永久存储区
          • 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息
          • 被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存
        • Young Generation Space 新生区
          • 新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命
          • 新生区又分为两部分:伊甸区(Eden space)幸存者区(Survivor pace)
            • 所有的类都是在伊甸区new出来的。
            • 幸存区有两个: 0区(Survivor 0 space)1区(Survivor 1 space)
              • 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区
              • 若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区
        • Tenure generation space养老区
          • 养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃
    • Method Area 方法区
      • 方法区是被所有线程共享,该区域保存所有字段方法字节码,以及一些特殊方法如构造函数接口代码也在此定义
    • PC Register 程序计数器
      • 每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令
    • Native Method Stack 本地方法栈

垃圾回收原理:
  • Minor GC(Young GC)
    • 每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0
    • 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)
    • 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代
  • Full GC
    • 当年老代内存不足时, 将执行Major GC,也叫 Full GC
    • 对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少

什么时候发生GC
  • CPU空闲的时候
  • 创建对象没有空间的时候进行GC,如何还没空间,在GC一次,在没空间,就报OutOfMemory错误

2. 垃圾收集的算法分析
  Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
  大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾回收首先需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。
   2.1. 引用计数法(Reference Counting Collector)
  引用计数法是 唯一没有使用 根集的垃圾回收的法,该算法 使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
  基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。
   2.2. tracing算法(Tracing Collector)
  tracing算法是为了解决引用计数法的问题而提出,它使用了 根集的概念。基于tracing算法的垃圾收集器 从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为 标记和清除(mark-and-sweep)垃圾收集器.
   2.3. compacting算法(Compacting Collector)
  为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法 将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,一般增加 句柄句柄表
   2.4. copying算法(Coping Collector)
  该算法的提出是为了 克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆 分成一个对象区和多个空闲区,程序从对象区为对象分配空间,当对象满了,基于coping算法的垃圾回收就从根集中扫描活动对象,并 将每个活动对象复制到空闲区(使得活动对象所占的内存之间没有空闲间隔),这样 空闲区变成了对象区,原来的对象区变成了空闲区,程序会在新的对象区中分配内存。
  一种典型的基于coping算法的垃圾回收是 stop-and-copy算法,它将堆分成对象区和空闲区域区,在对象区与空闲区域的切换过程中,程序暂停执行
   2.5. generation算法(Generational Collector)
  stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此, generation算法将堆分成两个或多个,每个子堆作为对象的一代 (generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。
   2.6. adaptive算法(Adaptive Collector)
  在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

一句话总结:
  • 存放对象的,但是对象内的临时变量是存在栈内存
  • 虚拟机启动时创建。Java堆的唯一目的就是存放对象实例绝大部分的对象实例都在这里分配
  • 是跟随线程的,有线程就有栈是跟随JVM的,有JVM就有堆内存
  • 静态变量存在方法区中,实例变量存在堆内存
  • 原生数据类型传递的引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去
  • 一台机器可以有多个JVM
  • Heap和Method Area是共享的,其他都是私有的
  • 其他对象
    • 常量池(constant pool)
      • 按照顺序存放程序中的常量,并且进行索引编号的区域
    • 安全管理器(Security Manager)
      • 提供Java运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader在Security Manager认证通过后才能加载class文件的
    • 方法索引表(Methods table)
      • 记录的是每个method的地址信息Stack和Heap中的地址指针其实是指向Methods table地址
  • 不建议在程序中显式的生命System.gc(),因为显式声明是做堆内存全扫描,也就是Full GC,是需要停止所有的活动的(Stop  The World Collection)
  • 方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等
  • JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据
  • 程序计数器、VM栈、本地方法栈三个区域随线程而生,随线程而灭
  • 栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操作;每一个帧中分配多少内存基本上是在Class文件生成时就已知的
  • 运行时的单位,而存储的单位
  • 解决程序的运行问题,即程序如何执行,或者说如何处理数据解决的是数据存储的问题,即数据怎么放、放在哪儿
  • 中存的是对象中存的是基本数据类型堆中对象的引用
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
  • 从软件设计的角度看,代表了处理逻辑,而代表了数据。这样分开,使得处理逻辑更为清晰分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现
  • 堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间
  • 因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可
  • 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美
JVM调优:
  • JVM中最大堆大小有三方面限制
    • 相关操作系统的数据模型(32-bt还是64-bit)限制
    • 系统的可用虚拟内存限制
    • 系统的可用物理内存限制32位系统下,一般限制在1.5G~2G64为操作系统对内存无限制。在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m)
  • 堆典型配置
    • -Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
    • -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
    • -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    • -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
    • -XX:MaxPermSize=16m:设置持久代大小为16m
    • -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  • 回收器选择
    • 串行收集器
      • 串行收集器只适用于小数据量的情况
      • 默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断
    • 并行收集器:吞吐量优先的并行收集器
      • 并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理
    • 并发收集器:响应时间优先的并发收集器
      • 并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域
常见配置汇总
 
堆设置
  -Xms:初始堆大小
  -Xmx:最大堆大小
  -XX:NewSize=n:设置年轻代大小
  -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  -XX:MaxPermSize=n:设置持久代大小
收集器设置
  -XX:+UseSerialGC:设置串行收集器
  -XX:+UseParallelGC:设置并行收集器
  -XX:+UseParalledlOldGC:设置并行年老代收集器
  -XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps
  -Xloggc:filename
并行收集器设置
  -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

调优总结

年轻代大小选择
响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用
 
 
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
  1. 并发垃圾收集信息
  2. 持久代并发收集次数
  3. 传统GC信息
  4. 花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
 
 
吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象减少中期的对象,而年老代尽存放长期存活对象
 
 
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
    1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

JVM收集器
  • Serial收集器 
    • 单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。 
  • ParNew收集器 
    • ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。 
  • Parallel Scavenge收集器 
    • Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。 
  • Serial Old收集器 
    • Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。 
  • Parallel Old收集器 
    • 老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
  • CMS(Concurrent Mark Sweep)收集器 
    • CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。 








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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值