什么是JVM?

是一个专门运行class字节码文件的操作系统,是用c语言开发的,不同的系统都有对应版本的jvm。专门屏蔽了底层的操作系统、硬件、CPU指令等层面上的细节,让字节码只面对jvm,不用去关注操作系统、硬件的差异。

JVM的组成

jvm由垃圾回收器、类加载器、运行时数据区、执行引擎、本地方法库组成。

其中运行时数据区是重点,也叫java内存部分,其余还可以关注垃圾回收器和类加载器。

1.类加载器

负责查找并装载类的部分称为类加载子系统,类加载子系统用于定位和加载编译后的class文件。

下面是类加载的整个生命周期:

graph LRA[字节码] -->B[类加载器]-->C[链接]-->D[初始化]-->E[使用]-->F[卸载]

其中这一步又细分为三步:

graph LRA[验证]-->B[准备]-->C[解析]

类加载器将类加载完毕后会将类的信息加入到。

类加载机制

类加载器主要是实现类的加载,将类加载到java内存中。

双亲委派机制:

2.运行时数据区(java内存区域)

运行时数据区又叫java的内存区域,内存区域主要分为两部分:线程私有线程公有

其中和是线程公有的,、、是线程私有的。

一般而言线程公有的区域都会存在线程安全的问题。

程序计数器:

程序计数器主要是用于记录程序执行的位置和行号,是一块很小的区域,不会存在内存溢出的问题。

虚拟机栈:

虚拟机栈又叫方法栈、线程栈。虚拟机栈采用的是栈的数据结构也就是先进后出,后进先出的机制。

在虚拟机栈中,入口和出口都是同一个口,虚拟机栈主要是执行方法的。方法的执行也叫压栈,方法执行结束就叫出栈。

一个方法执行就会创建一个栈帧,栈中可以压很多的栈帧,一个栈帧对应一个方法。

栈帧分为四个部分:局部变量表、操作数栈、动态链接、返回地址。

局部变量

假设现在有一个方法开始执行,也就是开始压栈,方法中可能会存在局部变量,那么这些局部变量会存在栈帧的局部变量表中。如果局部变量是基本数据类型,那么就会直接存在局部变量表中,如果是引用数据类型,那么只会把引用数据类型的地址存在局部变量表中,具体的引用类型数据则放在堆中,相当于局部变量表存了引用数据类型的一个指针,指向这个数据的在堆中的位置。

操作数栈

操作数栈指的是在方法中可能存在运算指令,那么这个运算指令会在操作数栈中进行操作。

动态链接

动态链接指的是在这个方法中可能会存在调用其他方法,那么就需要找到这个被调用的方法在哪,方法是放在元空间/方法区中的,方法只有一个,但类可以创建n个对象,所以这个方法可能会被调用n次,所以动态链接存的是被调用方法的一个内存地址,指向的是存在于元空间的方法。

返回地址

返回就比如返回方法的执行情况,比如成功或者失败或者异常之类的。

虚拟机栈可能会出现的问题

虚拟机栈溢出:

栈可以通过进行大小设置

本地方法栈

本地方法栈是为本地方法服务的,和虚拟机栈一样,当执行本地方法的时候,一样会进行压栈操作,这时会把栈帧压到本地方法栈而不是虚拟机栈,本地方法都是用关键字进行标记。

方法区/元空间

方法区/元空间是线程共享的,用于存储类的信息、常量、运行时常量池、静态变量、即时编译器编译后的代码等。在java虚拟机规范中描述的方法区是堆的一个逻辑部分,也就是说方法区在逻辑上是属于堆的一部分,但实际上为了和堆做区分,方法区也被称为非堆。在HotSpot虚拟机中,使用永久代来作为方法区的落地实现,但是因为一些问题,后面逐渐使用本地内存来作为方法区的落地实现。

ps:在1.7及其之前,方法区在hotspot中都是永久代,直到1.8版本,永久代变成了使用本地内存的元空间。在1.7版本的时候,将字符串常量池从方法区中去除,放在了堆中。

1.7及其之前的版本可以通过来设置最大值。

1.8开始通过来进行设置。

空间不够分配时会出现的问题。

比如:

元空间溢出

元空间有默认的大小,一旦元空间存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等或者通过反射大量生成动态类填充该区域超出大小范围即会发生内存溢出;

1.7之前的元空间溢出:

1.7及1.8的元空间溢出

