java虚拟机概述和基本概念

        大体上虚拟机可以分为系统虚拟机和程序虚拟机,Visual Box、VMware就属于系统虚拟机,它们完全是对物理虚拟机的仿真,提供了一个可以运行完整操作系统的软件平台。程序虚拟机典型代表就是java虚拟机,它专门为执行单个程序而设计,在java虚拟机中执行的指令我们都称为java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都限制于虚拟机提供的资源中。

java虚拟机中的基本结构

velocity vm文件拼接java文件 .java.vm_JVM

(1)类加载子系统

        负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。

(2)方法区

        用来存放类信息,常量信息,常量池信息,包括字符串字面量和数字常量等。

(3)java堆

        在java虚拟机启动的时候创建java堆,他是java程序最主要的内存工作区域。几乎所有的对象实例都存放于java堆中,堆空间是所有线程共享的。

(4)直接内存

        java的NIO库允许java程序使用直接内存,从而提高性能,通常直接内存会优于java堆。读写频繁的场合可能会考虑使用。

(5)java栈

        每个虚拟线程都有一个私有的栈,一个线程的java栈在线程被创建的时候创建,java栈中保存着局部变量、方法参数、同时java的方法调用、返回值等。

(6)本地方法栈

        本地方法栈和java栈非常类似,最大不同为本地方法栈用于本地方法调用,java虚拟机允许java直接调用本地方法(通常使用C编写)。

(7)垃圾回收系统

        垃圾收集系统是java的核心,也是必不可少的,java有一套自己的垃圾清理的机制,开发人员无需手工清理。

(8)PC寄存器

        PC寄存器也是每个线程私有的空间,java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个java线程总在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令,如果是本地方法,则PC寄存器的值为undefined,寄存器存放如当前执行环境指针、程序计数器、操作指针栈、计算的变量指针等信息。

(9)执行引擎

        虚拟机最核心的组件就是执行引擎了,他负责执行虚拟机的字节码,一般会先编译成为机器码后执行。

堆、栈、方法区概念和联系

堆解决的是数据存储的问题,及数据怎么放,放在哪。

栈解决的是程序运行的问题,及程序如何执行或者说如何处理数据。

方法区则是辅助堆、栈的快永久区,解决堆栈信息的产生,是先决条件。

年轻带与老年代区别

        每个对象内部有一个标识,当GC每进行一次垃圾回收,如果该对象还有使用没有被回收就更该他的标识,可以形象的理解为年龄+1,而当GC回收多次,该对象还没有被回收,就通过判断他的标识,如果超过了阈值就将其放入老年区。

新生代分为Eden区,s0区,s1区,s0和s1也被称为from和to区,他们是两块大小相等并且可以互换角色的空间。

绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1区,之后每经过一次新生代回收,如果对象存活他的年龄就+1,当对象达到一定年龄后,就进入老年代。

垃圾收集算法

复制算法:其核心思想就是将内存空间复制为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中,之后清除正在使用的内存块中所有对象,反复交换这两个内存中的角色,完成垃圾回收。(java新生代中的from和to区就是使用这个算法)

引用计数法:这是个比较古老而且经典的垃圾收集算法,其核心就是对象被其他所引用时计数器+1,当引用失效时-1,但是这种方式有非常严重的问题:无法处理循环引用的情况,还有就是每次加减操作比较浪费系统性能。

标记清楚法:就是分为标记和清楚两个阶段进行处理内存中的对象,当然这种方式也有非常大的弊端,就是空间碎片的问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。

标记压缩发:标记压缩法在标记清楚法基础上做了优化。把存活的对象压缩到内存一端,然后进行垃圾清理。(java中老年代就是使用的标记压缩法)。

java栈

java栈是一块线程私有的内存空间,一个栈一般有三个部分组成:局部变量表,操作数栈和帧数据区。

局部变量表:用于报错函数的参数及局部变量。

操作数栈:主要保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。

帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支持常量池的解析。这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外当函数返回或者出现异常时,虚拟局必须有一个异常处理表,方便发送异常时找到出现异常的代码,因此异常处理表也是帧数据区的一部分。

java方法区

java方法区和堆一样,方法区是一块所有线程共享的内存区域。他保存系统的类信息,比如类的字段,方法,常量池等。方法取得大小决定了系统可以保存多少个类。如果系统定义太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出的错误,方法去可以理解为永久区(Perm)。

虚拟机参数

在虚拟机运行过程中,如果可以跟踪系统的运行状态,那么对问题的故障排查会有一定的帮助,为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其实主要就是围绕着堆、栈、方法区进行配置。

堆内存分配参数(1)
-Xms5m          -Xmx20m              -XX:+PrintGCDetails           -XX:+UseSerialGC          -XX:+PrintCommandLineFlags

jvm初始堆内存  最大堆内存 打印GC详细信息 使用串行的垃圾收集器     将隐式参数传给虚拟机输出

velocity vm文件拼接java文件 .java.vm_老年代_02

velocity vm文件拼接java文件 .java.vm_java_03

velocity vm文件拼接java文件 .java.vm_老年代_04

多增加的4M是从max memory

中申请的

velocity vm文件拼接java文件 .java.vm_垃圾回收_05

velocity vm文件拼接java文件 .java.vm_java_06

总结:在实际工作中,我们可以直接将初始堆大小和最大堆大小的值设置相等,这样的好处是可以减少程序运行的垃圾回收次数,从而提高性能。

堆内存分配参数(2)

-Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能和GC行为有很大影响,新生代大小一般会设置整个对空间的1/3到1/4左右。

-XX:SurvivorRatio:用来设置eden空间和from/to区的比例。含义-XX:SurvivorRatio=eden/from=eden/to

(新生代会进行频繁的GC,而老年代中的对象会经历十五六次GC后才放入的,相对稳定,所以GC并不频繁)

velocity vm文件拼接java文件 .java.vm_java_07

第一次配置

-Xms20m             -Xmx20m                     -Xmn1m                 -XX:SurvivorRatio=2                     -XX:+PrintGCDetails
 初始化堆内存20M  堆内存最大值20M        新生代内存1M         eden空间和from/to区的比例         打印GC详细信息-XX:+UseSerialGC

  使用串行化

velocity vm文件拼接java文件 .java.vm_老年代_08

第二次配置

-Xms20m                 -Xmx20m                 -Xmn7m                 -XX:SurvivorRatio=2                         -XX:+PrintGCDetails
初始化堆内存20M  堆内存最大值20M        新生代内存7M         eden空间和from/to区的比例         打印GC详细信息
-XX:+UseSerialGC
  使用串行化

velocity vm文件拼接java文件 .java.vm_老年代_09

第三次配置:
除了可以设置新生代的绝对大小(-Xmn),还可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/年轻代。一般是2或者3。

-Xms20m -Xmx20m     -XX:NewRatio=2         -XX:+PrintGCDetails       -XX:+UseSerialGC

                                    老年代/新生代的比例

velocity vm文件拼接java文件 .java.vm_老年代_10

总结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特显进行合理的配置,当系统一旦运行,大部分情况都会有一些内存对象从运行创建开始到jvm结束一直存活的,所以老年代应该大一些,比例应该在1/3到1/4左右。-XX:NewRatio一般设置在3左右

基本策略:尽可能将对象留在新生代,减少老年代的GC次数。

栈配置:

java虚拟机提供了参数:-Xss来指定现成的最大栈空间,整个参数也直接决定了函数可调用的最大深度。

velocity vm文件拼接java文件 .java.vm_JVM_11

垃圾回收算法及概念

GC中的垃圾特指于内存中,不会再被使用的对象,而回收就相当于把垃圾倒掉。垃圾回收有很多种算法:如引用计数法,标记压缩法,复制算法,分代,分区的思想。

引用计数法:这是个比较古老而且经典的垃圾收集算法,其核心就是对象被其他所引用时计数器+1,当引用失效时-1,但是这种方式有非常严重的问题:无法处理循环引用的情况,包括多重继承时会产生一些列问题,还有就是每次加减操作比较浪费系统性能。

