JVM相关

一、JVM
jvm将类加载到jvm的内存中,总共经历七个阶段,加载、验证、准备、解析、初始化、使用、卸载。
加载是指将class文件从磁盘load到jvm的方法区的内存中,在堆区创建一个Class对象,封装这个类在方法区的数据结构
Java程序对类的使用方式分为两种:主动使用被动使用。所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化这个类,否则不执行初始化阶段操作。

主动使用(6种):

       1、创建类的实例

       2、访问某个类或接口的静态变量,或者对该静态变量赋值

       3、调用某个类的静态方法

       4、反射(Class.forName("java.lang.String"))

       5、初始化一个类的子类

       6、Java虚拟机启动时被标明为启动类的类

被动使用

1、通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化
2、通过数组定义类引用类,为类的被动使用,不会触发此类的初始化
3、常量在编译阶段会存入调用方法所在的类的常量池中

JVM内存模型
jvm内存主要分为:java栈、本地方法栈、程序计数器、方法区、堆
堆采用分代收集算法,分为新生代和老年代。新生代分为Eden区、FromSurvivor区和ToSurvivor区。比例是8:1:1
方法区存储类信息、常量、静态变量、编译后的代码等等


二、GC回收算法

2.1 gc存活判断
首先判断对象是否可以被回收
引用计数法(无法解决对象相互循环引用的问题)
可达性分析算法
将gc roots的对象作为起始点,向下搜索,如果有对象到gc roots没有任何引用链,就认为不可达,可以被gc回收。
gc roots的对象有:
  • java栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中native方法引用的对象

2.2 垃圾回收算法
  • 标记/清除算法
  • 复制算法
  • 标记/整理算法
  • 分代收集算法

1. 标记/清除算法
标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;
清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。
在垃圾收集器进行GC时,必须停止所有Java执行线程(也称"Stop The World"),原因是在标记阶段进行可达性分析时,不可以出现分析过程中对象引用关系还在不断变化的情况,否则的话可达性分析结果的准确性就无法得到保证。在等待标记清除结束后,应用线程才会恢复运行。
有效率问题(需要遍历内存的所有对象,stw)和空间问题(内存碎片)

2.复制算法
将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存所有的对象一次性清理掉。
简单高效,减少遍历、减少内存碎片
缺点是将内存缩小为原来的一半,浪费空间
现代jvm在新生代将内存分为一块较大的Eden空间和两块较小的From Survivor空间、To Survivor空间,三者的比例为8:1:1。每次使用Eden和From Survivor区域,To Survivor作为保留空间。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的。
GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色。
GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

3.标记/整理算法
标记/整理算法的标记过程任然与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。

4.分区收集
分代收集算法的原理是采用复制算法来收集新生代,采用标记/清理算法或者标记/整理算法收集老年代。
分区收集分为minor gc和full gc;

2.3 GC收集器
1、Serial收集器
串行化收集器,只使用一个线程去回收,会stop the word,新生代、老年代使用串行回收;新生代复制算法、老年代标记-整理
JVM相关

2、ParNew收集器
Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-整理
JVM相关

3、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
整个过程分为4个步骤:
  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)
其中初始标记重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
初始标记、并发标记、重新标记、并发清理
JVM相关

4、G1收集器
与CMS收集器相比G1收集器有以下特点:
1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
收集过程:
  • 标记阶段,会stop the word
  • root region scanning
  • concurrent marking(并发标记)
  • remark,再标记,会短暂stop the word
  • Copy/Clean up,多线程清除失活对象,会有STW


对象分配内存
对象优先分配在Eden区
大对象直接进入老年代(-XX:PretenureSizeThreshold参数
长期存活的对象进入老年代(默认15)

什么时候发起gc?
HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
分配担保失败。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。

分配担保是什么?
GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。
是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。


JVM结构

JVM相关
JVM主要由类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈组成。

类加载
类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM可以实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。
CLASSLOADER的分类:
  • 启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中所有的Java类,即Java的核心类都是由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。
  • 扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包。
  • 系统类加载器(System Class Loader):负责加载启动参数中指定的Classpath中的jar包及目录,通常我们自己写的Java类也是由该ClassLoader加载。在Sun JDK中,系统类加载器的名字叫AppClassLoader。
  • 用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。
类加载分为加载、链接、初始化三步。
JVM相关
JVM相关
加载
通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。
装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model)”的方式,当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader,所以称为父ClassLoader可能更容易理解)加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。
双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危害。
Java类随着它的ClassLoader一起具备了一种带优先级的层次关系。例如java.lang.Object存放在rt.jar之中,无论那个类加载器要加载这个类,最终都是委托给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类;同时双亲委派模型也保证了类型被使用的安全性。
链接
链接的任务是把二进制的类型信息合并到JVM运行时状态中去。
  1. 验证:校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM使用。
  2. 准备:为类分配内存,同时初始化类中的静态变量赋值为默认值。
  3. 解析(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步可以在用到相应的引用时再解析。
初始化
初始化类中的静态变量,并执行类中的static代码、构造函数。
JVM规范严格定义了何时需要对类进行初始化:
  1. 通过new关键字、反射、clone、反序列化机制实例化对象时。
  2. 调用类的静态方法时。
  3. 使用类的静态字段或对其赋值时。
  4. 通过反射调用类的方法时。
  5. 初始化该类的子类时(初始化子类前其父类必须已经被初始化)。
  6. JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。
Java栈、本地方法栈
方法区
类型信息和类的静态变量都存储在方法区中。方法区中对于每个类存储了以下数据:
a.类及其父类的全限定名(java.lang.Object没有父类)
b.类的类型(Class or Interface)
c.访问修饰符(public, abstract, final)
d.实现的接口的全限定名的列表
e.常量池
f.字段信息
g.方法信息
h.静态变量
i.ClassLoader引用
j.Class引用
主要是类的信息
堆(Heap)
新生代(New Generation)
大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制(Minor GC的过程在此不详细讨论)。
JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配,我们知道在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。
新生代 = Eden + 2 * Survivor
老年代(Old Generation/Tenuring Generation)
在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。
old gc 会引发stop the world
执行引擎
执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。
JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。
JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。注意这里是“通常”情况,之后会讲到由于优化导致的例外。
解释执行
和一些动态语言类似,JVM可以解释执行字节码。Sun JDK采用了token-threading的方式。
编译执行
为了提升执行速度,Sun JDK提供了将字节码编译为机器指令的支持,主要利用了JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,之后就可以重复利用。Oracle JRockit采用的是完全的编译执行。
自适应优化执行
自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,所以通过将那少部分代码编译为优化过的机器码就可以大大提升执行效率。自适应优化的典型代表是Sun的Hotspot VM,正如其名,JVM会监测代码的执行情况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该方法的字节码编译为极度优化的、静态链接的C++代码。当方法不再是热区时,则会取消编译过的代码,重新进行解释执行。
自适应优化不仅通过利用小部分的编译时间获得大部分的效率提升,而且由于在执行过程中时刻监测,对内联代码等优化也起到了很大的作用。由于面向对象的多态性,一个方法可能对应了很多种不同实现,自适应优化就可以通过监测只内联那些用到的代码,大大减少了内联函数的大小。