关于JVM的总结

333 阅读18分钟

前言

JVM,其实在普通的开发中,并没有真正的使用到,但是这并不代表JVM不重要,对JVM的了解,有利于提高你对自己开发的代码的了解,提高你对代码的优化。

当然,可能有人会说,小公司不会在乎你的代码写的好不好,只会在乎你现在忙不忙,这确实存在这种情况,但是这种情况是仁者见仁智者见智的,我们不能因为业内有着这种妖风的存在,就不去提高自己的技术了。

今天的话,我们来大概了解一下JVM,虽然只是走马观花,但是也希望我们能够对JVM的整体情况,有一定的了解。

文中会大量引用其他的博文的图片,但是因为我是之前在有道云总结了很多这方面的内容,时日已久,已经忘了引用的是哪篇博文的了,若有作者看到,请提醒我,我会及时表明引用来源。

JVM的加载流程

Java字节码是在JRE中运行(JRE: Java 运行时环境)。JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作。 JRE由Java API和JVM组成,JVM通过类加载器(Class Loader)加类Java应用,并通过Java API进行执行。

整个加载流程如下图:

JVM的基本特性

1.基于栈(Stack-based)的虚拟机: 不同于Intel x86和ARM等比较流行的计算机处理器都是基于寄存器(register)架构,JVM是基于栈执行的。

2.符号引用(Symbolic reference): 除基本类型外的所有Java类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。

3.垃圾回收机制: 类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。 通过明确清晰基本类型确保平台无关性: 像C/C++等传统编程语言对于int类型数据在同平台上会有不同的字节长度。JVM却通过明确的定义基本类型的字节长度来维持代码的平台兼容性,从而做到平台无关。

4.网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。

类加载器

JVM有的加载器大概如下:

Bootstrap加载器(根加载器,启动类加载器):Bootstrap加载器在运行JVM时创建,用于加载Java APIs,包括Object类。不像其他的类加载器由Java代码实现,Bootstrap加载器是由native代码实现的。

扩展加载器(Extension class loader):扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。

系统加载器(System class loader,应用程序类加载器):如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。 用户自定义加载器:这个是由用户的程序代码创建的类加载器。如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。

Java类加载器的特点如下:

层次结构:Java的类加载器按是父子关系的层次结构组织的。
          Boostrap类加载器处于层次结构的顶层,是所有
          类加载器的父类。
代理模型:基于类加载器的层次组织结构,类加载器之间是
          可以进行代理的。当一个类需要被加载,会先去
          请求父加载器判断该类是否已经被加载。如果父
          类加器已加载了该类,那它就可以直接使用而无
          需再次加载。如果尚未加载,才需要当前类加载
          器来加载此类。
可见性限制:子类加载器可以从父类加载器中获取类,反之则
           不行。
不能卸载: 类加载器可以载入类却不能卸载它。但是可以通过
           删除类加载器的方式卸载类。

每个类加载器都有自己的空间,用于存储其加载的类信息。当类加载器需要加载一个类时,它通过FQCN)(Fully Quanlified Class Name: 全限定类名)的方式先在自己的存储空间中检测此类是否已存在。在JVM中,即便具有相同FQCN的类,如果出现在了两个不同的类加载器空间中,它们也会被认为是不同的。存在于不同的空间意味着类是由不同的加载器加载的。

类加载器负责类的动态加载过程:

当JVM请示类加载器加载一个类时,加载器总是按照从类加载器缓存、父类加载器以及自己加载器的顺序查找和加载类。也就是说加载器会先从缓存中判断此类是否已存在,如果不存在就请示父类加载器判断是否存在,如果直到Bootstrap类加载器都不存在该类,那么当前类加载器就会从文件系统中找到类文件进行加载。

上图展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。

1、”A类加载器”加载类时,先判断该类是否已经加载过了;
2、如果还未被加载,则首先委托其”A类加载器”的”父类加
   载器”去加载该类,这是一个向上不断搜索的过程,当A
   类所有的”父类加载器”(包括bootstrap classloader)都
   没有加载该类,则回到发起者”A类加载器”去加载。
3、如果还加载不了,则抛出ClassNotFoundException。

类加载步骤的每一步的具体描述如下:

加载(Loading): 从文件中获取类并载入到JVM内存空间。
验证(Verifying): 验证载入的类是否符合Java语言规范和JVM规范。
                 在类加载流程的测试过程中,这一步是最为复杂
                 且耗时最长的部分。大部分JVM TCK(兼容测试)
                 的测试用例都用于检测对于给定的错误的类文件
                 是否能得到相应的验证错误信息。
