JVM梳理

JVM

JVM基本介绍

JVM包含四个部分:Class loader(类装载)、Execution engine(执行引擎);Runtime data area(运行时数据区)、Native Interface(本地接口)。

  • Class loader(类装载):装载class文件到method area。
  • Execution engine(执行引擎):将字节码命令解释给操作系统。
  • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):JVM的内存,存数据。

JVM执行过程

  1. 编译器把Java代码转成字节码,类加载器把字节码加载到内存(**运行时数据区**的方法区)中;
  2. 通过解析器**执行引擎**,将字节码翻译成OS底层指令,交由CPU执行;
  3. 整个过程中需要调用其他语言的**本地库接口**来实现程序功能。

jvm结构图

类加载机制

类的生命周期

类的生命周期:加载、连接(验证、准备、解析)初始化。

###加载

加载过程加载的是.class 文件。

加载过程

  1. 通过一个类的全限定名来获取其定义的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

加载方式

  1. 隐式加载:通过写程序new一个对象
  2. 显示加载:通过反射,class.forname()方法等

类加载过程

这一步用类加载器来实现。

连接

  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;

初始化

对静态变量和静态代码块执行初始化工作。

类加载器

类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通 过继承 java.lang.ClassLoader类的方式实现。

类加载器的层次

类加载机制

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。Java历史上,有三次没有使用双亲委派模型的案例。

双亲委派模型的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

对象的实例化过程

对象实例化顺序

父类子类实例化执行顺序

垃圾回收机制

逻辑结构:什么时候回收?能回收什么?用什么回收?怎么回收的?

垃圾回收原理机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。

判断一个对象是否可以被回收

两种策略:计数器算法和可达性分析算法

计数器算法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

该算法的缺陷是:两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

可达性分析算法

通过 GC Roots 作为起始点进行搜索,能够遍历到的对象都是存活的,遍历不到的对象可被回收。

引用类型

强引用

发生 gc 的时候不会被回收。不会被gc回收。

软引用

有用但不是必须的对象,在发生内存溢出之前会被回收。(按内存走的)

弱引用

有用但不是必须的对象,在下一次GC时会被回收。(按gc周期走的)

虚引用

无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

垃圾回收算法

标记清除算法

标记无用对象,然后进行清除回收。

缺点:标记无用对象,然后进行清除回收。

标记复制算法

每次移动存货对象到另一半区,然后清理可回收对象。主要用于回收新生代对象。

缺点:内存空间使用效率低

标记整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

将内存进行切块,不同块采用适当的收集算法。 一般将堆分为新生代和老年代。 新生代使用: 复制算法 老年代使用: 标记 - 清除 或者 标记 - 整理 算法。

垃圾收集器

CMS收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

执行过程:

  1. 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  3. 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  4. 并发清除: 不需要停顿。

缺点::

  1. 吞吐量低(主要体现在第二阶段)
  2. 无法处理浮动垃圾:浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。
  3. 标记 - 清除算法导致的空间碎片

G1收集器

G1 可以直接对新生代和老年代一起回收。G1基于“标记 - 整理”算法实现。

G1收集器引入 Region 的概念,通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

执行过程:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

内存分配策略

  1. 对象优先在 Eden 分配
  2. 大对象直接进入老年代
  3. 长期存活的对象进入老年代

内存回收策略

Minor GC: 发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Full GC: 发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

Minor Gc 的触发条件

当 Eden 区的空间耗尽时 Java 虚拟机便会触发一次 Minor GC。

Full Gc的触发条件

  1. 调用 System.gc()
  2. 老年代空间不足

减少Full Gc次数的措施

  1. 增加方法区的空间;
  2. 增加老年代的空间;
  3. 减少新生代的空间;
  4. 禁止使用System.gc()方法; (会产生STW)
  5. 使用标记-整理算法,尽量保持较大的连续内存空间;
  6. 排查代码中无用的大对象。

内存模型

JVM调优