一口气看完jvm虚拟机(java进阶)面试宝典

目录

一.认识jvm

1.什么是jvm

二.jvm的组成及每个部分的作用

 1.类加载器

1.1类加载器过程 

1.1.1图例

1.1.2加载过程

1.1.3 验证过程

1.1.4准备过程

1.1.5解析过程

1.1.6初始化

1.2什么时候类才算初始化(加载)完毕

1.3类在以下两种情况不会被加载

1.4类加载器分几种

1.4.1如下图实例类加载器分类

 1.4.2启动类加载器(引导类加载器)

 1.4.3扩展类加载器

1.4.4应用程序类加载器

1.5双亲委派机制

1.5.1如下图所示

1.5.2代码实例 

1.5.3如何打破双亲委派机制

2.运行时数据区

2.1图示运行时数据区组成

2.2 程序计数器

2.2.1如图所示程序计数器

2.3虚拟机栈

2.3.1如图所示虚拟机栈

 2.4本地方法栈

2.4.1例如:

2.5堆

2.5.1堆的特点

2.5.2堆分类

​编辑 2.5.3为什么要分区

 2.5.4对象在堆空间的分配过程

2.5.5 对象在堆空间的分配过程图例

2.5.6为什么对象在经过15次垃圾回收后就被放到老生区,垃圾回收的次数可以改变吗

2.6 方法区

2.6.1方法区中什么时候类才会被卸载(方法区的垃圾回收机制)

 2.6.1方法区与堆图例

3.执行引擎

3.1执行引擎有两个编译

 3.2解释器和编译器

3.3为什么Java是半编译解释的(为什么这么设计)

4.本地库接口

 4.1什么是本地方法

4.2为什么要用本地方法

二.垃圾回收

 1.早期垃圾回收

2.现代的垃圾回收机制

3.内存溢出和内存泄漏 

4.垃圾回收的区域

 三.垃圾标记阶段算法

 1.引用计数器算法(目前不使用)

 1.1循环引用

2.可达性分析算法

2.1.哪些对象可以被称为活跃性对象

3.finalize机制

 3.1finalize方法存在分类的三种对象

 四.回收阶段算法

1. 标记复制算法

1.1如图所示标记复制算法图例 

2.标记清除算法

2.1标记清除算法图例

3.标记压缩算法

 3.1标记压缩算法图例

4.分代收集

五.垃圾收集器

1.垃圾回收器分类

1.1从垃圾回收器线程数量上划分

1.2从工作模式上分 

1.2.1工作模式垃圾回收器如下图

1.3按照工作内存上分

 2.CMS回收器

3.G1垃圾收集器

3.1g1垃圾收集内存分配如图

3.2G1垃圾收集器收集过程图示 

六.Jvm总结


一.认识jvm

1.什么是jvm

简单来说jvm是将.class文件(字节码)编译为其他平台上对应的机器码指令,实现跨平台,帮助管理内存和线程。 

二.jvm的组成及每个部分的作用

 1.类加载器

类加载器:顾名思义就是加载类的,只加载.class文件(字节码),将硬盘上的字节码通过流的方式加载到内存中,存储在方法区中,在jvm中充当快递员的身份。

1.1类加载器过程 

1.1.1图例

1.1.2加载过程

通过流将硬盘上的字节码读取到内存中的方法区,生成该类对象的class对象。

1.1.3 验证过程

检验字节码格式是否正确如果错误不符合字节码(class文件)的格式会出错。

 如图1.1.3.1和1.1.3.2及1.1.3.3所示:

                                           图1.1.3.1 

                            图1.1.3.2

   如上 图1.1.3.1和 图1.1.3.2 上述字节码编译正确,所以检查无误

  下图1.1.3.3所示字节码编译错误

                                                                         图1.1.3.3 

1.1.4准备过程

在准备阶段将类中的静态属性进行初始化赋值。

例如:static int a=111   在准备阶段a的值初始化为0,在初始化阶段才赋值为111。注意不包含用final修饰的static常量。

1.1.5解析过程

将字节码中的符号引用(逻辑符号)替换为直接引用(内存地址)。

1.1.6初始化

 (1)当类被初始化后该类才算真正被加载完毕。

(2)初始化过程将类中的静态成员(成员方法和成员变量)初始化赋值。

1.2什么时候类才算初始化(加载)完毕

 简单来说只要类被用到了就算初始化(加载)完毕。

1.类中的静态成员(类名.成员变量,类名.成员方法)被使用。

2.运行该类的main方法。

3.new了该类的对象。

4.加载该类的子类时。

