java村支书 · 10月22日

解锁JVM成神之路(一)

jvm指的是Java虚拟机,一种能够运行Java字节码的虚拟机。它能够模拟具有完整硬件系统功能,运行在一个完全隔离环境中的完整计算机系统。作为一种编程语言的虚拟机,它不只是专注于Java语言,只要生成编译文件匹配jvm对加载编译文件格式的要求,任何语言都可以由jvm编译运行。比如scala,kotlin等。

JVM的基本组成结构

jvm由三部分组成:

  • 类加载子系统
  • 运行时数据区(内存空间,内存结构)
  • 执行引擎

结构图如下:
file

深入了解运行时数据区

file

线程共享:

  • 方法区(1.8之后已经不叫方法区了,叫元数据区):用来存储被jvm加载的类信息,常量,静态变量等数据。也就是类的所有属性,方法以及构造器,接口代码都在这里定义。

这个区域包含一个很重要的区域叫常量池。class对象除了类的版本,属性,方法等信息外,还有一些常量以及字面值,而这些几乎都存在于常量池里,这部分内容将在被加载的过程中存放到常量池里。

  • 堆:实例化的对象以及数组都存在这个区域。这个区域的cg最活跃,也就是cg进行垃圾回收的重要场所。这个区域,cg是以分代回收算法进行垃圾回收的。从cg的角度看,堆又可以分为:新生代和老年代。

线程私有:

  • 程序计数器:也就是一个指针,指向方法区中的方法字节码。也就是用于存储每条线程的执行字节码指令地址。每条线程都会拥有自己的程序计数器,以便线程切换的时候,能使程序正常运行。
  • jvm栈:存储着每一个栈帧。每个方法的执行都对应着每一个栈帧,栈帧里放着局部变量表,操作数栈,动态连接,方法出口等信息。每个方法的执行以及结束都对应到栈帧的入栈与出栈。

file

  • 本地方法栈:这个跟jvm栈差不多,只是jvm栈为我们的java服务的,而本地方法栈是为native 方法服务。

深入了解类加载子系统

我们的Java文件被jvm编译器编译成class对象之后,在runtime时,由类加载子系统将class字节码文件加载到运行时数据区中。
但是,类加载子系统在类的整个生命周期中主要的工作是什么呢?
类的生命周期:
file

类的生命周期分为:
加载-->连接-->初始化-->使用-->卸载

而连接过程又分为:验证-->准备-->解析。其实这个过程就是类加载子系统工作的过程。

一,工作过程

  • 1,加载:将class字节码文件加载到运行时数据区中。
  • 2.1,验证:1,检查字节码文件是否正确。2,检查文件头的magic number是否正确。

何为magic number?我们用编辑器打开随意一个class文件,发现都是以CAFEBABE开头。把这数据改掉或者删除,都会出错。这个就是magic number

file

  • 2.2,准备:给类的静态变量分配内存,赋予初始值。这里的初始值并不是赋值的值。打个比方:private int i = 100; 在准备阶段,并不是将100赋值给i。而是给i赋值0,因为int的默认初始值是0。
  • 2.3,解析:检查指定的类是否引用了其他的类或者接口,是否能正常引入。也就是装载当前类引入依赖的其他类。
  • 3,初始化:这里才是给类的静态变量赋正确的初始值。i的值由0变为100。

使用,卸载这就没什么可说的了。

通过源码了解加载过程:
在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:

   //
  protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}  

源码的注释写得很清楚了:

  • 加载的时候使用了synchronized加锁机制,也就是当前线程有在加载当前这个类时,不允许其他类进行加载。
  • 判断这个类是否已经被加载,如果已经加载了,就不再加载。如果没有,则要加载。

我们在看源码时,看到

         //从parent这个成员变量中,我们得知
         private final ClassLoader parent;
         try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            }
        c = findBootstrapClassOrNull(name);                    
               

这是一个很重要的信息~由此引申一个下面很重要的概念

双亲委派机制

我们先了解类加载器的种类:

  • 启动类加载器Bootstrap ClassLoader:负责将java_home/lib下的类加载到内存中
  • 扩展类加载器Extension ClassLoader:负责将java_home/lib/extde的类加载到内存中
  • 系统类加载器Application ClassLoader:负责将classpath的类加载到内存中
  • 用户自定义加载器User ClassLoader:负责加载用户自定义路径下的类包

在看源码可以看出,每次加载类的时候,当前加载器都将该类一层一层交给parent,而且每次都检查是否已经加载了,如果加载了就不再加载。当交给parent时,如果parent能加载,则就加载。如果parent都不能加载,则由当前加载器进行加载。

双亲委派的优势:

  • 沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
  • 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子CJlassLoader再加载一次

以上就是jvm的内存结构以及类加载子系统的相关内容。jvm还有许许多多的知识,比如垃圾回收机制,调优等。

本人水平有限,难免有错误或遗漏之处,望大家指正和谅解,提出宝贵意见,愿与之交流。

0 阅读 65
推荐阅读
1 条评论
关注数
0
文章数
15
目录
qrcode
关注微信服务号
实时接收新的回答提醒和评论通知