jvm详解

[TOC]

1、Java虚拟机概述和基本概念

1.1 Java虚拟机的原理

​ 所谓的虚拟机,就是一台虚拟的机器。它是一款软件,用来执行一系列虚拟计算机指令。大体上虚拟机分为系统虚拟机程序虚拟机,大名鼎鼎的Visual Box、VMare就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。Java发展至今,出现过很多的虚拟机,最初Sun使用的一款叫Classic的Java虚拟机,到现在引用最广泛的是HotSpot虚拟机。除了Sun以外,还有BEA的JRockit,目前JRockit和HotSpot都被Oracle收入旗下,大有整合的趋势。

1.2 认识Java虚拟机

  • 类加载器子系统:负责从文件系统或网络中加载Class信息,加载的信息存放在一块称为方法区的内存空间。
  • 方法区:就是存放类信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
  • Java堆:在java虚拟机启动的时候建立java堆,它是Java程序最主要的内存工作区域,几乎所有的对象的实例都存放在Java堆中,堆空间是所有的线程共享的。
  • 直接内存:Java的NIO库允许Java程序使用直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
  • JVM栈:每个虚拟机线程都有一个私有的栈,一个线程的Java栈在线程创建的时候被创建,java栈中保存着局部变量、方法参数、同时java方法调用、返回值等
  • 本地方法栈:和java栈非常相似,最大的不同为本地为本地方法栈用于本地方法调用。java虚拟机允许java直接调用本地方法(通常使用C编写)
  • 垃圾收集系统:是java的核心,也是必不可少的,java有一套自己进行垃圾清理的机制,开发人员无需手工清理,稍后在第三节我们详细说明。
  • PC(Program Counter):寄存器也是每个线程私有的空间,java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个java线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令,如果是本地方法,则PC寄存器为undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等信息。
  • 虚拟机最核心的就是执行引擎了,它负责执行虚拟机的字节码。一般进行编译成机器码后执行。

2、堆、栈、方法区

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

堆解决的数据存储的问题,即数据怎么放、放在哪个。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
方法区则是辅助堆栈的永久区(Perm),解决堆栈信息的产生,是先决条件。
比如:我们创建一个新的对象,User,那么User类的一些信息(类信息、静态信息都存放在方法区中)

  • 而User类实例化后,被存储到java堆中
  • 当我们使用的时候,都是使用user对象的引用,形如User user = new User();
  • 这里的user存放在java的栈中,即User真是对象时一个引用

2.2 辨清java堆

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

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

新生代分为eden区、s0区、s1区,s0和s1也成为from和to区域,它们是两块大小相等并且可能相互转换角色的空间

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

2.3 java栈

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

  • 局部变量表:用于报错函数的参数以及局部变量
  • 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
  • 帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支持常量池的解析,这里的帧数据区保存着访问常量池的指针,方便程序访问常量池,另外,当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表噎死帧数据区的一部分。

2.4 java方法区

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

3、了解虚拟机参数

3.1 虚拟机参数

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

3.2 堆分配参数(一)

命令 说明
-XX:+PrintGC 虚拟机启动后,只要遇到GC就会打印参数
-XX:+UserSerialGC 配置串行回收器
-XX:PrintGCDetails 可以查看详细信息,包括各个区的情况
-Xms 设置程序启动的时候初始堆大小
-Xmx 设置程序启动的时候最大堆大小
-XX:+PrintCommandLineFlags 可以显示或隐式给虚拟机的参数输出

实例: Test01

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

3.3 堆分配参数 (二)

参数 说明
-Xmn 设置新生代大小,新生代通常为堆空间的1/3到1/4左右
-XX:SurvivorRatio 用来设置新生代中eden\from\to的空间比例

-XX:SurvivorRatio=eden/from=eden/to

实例:Test02

总结:在不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽量可能将对象预留在新生代,减少老年代GC次数。

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

3.4 堆溢出处理