1.8元空间设置大小

堆是线程共享的,是java内存中最大的区域,只要虚拟机启动就会创建堆,用于存放所有的实例对象或者数组,是垃圾回收器主要管理的区域,堆可划分为新生代和老年代。

堆可以通过和来调节堆的大小。

堆会出现

新生代可细分为:伊甸区、from区和to区

​新生区会引发轻gc(minorgc),养老区会引发重gc(fullgc),垃圾回收主要发生在新生区和养老区,新生区到养老区触发fullgc

新生代占整个堆的1/3,老年代占2/3。其中伊甸区和from区、to区又按照8:1:1的比例分配堆的1/3。

概念:

3.JVM垃圾回收

垃圾回收器主要关注线程共享的部分:堆和方法区/元空间。

哪些需要回收?什么时候回收?如何回收?

堆创建对象的过程:

对象在内存中的布局:

如何判断可以回收--可达性路径分析:

jvm通过可达性分析算法来判断是否可以回收,之前说过可达性就相当于持有对象的引用,持有方只要还存在,那被持有方一样是不能回收的。

机制是通过一个对象(这个对象被称为GC Root)为根节点,以这个根节点为起始点开始搜索,看这个对象和其他对象有没有可达路径,如果没有则表示该对象是不可用的,可以被回收。

那么哪些对象可以作为GC Root:

jvm主要是通过引用来分析是否有可达路径的。

主要有四种引用:

  1. 强引用

    强引用是最普遍的引用,比如 User user = new User(); 这是强引用,垃圾收集器不会回收强引用;

  2. 软引用

    软引用是表示一些对象还有用,但是也不是必须要用的,比如像缓存对象就可以采用软引用,在系统内存不足时,这些软引用是可以被垃圾回收器回收的,如果我们要使用软引用的话,编码的时候,把对象采用jdk中提供的SoftReference类型来包装;

  3. 弱引用

    弱引用也是表示一些不是必须的对象,但它比软引用更弱,被弱引用所引用的对象只能生存到下一次垃圾收集之前,当开始垃圾收集时,无论内存是否足够,都会回收弱引用对象;

  4. 虚引用

    虚引用PhantomReference是一种特殊的引用,也是最弱的引用关系,用来实现Object.finalize功能,在开发中很少使用;

方法区/元空间的垃圾回收

在JVM的垃圾回收中,堆内存是回收最频繁也是最多的,方法区/元空间的垃圾收集效率非常低,所以JVM规范中并没有要求一定要回收方法区/元空间,如果方法区/元空间有无用的类信息、常量池,JVM不是必须要回收的;

Hotspot 虚拟机默认会进行类的卸载,如果不想要对无用的类进行卸载,可以加上参数-Xnoclassgc(不卸载),默认情况下Hotspot 虚拟机会卸载类;

类的加载和卸载参数:

方法区/元空间垃圾回收主要两部分内容:废弃的常量和无用的类;

判断废弃常量:一般是判断没有任何对象引用该常量;

判断无用的类:要满足以下三个条件

(1)该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例;

(2)加载该类的 ClassLoader 已经被回收;

(3)该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法;

JVM回收对象的两次标记过程

1、第一次标记

如果对象进行可达性分析算法之后没有发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选;

筛选条件:判断此对象是否有必要执行finalize()方法;

筛选结果:当对象没有覆盖finalize()方法或者finalize()方法已经被JVM执行过,则判定为可回收对象,如果对象有必要执行finalize()方法,则被放入F-Queue队列中,稍后在JVM自动建立低优先级的Finalizer线程(可能多个线程)中触发这个方法;  

2、第二次标记

GC对F-Queue队列中的对象进行二次标记;

如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么第二次标记时则会将它移出“即将回收”集合,如果此时对象还没成功逃脱,那么只能被回收了;

注:finalize() 方法

finalize()是Object类的一个空方法、该方法是被垃圾收集器所调用,一个对象的finalize()方法只会被垃圾收集器自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

不提倡在程序中调用finalize()来进行对象的自救,因为该方法执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),无法保证各个对象的调用顺序(甚至有不同线程中调用)。

垃圾回收算法
  • 复制算法

  • 标记-清除算法

    分为‘ 标记 ’和‘ 清除 ’两个阶段:

  • 标记-整理算法

  • 分代收集算法

垃圾收集器

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

Serial

ParNew

GC日志常用参数
06-21 00:07