虚拟机,就是一台虚拟的机器。它是一款软件,用来执行一系列虚拟计算机指令,大体上虚拟机可以分为系统虚拟机和程序虚拟机。


系统虚拟机:例如Visual Box、VMare,它们完全是对物理计算机的仿真,提供了一个可以运行完整操作系统的软件平台。


程序虚拟机:例如JVM,它专门为执行单个计算机程序而设计,在JVM中执行的指令为Java的字节码指令。


无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。


在Java发展中,有很多虚拟机比如SUN的HotSpot, BEAde JRockit等,不过JRockit也被Oracle收购了。





JVM虚拟机原理_老年代


1.方法区(各个线程共享)
又称:永久区、非堆
存放:类信息、常量、静态变量
大小:默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize和-XX:MaxPermSize参数限制方法区的大小。

2.虚拟机栈(线程私有、生命周期与线程一致)
存放:局部变量表(包括参数)、操作栈、方法出口等信息。
局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用,其中64位长度的long和double类型的数据会占用2个局部的变量空间slot,其余占用1个。

3.本地方法栈
与虚拟机栈类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。

4.堆(各个线程共享)
又称:java堆、GC堆
是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。
该内存存放对象实例和数据(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置。
-Xms:为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G
-Xmx:为JVM可申请的最大内存,默认为物理内存的1/4但小于1G
通常-Xms和-Xmx的值设成一样。
现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)仍然存活的对象。

新生代:
新创建的不大的对象都是从新生代分配内存,新生代有Eden Space和两块相同大小的Survivor Space(SO和S1/From和To)构成。可以通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。

老年代:
用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:
(1)大对象,可以通过设置参数-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
(2)大的数据对象,且数组中无引用外部对象。
老年代所占的内存大小为-Xmx对象的值减去-Xmn对应的值。

5.程序计数器
是最小的内存区域,它的作用是记录当前程序所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器的工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,异常处理,线程恢复等基础功能都需要依赖计数器完成。






一、JVM虚拟机结构


1. 类加载器


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


2. 方法区


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


3. 堆


在JVM启动的时候,建立堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放在堆中,堆空间是所有程序共享的。


4. 直接内存


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


5. 栈


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


6. 本地方法栈


和栈非常的类似,最大的不同为本地的方法栈用于本地方法调用。JVM允许Java直接调用本地方法。


7.垃圾回收系统


是JVM的核心。


8.PC寄存器


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


9. 执行引擎


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




二、堆/栈/方法区的概念和联系


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


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


方法区是辅助堆栈的永久区(Perm Gen),解决堆栈信息的产生。


若创建一个对象Person,那么Person类的一些信息(类信息、静态信息都存在于方法区中)


而当Person被实例化后,被存储到堆中,一块内存空间。


当要去使用的时候,都是使用Person对象的引用,形如Person person = new Person()


这里person是存在栈中,它是Person对象的一个引用。




二、Java堆


Java堆和Java应用程序关系最密切的内存空间,几乎所有的对象都是存放在其中,并且Java堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示地释放。


根据垃圾回收机制的不同,Java堆可能有不同的结构。最为常用的就是将整个Java堆分为新生代和老年代。其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。


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


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




三、Java栈


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


局部变量表:用于保存函数的参数及局部变量。


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


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




四、Java方法区


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




五、虚拟机参数


进行虚拟机参数配置,其实主要就是围绕着堆/栈/方法区进行配置。


5.1 堆分配参数(一)


-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志


-XX:+UseSerialGC 配置串行回收器


-XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况


-Xms: Java程序启动时初始堆的大小


-Xmx: Java程序能获得的最大堆的大小


例如:-Xmx20m -Xms5m -XX:PrintCommandLineFlags


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




5.2 堆分配参数(二)


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


-XX:SurvivorRatio


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


除了可以设置新生代的绝对代大小(-Xmn),还可以使用-XX:NewRatio设置新生代和老年代的比例:-XX:NewRatio=新年代/老生代


这个比例设为1:2或者1:3是比价合适的。


-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Test03.dump


将OOM错误保存在Test03.dump文件中,这样可以用内存分析工具进行分析。




5.3 栈分配参数


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




5.4 方法区分配参数


-XX:MaxPermSize为64MB,


-XX:PermSize=64M -XX:MaxPermSize=64M




5.5 直接内存配置


-XX:MaxDirectMemorySize,如果不是设置,默认的是最大的堆空间,即-Xmx




七、垃圾回收的概念


GC,指清理存在于内存中的、不会再使用的对象。


有很多算法,比如:引用计数法、标记压缩法、复制算法、分代、分区的思想等。




八、垃圾回收算法


引用计数算法:核心就是在对象被其他所引用时计数器加1,而当引用失效时则减1,但是这种方法有非常严重的问题:无法处理循环引用的情况、还有就是每次进行加减操作比较浪费系统性能。


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


复制算法:其核心思想是将内存分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之前正在使用的内存块中所有的对象。反复去交换两个内存的角色,完成垃圾收集。


(Java中新生代的from和to空间就是使用这个算法)


标记压缩算法:标记压缩法在标记清除法基础上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。


(Java中老年代使用的就是标记压缩法)