准备(Preparing): 根据内存需求准备相应的数据结构,并分别描述
                 出类中定义的字段、方法以及实现的接口信息。
解析(Resolving): 把类常量池中所有的符号引用转为直接引用。
初始化(Initializing): 为类的变量初始化合适的值。执行静态初
                     始化域,并为静态字段初始化相应的值。

关于类加载过程中的几种常见异常

1、ClassNotFoundException
    JVM要加载指定的文件的字节码到内存中,但是并没有发现
    这个文件的字节码。检查方法就是在classpath中看看是否
    有指定文件存在。
2、NoClassDefFoundError
    JVM规范中这个异常出现的情况是使用new 关键字、属性引
    用类、实现接口、继承类,如果不存在,会报这个异常。
3、ClassCastException
    对于普通对象,对象必须是目标类的实例或者目标类的子
    类的实例,如果目标类是接口,那么会把他当做实现了该
    接口的一个子类。

GC机制

JAVA GC(Garbage Collection,垃圾回收)机制是区别C++的一个重要特征,C++需要开发者自己实现垃圾回收的逻辑,而JAVA开发者则只需要专注于业务开发,因为垃圾回收这件繁琐的事情JVM已经为我们代劳了,从这一点上来说,JAVA还是要做的比较完善一些。但这并不意味着我们不用去理解GC机制的原理,因为如果不了解其原理,可能会引发内存泄漏、频繁GC导致应用卡顿,甚至出现OOM等问题,因此我们需要深入理解其原理,才能编写出高性能的应用程序,解决性能瓶颈。

内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。 三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分
为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内
存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃
圾回收时会出发Minor GC(也称作Youn GC)。 

旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象
,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾
回收称作Major GC(也称作Full GC)。

持久代(Permanent Generation):在Sun的JVM中就是方法区的意思,尽管大
多数JVM没有这一代。GC很少在这个区域进行,但不代表不会回收。这个区域
回收目标主要是针对常量池的回收和对类型的卸载。当内存申请大于实际可用
内存,抛OOM。

当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。这么做主要是为了减少内存碎片的产生。

GC判断对象是否"存活"或"死去"(GC回收的对象):

第一种:经典的引用计数算法,每个对象添加到引用计数器,每被引用一次,计数器+1,失去引用,计数器-1,当计数器在一段时间内为0时,即认为该对象可以被回收了。但是这个算法有个明显的缺陷:当两个对象相互引用,但是二者都已经没有作用时,理应把它们都回收,但是由于它们相互引用,不符合垃圾回收的条件,所以就导致无法处理掉这一块内存区域。

第二种:Sun的JVM并没有采用这种算法,而是采用一个叫——根搜索算法(可达性搜索法),如图:

基本思想是:从一个叫GC Roots的根节点出发,向下搜索,如果一个对象不能达到GC Roots的时候,说明该对象不再被引用,可以被回收。如上图中的Object5、Object6、Object7,虽然它们三个依然相互引用,但是它们其实已经没有作用了,这样就解决了引用计数算法的缺陷。

什么样的类需要被回收

a.该类的所有实例都已经被回收;
b.加载该类的ClassLoad已经被回收;
c.该类对应的反射类java.lang.Class对象没有被任何地方引用。

当GC线程启动时,会通过可达性分析法把Eden区和From Space区的存活对象复制到To Space区,然后把Eden Space和From Space区的对象释放掉。当GC轮训扫描To Space区一定次数后,把依然存活的对象复制到老年代,然后释放To Space区的对象。

GC大概有三种,Minor GC、Major GC和Full GC。我们来分析这三者的区别:

Minor GC:从年轻代空间(包括 Eden 和 Survivor 区域)回收内存。(算法:复制)

当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。
所以分配率越高,越频繁执行 Minor GC。内存池被填满的时候,其中的内容全部
会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制
操作,取代了经典的标记、扫描、压缩、清理操作。所以Eden
和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。执行 
Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GCroots
,从年轻代到永久代的引用在标记阶段被直接忽略掉。质疑常规的认知,所有的 
Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。
对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,
大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者
老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,
Minor GC 执行时暂停的时间将会长很多。
每次 Minor GC 会清理年轻代的内存。

Major GC :是清理老年代。(算法:标记)
许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC分离是不太可
能的。另一方面,许多现代垃圾收集机制会清理部分永久代空间,所以使用“cleaning”
一词只是部分正确。

Full GC: 是清理整个堆空间—包括年轻代和老年代。(算法:标记)
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于
     ToSpace可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

Full GC和Major GC的区别
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