标记清除法:就是分为标记和清除两个阶段进行处理内存中的对象,当然这种方式也有非常大的弊端,就是空间碎片的问题,GC进行回收时并不是严格按照回收5M就准确清理5*1024*1024的空间,包括堆内存的创建并不是指定了5M就会严格产生5*1024*1024的空间,而是会产生一些残留,可以通过JVM参数查看GC工作时的具体信息。垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。 

标记压缩发:标记压缩法在标记清楚法基础上做了优化。把存活的对象压缩到内存一端,然后进行垃圾清理。(java中老年代就是使用的标记压缩法)

复制算法:其核心思想就是将内存空间复制为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中,之后清除正在使用的内存块中所有对象,反复交换这两个内存中的角色,完成垃圾回收。(java新生代中的from和to区就是使用这个算法)

分区算法:这种算法是在oracle收购了java,1.7以后提出的,主要作用就是讲整个内存分为N多个小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收多少个小空间和哪些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC停顿时间。

为什么新生代和老年代使用不同的算法呢?

        因为新生代的GC是非常频繁的,其中的对象非常不稳定,比如其中有100个对象,一次就要回收4、50个,而老年代中的对象已经极力过了很多次GC,说明一直都在被使用,所以可能被GC回收的对象会比较少,比方说100个中回收4、5个,所以讲大部分的对象压缩到一端,只回收一小部分区域中的4、5个对象,没必要使用复制算法,这样更加节约性能。

这样也体现了JVM通过不同的算法和机制来提升GC的性能,这种算法的不同也叫作分代算法。

垃圾回收时的停顿现象:

垃圾回收器的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效地执行,大部分情况下,会要求系统进入一个停顿状态。停顿的目的是终止所有的应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态在某个瞬间的一致性,也有益于更好地标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。

对象如何进入老年代:

一般而言对象首次被创建会被放在新生代的eden区,如果没有GC介入,则对象不会离开eden区,那么eden区的对象如何进入老年代呢?一般来讲,只要对象的年龄达到一定大小就会离开新生代进入老年区,对象年龄是由对象经历过的GC次数决定的。在新生代每次GC过后如果对象没有被回收,则年龄加1,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会进入老年代。

控制新生代对象的最大年龄参数: -XX:MaxTenuringThreshold,默认情况下为15.

刚创建的对象都在eden区:

velocity vm文件拼接java文件 .java.vm_老年代_12

新生代的eden区被使用了35%,老年代完全没有被使用

velocity vm文件拼接java文件 .java.vm_老年代_13

测试对象进入老年代过程,使用参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15
-XX:+PrintGCDetails

velocity vm文件拼接java文件 .java.vm_老年代_14

velocity vm文件拼接java文件 .java.vm_JVM_15

另外,大对象(新生代eden区无法装入时,也会直接进入老年代)。JVM里有参数(-XX:PretenureSizeThreshold)可以直接设置对象的大小在超过指定大小后,直接进入老年代

使用参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1024000

  

velocity vm文件拼接java文件 .java.vm_java_16

因为参数XX:PretenureSizeThreshold的值:1024000<new byte[1024*1024]的值,可见大部分数据进入了老年区

velocity vm文件拼接java文件 .java.vm_JVM_17

但是当换一种方式后,参数为:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 

velocity vm文件拼接java文件 .java.vm_老年代_18

虽然new byte[1024];的容量>-XX:PretenureSizeThreshold的1000 ,但看下图大量的对象还是进入了eden

velocity vm文件拼接java文件 .java.vm_JVM_19

这是因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会

加上参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB(-为禁止使用该功能),重新测试

velocity vm文件拼接java文件 .java.vm_JVM_20

这次可见大多数据进入了老年区。