5.通过反射机制Class.forName()。

 如下示例:

package com.ffyc.javapro.jvm.classloader;

public class Student {
    public static final  int M=10;//静态常量
    public static int X=22;//静态变量
    static {
        System.out.println("此类被加载了");
    }
}


package com.ffyc.javapro.jvm.classloader;

public class Test {
    public static void main(String[] args) {
        System.out.println(Student.M);//只访问类中的静态常量类不会被加载
        System.out.println(Student.X);

    }
}

1.3类在以下两种情况不会被加载

  System.out.println(Student.M);//只访问类中的静态常量类不会被加载

   Student[]students=new Student[10];//创建数组对象时,类只是被作为类型存在不会被加载,

1.4类加载器分几种

大方向上类加载器分为java编译的以及非Java编译(c或者c++编译的)的。

1.4.1如下图实例类加载器分类

 

 1.4.2启动类加载器(引导类加载器)

这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库。

 1.4.3扩展类加载器

Java 语言编写的,从JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。

1.4.4应用程序类加载器

 Java 语言编写的,加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类。(该类加载器是程序中默认的类加载器)

1.5双亲委派机制

当加载一个类时,先由上级类加载器完成,优先加载系统中的类,一直到启动类加载器,如果到达了顶部类加载器,找到了就返回该类,未找到就委派到他的子级让他的子级类加载器去加载,在哪一级找到了就在哪一级返回,如果向下都没找到,就抛出类找不到异常。(优先加载系统中的类,防止我们自己的类替换了系统中的类)

1.5.1如下图所示

1.5.2代码实例 

创建一个java.lang包下的String类并创建该类对象,验证双亲委派机制是否起作用,加载的优先是系统中的类。

package java.lang;

public class String {

    static {
        System.out.println("MyString加载了");
    }
    public String(){
        System.out.println("成功了");
    }
}

如下图该类调的依然是java库中的String类。 

1.5.3如何打破双亲委派机制

 可以自定义类加载器