在Java程序的运行过程中,如果堆空间不足,则会抛出内存溢出的错误(OutOfMemory)OOM,java虚拟机提供了-XX:+HeapDumpOnOutOfMemoryError,使用该参数可以在内存溢出导出整个堆信息,与之配合使用的还有参数,-XX:HeapDumpPath,可以设置导出路径

内存分析工具:Memory Analyzer 1.5.0

实例:Test03

3.5 栈配置

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

实例:Test04

3.6 方法区

可以使用-XX:MaxPremSize-XX:PermSize进行配置 默认情况为64m。

3.7 Client和Server虚拟机模式

可以通过java -version查看虚拟机的模式,可以通过-client指定为Client模式

Client模式启动较快,如果不追求系统的长时间使用可以使用Client模式。

JVM一个不错的博客

4、垃圾回收概念和算法

一、对象存活判断

判断对象是否存活一般有两种方式:

  • 1.引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 2.可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

虚拟机栈中引用的对象。

方法区中类静态属性实体引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI引用的对象。

二、JVM的垃圾回收过程

首先从GC Roots开始进行可达性分析,判断哪些是不可达对象。

对于不可达对象,判断是否需要执行其finalize方法,如果对象没有覆盖finalize方法或已经执行过finalize方法则视为不需要执行,进行回收;如果需要,则把对象加入F-Queue队列。

对于F-Queue队列里的对象,稍后虚拟机会自动建立一个低优先级的线程去触发其finalize方法,但不会等待这个方法返回。

如果在finalize方法的执行过程中,对象重新被引用,那么进行第二次标记时将被移出F-Queue,在finalize方法执行完成后,对象仍然没有被引用,则进行回收。

对于被移出F-Queue的对象,如果它下一次面临回收时,将不会再执行其finalize方法。

finalize方法只执行一次。

三、垃圾收集算法

  • 1、引用计数(reference counting)

​ 原理:此对象有一个引用,则+1;删除一个引用,则-1。只用收集计数为0的对象。

​ 缺点:无法处理循环引用的问题。如:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。

  • 2、复制(copying)

​ 原理:把内存空间划分为2个相等的区域,每次只使用一个区域。垃圾回收时,遍历当前使用区域,把正在使用的对象复制到另外一个区域。

​ 优点:不会出现碎片问题。

​ 缺点:1、暂停整个应用。2、需要2倍的内存空间。

  • 3、标记-清扫(Mark-and-sweep)—sun前期版本就是用这个技术。

​ 原理:对于“活”的对象,一定可以追溯到其存活在堆栈、静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。第一阶段:从GC roots开始遍历所有的引用,对有活的对象进行标记。第二阶段:对堆进行遍历,把未标记的对象进行清除。这个解决了循环引用的问题。

​ 缺点:1、暂停整个应用;2、会产生内存碎片。

  • 4、标记-压缩(Mark-Compact)自适应

​ 原理:第一阶段标记活的对象,第二阶段把为标记的对象压缩到堆的其中一块,按顺序放。

​ 优点:1、避免标记扫描的碎片问题;2、避免停止复制的空间问题。

​ 具体使用什么方法GC,Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-扫描”方式;同样,Java虚拟机会跟踪“标记-扫描”的效果,要是堆空间碎片出现很多碎片,就会切换回“停止-复制”模式。这就是自适应的技术。

  • 5、分代(generational collecting)—–J2SE1.2以后使用此算法

​ 原理:基于对象生命周期分析得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同的生命周期使用不同的算法(2-3方法中的一个即4自适应)进行回收。

  • 6、自适应算法(Adaptive Collector)

​ 在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

年轻代(young)

年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到tenured generation。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个 Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。young generation的gc称为minor gc。经过数次minor gc,依旧存活的对象,将被移出young generation,移到tenured generation。

年老代(tenured)

存放从年轻代(young)复制过来的对象。生命周期较长的对象,归入到tenured generation。一般是经过多次minor gc,还依旧存活的对象,将移入到tenured generation。(当然,在minor gc中如果存活的对象的超过survivor的容量,放不下的对象会直接移入到tenured generation)。tenured generation的gc称为major gc,就是通常说的full gc。