TLAB

        TLAB全称是:Thread Local Allocation Buffer即线程本地缓存分配,从名称上看是一个线程专用的内存分配区域。是为了加速个线程执行效率而生的(volatile是为了该线程不再读取自己的内存缓存,而强制该线程回主内存中取数据,该线程的内存缓存就是TLAB)。每个线程都会产生一个TLAB,该线程独享的工作区域,java虚拟机使用这种TLAB区避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法再TLAB中分配时,直接分配在堆中。

        jdk1.7后TLAB虚拟机会自动进行调整,所以如果对TLAB不熟悉,不建议随意对其改动。

对象的创建流程

velocity vm文件拼接java文件 .java.vm_老年代_21

垃圾收集器

在java的虚拟机中,垃圾回收器不仅仅只有一种,什么情况下该使用哪种,都是我们需要了解的:

串行垃圾回收器(较古老)

串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程,对并行能力(配置较弱)较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现,串行回收器可以在新生代和老年代使用,根据不同的堆空间分为,新生代串行回收器和老年代串行回收器。使用-XX:+UseSerialGC  参数可以设置新生代串行回收器和老年代串行回收器

并行垃圾回收器

并行回收器可以使用多个线程同时进行垃圾回收,对计算能力强的计算机而言,可以有效地缩短垃圾回收所需的实际时间。

ParNew回收器(较古老)是一个工作在新生代的垃圾收集器,他只是简单地将串行回收器多线程化,他的回收策略和算法和串行回收器一样。

使用-XX:+UseParNewGC    新生代parnew回收器,老年代则使用串行回收器,ParNew回收器工作室的线程数量可以使用

-XX:ParallelGCThreads参数指定,一般最好和计算机的CPU相当,避免过多的线程影响性能。

新生代ParalleIGC回收器(现在使用),使用了复制算法的收集器,也是多线程独占形式的收集器,但是ParalleIGC有个非常重要的特点,就是非常关注系统的吞吐量。
提供了两个非常重要的参数设置系统的吞吐量,
-XX:MaxGCPauseMillis:设置最大垃圾停顿时间,可用把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间MaxGCPauseMillis可以设置很小,但会导致GC频繁,从而增加了GC的总时间,降低了吞吐量,所以需要根据实际情况设置该值。
-XX:GCTimeRatio:设置吞吐量的大小,他是一个0--100之间的整数,默认情况下取值是99,那么系统将花费不超过

1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。

另外,还可以指定-XX:UseAdaptiveSizePolicy打开自适应模式,在这种模式下,新生代的大小,eden,from/to的比例,以及晋升老年代的年龄参数会被自动调整,已达到堆大小,吞吐量和停顿时间之间的平衡点。

老年代ParalleIOldGC回收器(现在使用)也是一种多线程的回收器,和新生代的ParalleIGC回收器一样,也是一种关注吞吐量的回收器,它使用了标记压缩算法进行实现。

-XX:+UseParalleIOldGC:设置使用该收集器
-XX:+ParalleIGCThreads:设置垃圾收集时的线程数量

CMS回收器(在1.7是主流,目前最成熟的一种)

CMS全称为Concurrent Mark Sweep意为并发标记清除,它使用的是标记清除法,主要关注系统停顿时间。
        使用-XX:+UseConcMarkSweepGC 进行使用设置
        使用-XX:ConcGCThreads 设置线程并发数量

-XX:CMSInitiatingOccupancyFraction来指定,默认为68,也就是说当老年代的使用率达到68%的时候,会执行CMS回收。如果内存使用增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS就会回收失败,虚拟机将启用老年代串行回收器进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC可能停顿时间较长,所以-XX:CMSInitiatingOccupancyFraction的值要根据实际情况来设置。

    之前有提到标记清除法有个缺点就是会产生内存碎片的问题,那么CMS有个参数设置:
-XX:UseCMSCompactAtFullCollection可以使CMS回收完成后进行一次碎片整理,-XX:CMSFullGCsBeforeCompaction参数可以设置进行多少次CMS后,堆内存进行一次压缩。

新生代中的数据并不稳定,老年代较为稳定,并且老年代中可能会有大量数据需要停留。