package com.ffyc.javapro.jvm.classloader;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader{
    //类的路径
    private String classPath;

    public MyClassLoader(ClassLoader parent, String codePath) {
        super(parent);
        this.classPath = codePath;
    }

    public MyClassLoader(String codePath) {
        this.classPath = codePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis=null;
        ByteArrayOutputStream baos=null;
        //完整的类名
        String file = classPath+name+".class"; // E:/Hello.class
        try {
            //初始化输入流
            bis = new BufferedInputStream(new FileInputStream(file));
            //获取输出流
            baos=new ByteArrayOutputStream();
            int len;
            byte[] data=new byte[1024];
            while ((len=bis.read(data))!=-1){
                baos.write(data,0,len);
            }
            //获取内存中的字节数组
            byte[] bytes = baos.toByteArray();
            //调用defineClass将字节数组转换成class实例
            Class<?> clazz = defineClass(null, bytes, 0, bytes.length);
            return clazz;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader("E:/");
        try {
            //Class<?> clazz = myClassLoader.loadClass("Hello");//用系统的类加载流程
            Class<?> clazz = myClassLoader.findClass("Hello");//用我们自己重写的加载类的方式
            //打印具体的类加载器,验证是否是由我们自己定义的类加载器加载的
            System.out.println("测试字节码是由"+clazz.getClassLoader()+"加载的。。");
            Object o = clazz.newInstance();
            System.out.println(o.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.运行时数据区

运行时数据区就是用来存储各种运行时的数据。(包括堆,方法区,程序计数器,虚拟机栈,本地方法栈)

2.1图示运行时数据区组成

 

2.2 程序计数器

用来记录每个线程执行的指令的位置,因为cup要切换线程。

特点:

(1)程序计数器占内存小,速度快。

(2)线程私有的每个线程都有一个计数器。

(3)生命周期与线程相同。

(4)不会出现栈溢出情况。

2.2.1如图所示程序计数器

2.3虚拟机栈

(1)虚拟机栈是运行结构,主要用来运行java的方法的。

(2)栈是线程私有的,每个线程运行都有一个自己的虚拟机栈空间。

(3)栈运行特点:先进后出,后进先出。

(4)栈是会出现内存溢出的。(递归调用)

(5)一个方法被调用后,在栈中被称为一个栈帧,保存局部变量,操作数栈,方法返回地址。

2.3.1如图所示虚拟机栈

 2.4本地方法栈

用来运行调用本地方法(java中用native修饰的方法)。

2.4.1例如:
(1)new Object().hashCode();
(2)线程中的new Thread().start();

(3)文件与I/O操作中的FileInputStream/FileOutputStream 的 open 方法

注意:运行时数据区中程序计数器,虚拟机栈,本地方法栈都没有垃圾回收机制。堆和方法区才有垃圾回收机制。 

2.5堆

java中的堆是用来存放java产生的对象,Java产生的对象都放在堆空间中。

2.5.1堆的特点

 (1)堆是虚拟机中空间最大的一块区域,在运行时数据区(除了程序计数器外,其他四个区域的内存空间大小都可以调整)。

(2)堆是可以被所有的线程所共享的。

(3)堆是垃圾回收的重点区域。

2.5.2堆分类

 堆从大方向上分为新生代/新生区和老生区。

新生区分为:Eden(伊甸园)新创建的对象存放在此处,幸村者(0)和幸存者(1)

 2.5.3为什么要分区

分区是为了将存活时间不同的对象存放到不同的区域,对这些不同的区域分别进行垃圾回收,减少对老生区的垃圾回收,频繁对新生区的垃圾回收。

 2.5.4对象在堆空间的分配过程

新创建的对象都存放在伊甸园区。

(1)在经过一次垃圾回收后,伊甸园区中存活的对象就会被存放到幸存者0区。

(2)再次进行垃圾回收时就会将伊甸园区和幸存者0区存活的对象存放到幸存者1区。

(3)每次进行垃圾回收时都要保证一个幸存者区是空闲的,减少空间碎片。

(4)当一个对象经过15次垃圾回收后依然存活,那么就将此对象存放到老生区。

(5)如果一个对象很大,也可以直接存放到老生区。 

2.5.5 对象在堆空间的分配过程图例

2.5.6为什么对象在经过15次垃圾回收后就被放到老生区,垃圾回收的次数可以改变吗

如下图所示:

(1)如上图在对象的组成对象头中有一块区域(分代年龄区)用来记录垃圾回收的最大次数,最大占四个比特位也就是最大是15,所以垃圾回收的最大次数就是15次。

(2)垃圾回收的次数可以通过设置一个参数来改变:-XX:MaxTenuringThreshold=<N>

在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111, 即为 15。所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。

注意:

在老年代,相对悠闲,当老年代内存不足时,触发 GC,进行老年代的内存清理.
 若老年代执行了 GC 之后发现依然无法进行对象存储,会对整堆进行 GC, 之后依然无法进行对象存储, 就会产生 OOM 异常. (Java.lang.OutOfMemoryError:Java heap space)

2.6 方法区

存储加载类的字节码,主要存储加载到内存中的类信息。

特点:

(1)方法区在物理上与堆是属于一个空间,但是逻辑上是分开的。

(2)方法区的大小可以通过设置-XX:MetaspaceSize=N参数改变,方法区一旦空间不足就会触发 Full GC(整堆收集),会影响应用线程,一般情况可以把方法区大小设置大一点。

(3)方法区是会出现内存溢出的。

(4)方法区也存在垃圾回收。

2.6.1方法区中什么时候类才会被卸载(方法区的垃圾回收机制)

 方法区中的类要被卸载需要满足三个条件

(1)该类以及该类的子类对象不存在了。

(2)加载该类的类加载器不存在了。

(3)该类的class对象不存在了。

 2.6.1方法区与堆图例

3.执行引擎

执行引擎就是将Java编译的.class文件编译为机器码。

3.1执行引擎有两个编译

(1)第一次编译将Java文件编译为class文件,在开发阶段执行与运行无关,称为前端编译。

(2)第二次是在运行时虚拟机通过执行引擎将class文件编译为机器码,称为后端编译。

 3.2解释器和编译器

1.解释器:无需进行编译,直接逐行对字节码进行解释,由于无需编译只需要解释就可以立即执行,速度快但效率低。

2.编译器:将字节码整体编译为机器码后执行,编译需要花费时间多,但效率高。

3.3为什么Java是半编译解释的(为什么这么设计)

解释执行虽然效率低,但是只需解释,在程序开始运行后就能立即执行,速度快。

编译执行虽然可以将一些热点代码缓存起来效率高,但是编译需要时间,速度慢。

所以Java在程序执行时使用解释器,编译器编译完成后使用编译器执行。

4.本地库接口

用来对接本地的方法,以及配合操作系统。

 4.1什么是本地方法

本地方法就是用native修饰的方法。

4.2为什么要用本地方法

 有时候我们需要用java操作我们电脑上的硬盘或者内存,因为Java属于应用程序语言没有权限对我们电脑上的硬盘或者内存直接操作。

例如:需要读取硬盘数据(new FileInputStream.read)或者读取内存地址(new Object().hashCode()),这时候就需要调用我们的本地方法。

二.垃圾回收

 Java中的垃圾就是运行程序中没有被指向和引用的对象,该对象被称为垃圾对象。

 1.早期垃圾回收

早期垃圾回收用使用c或c++需要程序员手动进行垃圾回收,方便灵活对内存的管理控制,但是需要程序员频繁对垃圾进行回收,一旦程序员忘记对某区域垃圾进行回收,时间一长,垃圾对象未清理就会产生内存泄露。

好处:可以精确管理内存,用的时候申请,不用的时候可以立即释放。

坏处:给程序员带来负担,一旦忘记释放垃圾就会一直存在。

2.现代的垃圾回收机制

 Java,c#,python等都是使用的垃圾回收机制。

优点:解放了程序员,不用再关心什么时候需要释放垃圾对象。

缺点:降低了程序员对内存的管理。

3.内存溢出和内存泄漏 

(1)内存溢出:内存满了不够用了,新产生的对象无法存放,Java会抛出内存溢出错误。

(2)内存泄露:一些对象已经不使用,垃圾回收器不能回收它,例如数据库连接对象,网络连接Socket对象还有IO对象,提供close()但是没有调用,那么垃圾回收器认为此对象还在使用中,这些对象就悄悄的占用着内存,这种现象被称为内存泄露,久而久之就会变为内存溢出。

4.垃圾回收的区域

运行时方法区里的堆和方法区是回收的区域。

堆是回收的重点区域,较少对老生代的垃圾回收,频繁对新生代的垃圾回收。

基本不回收方法区(在full gc整堆回收时才对方法区进行回收)。

 三.垃圾标记阶段算法

垃圾标记阶段算法主要就是判断哪些对象是垃圾对象,把没有指向引用的垃圾对象标记出来,在垃圾回收时进行垃圾回收。

 1.引用计数器算法(目前不使用)

对每个对象都加一个计数器,当有对象被指向或者引用时该计数器计数加一,当计数器为零时,表示该对象没指向或引用实现简单,则该计数器的对象为垃圾对象。(但引用计数器无法解决循环引用)

 1.1循环引用

当多个对象之间相互引用,计数器不为零,他们和外界没有联系了,不被使用了,但是他们之间又相互引用,垃圾回收无法回收,就会造成内存泄露。

2.可达性分析算法

可达性分析算法可以解决引用计数器中的循环引用问题。

可达性分析算法先从活跃对象开始查找,跟活跃对象相连的对象都是被使用的,不与活跃对象相连接的对象是垃圾对象。

2.1.哪些对象可以被称为活跃性对象

1.虚拟机栈中被引用的对象:例如各个线程被调用的方法中被使用的局部变量,参数等。

2.方法区中类静态属性引用的对象:Java类中引用类型静态变量。

3.所有被同步锁synchronized持有的对象。

4.Java虚拟机内部的引用。基本类型数据对应的class对象,一些常驻的异常类型对象(NullpointerException等),系统类加载器。

3.finalize机制

Java中的Object类中提供的finalize方法。

这个方法是当对象被垃圾回收器判定为垃圾时,在垃圾回收器回收这个对象时,会调用这个方法。

finalize方法规定只调用一次,当一个对象第一次被回收时,调用finalize,当这个对象在finalize中使用了,复活了,当第二次被判定为垃圾时,回收时不在调用finalize方法。

我们可以重写finalize方法,在对象被垃圾回收前执行一些最终操作,但是不建议在自己主动去调用该方法,此方法的内容要慎重,否则会影响垃圾回收器的性能。

例如:

package com.ffyc.javapro.jvm.gc;

public class CanReliveObj {

	public static CanReliveObj obj;//类变量,属于 GC Root


	//此方法只能被调用一次
	 @Override
	protected void finalize() throws Throwable {
		System.out.println("调用当前类重写的finalize()方法");
		obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
	}


	public static void main(String[] args) {
		try {
			obj = new CanReliveObj();
			// 对象第一次成功拯救自己
			obj = null;
			System.gc();//调用垃圾回收器,触发FULL GC  也不是调用后立刻就回收的,因为线程的执行权在操作系统
			System.out.println("第1次 gc");
			// 因为Finalizer线程优先级很低,暂停2秒,以等待它
			Thread.sleep(2000);
			if (obj == null) {
				System.out.println("obj is dead");
			} else {
				System.out.println("obj is still alive");
			}

			System.out.println("第2次 gc");
			// 下面这段代码与上面的完全相同,但是这次自救却失败了
			obj = null;
			System.gc();
			// 因为Finalizer线程优先级很低,暂停2秒,以等待它
			Thread.sleep(2000);
			if (obj == null) {
				System.out.println("obj is dead");
			} else {
				System.out.println("obj is still alive");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}


}

 

 3.1finalize方法存在分类的三种对象

由于finalize方法的存在,在虚拟主机中将对象的状态分为三种

可触及的:使用中的对象,没有被判定为垃圾
可复活的:被判定为垃圾对象但是还没有调用finalize方法 ,该对象有可能在 finalize()中复活。
不可触及的:对象第二次被判定为垃圾, 并且finalize被调用过了,该对象没有复活,那么就会进入不可触及状态。
以上 3 种状态中,是由于 finalize()方法的存在,进行的区分。只有在对象不可 触及时才可以被回收。

 四.回收阶段算法

在垃圾标记算法标记处垃圾对象后,再使用回收阶段算法,进行垃圾回收。

1. 标记复制算法

可以有多个内存,将内存分为多块,每次进行垃圾回收时将正在使用内存中存活的对象,复制到另一块未使用的内存块中,然后清除原来的内存块。

特点:

(1)使用多个内存块。

(2)回收时需要移动对象,适合存活对象少,内存小,垃圾对象多的场景。(新生代)

(3)回收后内存碎片少。

1.1如图所示标记复制算法图例 

2.标记清除算法

只需要一块内存空间,存活对象不需要移动,直接清除垃圾对象。

特点:

(1)只需要一块内存。

(2)不需要移动存活对象,只需要清除垃圾对象。

(3)回收后会产生内碎片问题。

(4)适合存活对象多,请对象较大的场景。(老生代)

2.1标记清除算法图例

 

3.标记压缩算法

在标记清除算法的基础上,要移动存活对象,将存活对象移动到内存一端,按顺序排放,清除边界之外区域。

特点:

(1)是一种移动式垃圾回收算法,回收后,内存中没有碎片。

(2)适合老年代回收

 3.1标记压缩算法图例

4.分代收集

不同的对象的生命周期是不一样的。因此, 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

年轻代存活对象少,垃圾多,使用标记复制算法进行回收。

老生代对象大,存活时间长,使用标记清除算法和标记压缩算法。

五.垃圾收集器

 垃圾收集器是jvm中内存回收的实践者。

垃圾回收器有很多,不同的虚拟机可以使用不同的垃圾回收器。

1.垃圾回收器分类

1.1从垃圾回收器线程数量上划分

(1)单线程:垃圾回收器只有一个线程在进行垃圾回收操作。(使用于小型简单的使用场景,垃圾回收时,其他用户的线程会暂停)

(2)多线程:垃圾回收器中有多个线程在进行垃圾回收操作。(在多 cpu 情况下大大提升垃

圾回收效率,但同样也是会暂停其他用户线程)

1.2从工作模式上分 

(1)独占式的:当垃圾回收线程执行时,其他用户线程暂停。(把用户线程暂停的线程称为stw

(2)并发式的:垃圾回收线程和用户线程可以同时执行,减少用户暂停次数和时间。

1.2.1工作模式垃圾回收器如下图

 

1.3按照工作内存上分

(1)新生代垃圾收集器

(2)老生代垃圾收集器

 

 2.CMS回收器

CMS(Concurrent Mark Sweep,并发标记清除)收集器,首创了垃圾收集线程和用户线程并发执行,追求低延时。
回收过程:
(1)初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。 (独占式的)
(2)并发标记:垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。 (并发式的)
(3)重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。 (独占式的)
(4)并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。 (并发式的)

3.G1垃圾收集器

 G1垃圾收集器适合整堆收集,不在区分新生代和老年代,适合服务器场景。

G1垃圾收集器将每个区域划分为更小的区域,例如将伊甸园区划分为多个小区域,优先回收垃圾对象较多的区域,故名做Garbage First,也支持并发执行。

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。

3.1g1垃圾收集内存分配如图

 

3.2G1垃圾收集器收集过程图示 

 

六.Jvm总结

总结:总的来说jvm是将java编译的字节码(.class)文件通过类加载器(也就是运输员)存放到内存也就是运行时数据区,因为字节码是底部数据指令不能由操作系统直接处理,所以需要再通过执行引擎(也就是翻译器)将将字节码转化为cpu能够处理的底部数据指令,这个过程要通过本地接口库借用其他语言接口来实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值