在新生代中,新实例化的对象先存在于eden区,发生GC后,将依然存在的对象转入S0(或者S1)区内,当S0区GC时,依然存活的对象复制到S1区后,将S0内部内的对象移除。当S1区GC时,依然存活的对象复制到S0区后,将S1内部内的对象移除。




那为什么新生代(复制算法)和老年(标记压缩算法)代使用不同的算法?


因为:新生代GC比较频繁。


老年的对象是相对来说比较稳定,若复制算法的话,那需要复制很多了。




分代算法:就是根据对象的特点把内存分成N块,然后根据每个内存的特点使用不同的算法。


对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时都很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC。




分区算法:将整个内存分为N个独立的空间,每个小空间都可以独立的使用,这样细粒度的控制一次回收多少个小空间和那些小空间,而不是针对整个空间进行GC, 从而提升性能,并减少GC的停顿时间。




九、垃圾回收时的停顿现象


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




十、对象如何进入老年代


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


-XX:MaxTenuringThreshold,默认情况下为15。


总结:根据设置的MaxTenuringThreshold参数,可以指定新生代对象经过多少次回收后进入老年代。


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


-XX:PretenureSizeThreshold.


总结:使用PretenureSizeThreshold可以进入指定进入老年代的对象大小,但是要注意TLAB区域优先分配空间。


对于体积不大的对象,会优先把数据分配到TLAB区域,因此老年代会失去优先分配的机会。可以禁用-XX:-UserTLAB




TLAB区:(联想volatile关键字)


TLAB全称是Thread Local Allocation Buffer即线程本地分配缓存,是一个线程专用的内存分配区域,是为了加速对象分配而生的。每个线程都会产生一个TLAB,该线程独享的工作区域,JVM使用这种TLAB区来避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法再TLAB分配时,则会直接分配到堆上。


-XX:+UserTLAB


-XX:+TLABSize


-XX:TLABRefillWasterFraction设置维护进入TLAB空间的单个对象大小,是一个比值,默认为64,即如果对象大于整个空间的1/64,则在堆创建对象。


-XX:+PrintTLAB查看TLAB信息


-XX:ResizeTLAB自动调整TLABRefillWasteFraction阀值。




十一、对象创建流程图


一个对象创建在什么位置,JVM会有一个比较细节的流程,根据流程的大小,参数的设置,决定如何创建分配以及其位置。





十二、垃圾收集器


JVM中,垃圾收集器不仅仅只有一种。


串行收集器


并行收集器


CMS回收器


G1回收器




串行收集器:


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


使用-XX:+UseSerialGC参数可以设置使用新生代串行回收器和老年代串行回收器。




并行收集器:


在串行回收器基础上做了改进,可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短垃圾回收所需要的实际时间。




1. ParNew回收器


是一个工作在新生代的垃圾回收器,只是简单的将串行回收器多线程化,回收策略和算法和串行回收器一样。


使用-XX:+UserParNewGC新生代ParNew回收器,老年代则使用串行回收器。


ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定,一般最好使用和计算机的CPU相当,避免过多的线程影响性能。




2. ParallelGC回收器


使用了复制算法的收集器,是多线程独占形式的收集器,但ParallelGC回收器有个非常重要的特点,非常关注系统的吞吐量。


提供了2个非常关键的参数控制系统的吞吐量


-XX:MaxGCPauseMilis:设置最大垃圾收集停顿时间,可把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁,从而增加了GC的总时间,降低了吞吐量,所以要根据实际情况设置该值。


-XX:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下它的取值是99,那么系统将花费不超过1/(1 + n)的时间用于垃圾回收,也就是1/(1+99) = 1%的时间。


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


3. ParallelOldGC回收器


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


-XX:+UseParallelOldGC:进行设置


-XX:+ParallelGCThreads:设置垃圾回收收集时的线程数量。




CMS回收器:


Concurrent Mark Sweep意为并发标记清除,使用的是标记清除法,主要关注系统停顿时间。


使用:


-XX:+UserConcMarkSweepGC


-XX:+ConcGCThreads设置并发线程的数量。


CMS并不是独占式的回收器,也就是说CMS回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断地产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用。CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一个阀值的时候开始回收,回收阀值可用指定的参数进行配置,


-XX:CMSInitiatingOccupancyFraction


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


的设置要根据实际的情况。


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




G1回收器:


Garbage-First是在JDK1.7之后提出的垃圾回收器,从长期目标来看,是为了取代CMS回收器,G1回收器拥有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代、老年代空间都连续,它使用分区算法。


并行性:G1回收期间可多线程同时工作。


并发性:G1拥有与应用程序交替执行能力,部分工作可与应用程序同时执行,在这个GC期间不会完全阻塞应用程序。


分代GC:G1依然是一个分代的收集器,但是它是兼顾新声代和老年代一起工作,之前的垃圾收集器它们或者是在新生代工作或者在老年代工作,因此这是一个很大的不同。


空间整理:G1在回收过程中,不会像CMS那样在若干次GC后需要进行碎片整理,G1采用了有效复制对象的方式,减少空间碎片。


可预见性:由于分区的原因。G1可以只选择部分分区进行回收,缩小了回收的范围,提升了性能。


使用:


-XX:+UseG1GC:使用G1收集器


-XX:MaxGCPauseMillis:指定最大的停顿时间


-XX:ParallelGCThreads:设置并行回收的线程数量