Partial GC:并不收集整个GC堆的模式
            Young GC:只收集young gen的GC
            Old GC:只收集old gen的GC。只有CMS的concurrent 
                    collection是这个模式
            Mixed GC:收集整个young gen以及部分oldgen的GC。
                     只有G1有这个模式(也被叫做major  GC)
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old gen。

在JVM中,GC是由垃圾回收器来执行,所以,在实际应用场景中,我们需要选择合适的垃圾收集器,下面我们介绍一下垃圾收集器有那些

1.串行收集器(Serial GC)
Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA
SE6中客户端虚拟机采用的默认配置。比较适合于只有一个处理器的系统。在串行处
理器中minor和major GC过程都是用一个线程进行回收的。它的最大特点是在进行垃
圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难
以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之
内,大多数应用还是可以接受的,而且事实上,它并没有让我们失望,几十毫秒的停
顿,对于我们客户机是完全可以接受的,该收集器适用于单CPU、新生代空间较小且
对暂停时间要求不是特别高的应用上,是client级别的默认GC方式。

2. ParNew GC
基本和Serial GC一样,但本质区别是加入了多线程机制,提高了效率,这样它就可以
被用于服务端上(server),同时它可以与CMSGC配合,所以,更加有理由将他用于server端。

 3.Parallel Scavenge GC
在整个扫描和复制过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的
应用,是server级别的默认GC方式。

 4.CMS (Concurrent Mark Sweep)收集器
该收集器的目标是解决Serial GC停顿的问题,以达到最短回收时间。常见的B/S架构的
应用就适合这种收集器,因为其高并发、高响应的特点,CMS是基于标记-清楚算法实现
的。
CMS收集器的优点:并发收集、低停顿,但远没有达到完美;
CMS收集器的缺点:
a.CMS收集器对CPU资源非常敏感,在并发阶段虽然不会导致用户停顿,但是会占用CPU
  资源而导致应用程序变慢,总吞吐量下降。
b.CMS收集器无法处理浮动垃圾,可能出现“Concurrnet Mode Failure”,失败而导致
  另一次的Full GC。
c.CMS收集器是基于标记-清除算法的实现,因此也会产生碎片。

5.G1收集器
相比CMS收集器有不少改进,首先,基于标记-压缩算法,不会产生内存碎片,其次可以
比较精确的控制停顿。

6.Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用
“标记-整理”算法。主要使用在Client模式下的虚拟机。

7.Parallel Old收集器
 Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
 
8.RTSJ垃圾收集器
 RTSJ垃圾收集器,用于Java实时编程。

JVM的调优

第一种方法:将新对象预留在新生代

1.由于Full GC的成本要远远高于Minor GC,因此需要把对象尽可能分配给新生代。

2.通过设置一个较大的新生代预留新对象,设置合理的survivor区并提高survivor区的使用率。

第二种方法:大对象进入老年代

1.大对象分配在新生代可能破坏原有新生代对象结构 2.短命大对象存到老年代,导致回收短命大对象成为一种灾难

解决方法:

-XX:PretenureSizeThreshold:设置大对象直接进入老年代的阀值,当对象的大小超过这个值,将直接在老年代分配。 但这个参数只对串行收集器和新生代并行收集器有效,并行回收器不识别这个参数。

第三种方法:设置对象进入老年代的年龄

对象在新生代经过一次GC依然存活,则年龄+1,当年龄达到阀值,就移入老年代。 阀值的最大值通过参数:

-XX:MaxTenuringThreshold来设置,它默认是15。 在实际虚拟机运行过程中,并不是按照这个年龄阀值来判断,而是一句内存使用情况来判断,但这个年龄阀值是最大值,也就说到达这个年龄的对象一定会被移到老年代。

第四种方法:稳定与震荡的堆大小

1.当-Xms与-Xmx设置大小一样,是一个稳定的堆,这样做的好处是,减少GC的次数。

2.当-Xms与-Xmx设置大小不一样,是一个不稳定的堆,它会增加GC的次数,但是它在系统不需要使用大内存时,压缩堆空间,使得GC应对一个较小的堆,可以加快单次GC的次数。

3.可以通过两个参数设置用语压缩和扩展堆空间:

-XX:MinHeapFreeRatio:设置堆的最小空闲比例,默认是40,当堆空间的空闲空间小于这个数值时,jvm会自动扩展空间。

-XX:MaxHeapFreeRatio:设置堆的最大空闲比例,默认是70,当堆空间的空闲空间大于这个数值时,jvm会自动压缩空间。

小结

今天的JVM的学习就到了为止。