​ 采用compaction算法。由于tenured generaion区域比较大,而且通常对象生命周期都比较长,compaction需要一定时间。所以这部分的gc时间比较长。

​ minor gc可能引发full gc。当eden+from space的空间大于tenured generation区的剩余空间时,会引发full gc。这是悲观算法,要确保eden+from space的对象如果都存活,必须有足够的tenured generation空间存放这些对象。

持久代(perm)

​ 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著的影响,但是有些应用可能动态生成或者调用一些class。持久代大小通过-XX:MaxPermSize=N进行设置

该区域比较稳定,主要用于存放classloader信息,比如类信息和method信息。

对于spring hibernate这些需要动态类型支持的框架,这个区域需要足够的空间。(这部分空间应该存在于方法区而不是heap中)。

四、Minor collections和Major collections

Minor collection当young space被占满时执行。它比major collections快,因为minor collection仅仅检查major collection相应的一个子集对象。minor collection比major collection发生的频率高。

Major collection当tenured space被占满时执行。他会清理tenured和young。

Thinking in java给java gc取了一个罗嗦的称呼:“自适应、分代的、停止-复制、标记-扫描”式的垃圾回收器。

五、导致Gc的情况:

1、tenured被写满

2、perm被写满

3、System.gc()的显式调用。

4、上一次GC之后heap的各域分配策略动态变化。

六、GC运行的三种方式

在java5和java6中有4中垃圾回收的算法,有一种算法将不再支持,剩余的三种垃圾回收算法是:serial, throughput and concurrent low pause。

Stop the world(停止所有程序的方式):在这种方式运行的GC,在GC完成前,JVM中的所有程序都不允许运行。Serial collector此时做minor和major收集。Throughput collector此时做major collector。

Incremental(增量运行方式):目前没要Java GC算法支持这种运行方式。GC以这种方式运行时,GC允许程序做一小段时间的工作,然后做垃圾回收工作。

Concurrent(并行运行):Throughput collector此时做minor collect,Concurrent low pause collector此时做minor和major收集。在这种运行方式下,GC和程序并行的运行,因此程序仅仅被短暂的暂停。

七、关于finalize方法的问题

finalize方法使得GC过程做了更多的事情,增加的GC的负担。

如果某个对象的finalize方法运行时间过长,它会使得其他对象的finalize方法被延迟执行。

finalize方法中如果创建了strong reference引用了其他对象,这会阻止此对象被GC。

finalize方法有可能以不可确定的顺序执行(也就是说要在安全性要求严格的场景中尽量避免使用finalize方法)。

不确保finalize方法会被及时调用,也许程序都退出了,但是finalize方法还没被调用。

八、对象引用的类型

Reference(or named Strong Reference)( 强引用):普通类型的引用。

SoftReference( 软引用):被这种引用指向的对象,如果此对象没要再被其他Strong Reference引用的话,可能在任何时候被GC。虽然是可能在任何时候被GC,但是通常是在可用内存数比较低的时候,并且在程序抛出OutOfMemoryError之前才发生对此对象的GC。SoftReference通常被用作实现Cache的对象引用,如果这个对象被GC了,那么他可以在任何时候再重新被创建。另外,根据JDK文档中介绍,实际JVM的实现是鼓励不回收最近创建和最近使用的对象。SoftReference 类的一个典型用途就是用于内存敏感的高速缓存。

WeakReference(弱引用):如果一个被WeakReference引用的对象,当没要任何SoftReference和StrongReference引用时,立即会被GC。和SoftReference的区别是:WeakReference对象是被eagerly collected,即一旦没要任何SoftReference和StrongReference引用,立即被清楚;而只被SoftReference引用的对象,不回立即被清楚,只有当内存不够,即将发生OutOfMemoryError时才被清除,而且是先清除不常用的。SoftReference适合实现Cache用。WeakReference 类的一个典型用途就是规范化映射( canonicalized mapping )

PhantomReference(虚引用):当没有StrongReference,SoftReference和WeakReference引用时,随时可被GC。通常和ReferenceQueue联合使用,管理和清除与被引用对象(没有finalize方法)相关的本地资源。

##