Java虚拟机整体架构祥图

image-20211104235321347

类加载器

  1. 虚拟机自带的加载器

  2. 启动类(根、BootStrap)加载器

  3. 扩展类加载器

  4. 应用程序(系统)加载器

public static void main(String[] args) {
    ClassLoader classLoader = OtherTest.class.getClassLoader();
    //AppClassLoader,应用程序加载器   实现java.lang.abstract
    System.out.println(classLoader);
    //ExtClassLoader,扩展加载器   jre/lib/ext目录
    System.out.println(classLoader.getParent());
    //null,不存在或者java无法获取   rt.jar
    System.out.println(classLoader.getParent().getParent());
}

双亲委派机制

  1. 类加载器收到加载类的请求

  2. 将这个请求向上委托父类加载器去完成,一直向上委托到根加载器

  3. 根加载器检查是否能够加载此类,能加载就加载并结束;不能加载则抛出异常,通知子加载器进行加载

  4. 重复步骤3

示例代码:

package java.lang;
/*
 * @Description
 * @Author Doubly
 * @Date 2021/10/8
 */
public class String {
    public String toString(){
        return "Hello";
    }
    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.toString());
    }
}

运行结果:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application           

为了保证程序的安全,首先在AppClassLoader中找String,虽然已经找到,但是会继续在ExtClassLoader中找,最后在BootStrapClassLoader中找,找到rt.jar下面的String。所以不会家在我们自己写的String,会报没有main方法的错误。

AppClassLoader -> ExtClassLoader -> BootstrapClassLoader(最终执行)

native关键字

凡是带了native关键字的,说明Java的作用范围达不到了。会进入本地方法栈,通过本地方法接口(JNI)调用本地方法库。比如Object中的hashcode()方法,就是调用的本地方法。

方法区

静态变量常量类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存,和方法区无关

8大基本类型+对象引用+实例的方法

一个jvm只有一个堆内存,堆内存大小是可以调节的。

默认分配的总内存是电脑的1/4,初始化的内存是电脑的1/64.

代码示例:

public class Test {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory();
        //返回虚拟机初始化使用的总内存
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("max:"+maxMemory + "字节 " + (maxMemory/(double)1024/1024) + "MB");
        System.out.println("max:"+totalMemory + "字节 " + (totalMemory/(double)1024/1024) + "MB");
    }
}

结果:

max:1908932608字节 1820.5MB 
max:128974848字节 123.0MB  

堆内存细分三个区域

  • 新生区(伊甸园区+幸存区0区+幸存区1区)(幸存区也叫from区与to区)

  • 养老区

  • 永久区

新生区

伊甸园区

所有的对象都是在伊甸园区new出来的,伊甸园区满了会出发轻gc

幸存区(0区+1区)

gc触发后,还存在引用指向对象,对象就不会被回收,会进入幸存区。伊甸园区和幸存区都满了以后会今进行重gc。

养老区

幸存区满了之后,会进入养老区。一般99%的对象都会被清除,默认当一个对象经历15次后可以进入养老区。养老区满了之后就出现了OOM(Out Of Memory)。

查看JVM堆中每个区的情况,可使用如下参数

JVM参数:

-Xms1024m -Xmx2048m -XX:+PrintGCDetails   

输出:

Heap
 PSYoungGen      total 305664K, used 20971K [0x0000000795580000, 0x00000007aaa80000, 0x00000007c0000000)
  eden space 262144K, 8% used [0x0000000795580000,0x00000007969fafb8,0x00000007a5580000)
  from space 43520K, 0% used [0x00000007a8000000,0x00000007a8000000,0x00000007aaa80000)
  to   space 43520K, 0% used [0x00000007a5580000,0x00000007a5580000,0x00000007a8000000)
 ParOldGen       total 699392K, used 0K [0x0000000740000000, 0x000000076ab00000, 0x0000000795580000)
  object space 699392K, 0% used [0x0000000740000000,0x0000000740000000,0x000000076ab00000)
 Metaspace       used 3104K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 388K, committed 512K, reserved 1048576K
           

OOM之后如何排查问题?

  1. 尝试扩大堆内存看结果

  2. 分析内存,看一下哪个地方出现了问题(专业工具)

内存快照分析工具:MAT(Eclipse最早继承),Jprofiler

MAT、JProfiler作用:

  1. 分析Dump内存文件,快速定位内存泄漏

  2. 获得堆中的数据

  3. 获得大的对象

如何获取JVM的dump文件?

-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
public class Test {
    //-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
    public static void main(String[] args) {
        String str = "MyString";
        while (true){
            System.out.println(str);
            str += new Random(999999999);
        }
    }
}    

永久区/元空间

逻辑上存在,物理上不存在。新生区+老年区=jvm使用的堆内存

这个区域常驻内存的,用来存放jdk自身携带的Class对象、Interface元数据,存储的是Java运行的一些环境或类信息。这个区域不存在垃圾回收。关闭虚拟机就会释放这个区域的内存。

历史:

  • Jdk1.6之前:永久代,常量池在方法区重

  • JDK1.7:永久代,但是慢慢的退化了。去永久代,常量池在堆中

  • JDK1.8以后:无永久代,常量池在元空间

垃圾回收

垃圾回收主要是在伊甸园区和养老区

垃圾回收可分为两种:

  1. 轻量级垃圾回收(轻gc)

    ​ 回收新生区和偶尔幸存区

  2. 重量级垃圾回收(重gc、full gc)

    ​ 养老区满了,回收养老区

JVM如何判断对象需要回收?

  1. 引用计数法:每个对象引用次数做计数,每次被引用则计数+1,失去引用则-1。当计数为0的就清除。
  2. 可达性算法:从GC Roots开始查找引用,能够被找到的说明被引用可达,其余的则引用不可达,需要清除。

什么是GC Roots?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的native方法)中引用的对象

gc算法

标记清除法:扫描对象,对活着的对象进行标记;对没有标记的对象进行清除

​ 优点:不需要额外空间

​ 缺点:标记和清除两次扫描,严重浪费时间;会产生内存碎片

标记压缩:再对标记清除算法进行压缩整理

​ 再次扫描,向一段移动存活对对象,防止内存碎片的产生

复制算法:新生区以及from区的存活对象复制到to区,to区永远是空的

​ 优点:没有内存碎片

​ 缺点:浪费空间,to区永远是空的

​ 最佳使用场景:对象存活度较低,新生区

总结

内存效率:复制算法>标记清除>标记压缩(时间复杂度)

内存整齐度:复制算法=标记压缩>标记清除

内存利用率:标记压缩=标记清除>复制算法

没有最优的算法,只有最合适的算法。

所以GC使用分代收集算法

新生区:存活率较低,使用复制算法

老年区:存活率较高,使用标记清除+标记压缩算法