java虚拟机篇一(基础)

21 阅读34分钟

什么是java编译器

Java的源代码(xxx.java)可以通过java的编译器生成字节码文件(xxx.class),java的编译器的功能有如下几点: 在这里插入图片描述 Java的字节码文件通过java虚拟机编译运行,java虚拟机有类加载器字节码校验器编译字节码(解析执行)JIT编译器(编译执行)编译出机器指令,最后经由操作系统运行机器指令得出结果。编译字节码和JIT编译器类似于java的执行引擎

java代码的执行流程

在这里插入图片描述

怎样判断两个class对象是否属于同一个类加载的?

在java虚拟机中表示两个class对象是否为同一个类的必要条件:

1、类的完整类名必须一致,包括包名。 2、加载这个类的CLassLoader(指的是ClassLoader实例对象)必须相同

总的来说:在java虚拟机中,两个类对象来源于同一个java源代码文件,被同一个虚拟机加载,只要是加载它们的ClassLoader实例对象不一样,那这两个类对象也是不一样的。 实例代码:

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
    	// 构建一个简单的类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("GenericsTest.JVMTest.Test816.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof GenericsTest.JVMTest.Test816.ClassLoaderTest);
    }
}

结果: 在这里插入图片描述

多语言编译为字节码在JVM运行

java语言是高级编程语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,编译字节码文件那就需要java虚拟机,先运行java虚拟机,编译java源代码成字节码文件,java虚拟机才能正确识别代码转换后的指令并将其运行。

什么是java虚拟机

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释编译为对应平台上的机器指令执行每一条java指令。java虚拟机规范中都有详细的定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。其在代码运行的过程中居最重要的地位。 在这里插入图片描述 java虚拟机的整体结构: 在这里插入图片描述

java虚拟机的作用

1、Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。 2、JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等 在这里插入图片描述 特点:

1、一次编译,到处运行 2、自动内存管理 3、自动垃圾回收功能

java虚拟机的架构模型

Java编译器输入的指令流基本上是一种基于栈的指令集架构,还有另外一种指令集架构则是基于寄存器的指令集架构。 基于栈式架构的特点:

  • 设计和实现更简单,适用于资源受限的系统;
  • 避开了寄存器的分配难题:使用零地址指令方式分配。
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。

基于寄存器架构的特点:

  • 典型的应用是x86的二进制指令集:比如传统的Pc以及Android的Davlik虚拟机。
  • 指令集架构则完全依赖硬件,可移植性差。
  • 性能优秀和执行更高效。
  • 花费更少的指令去完成一项操作。
  • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。

java虚拟机包含的部分

JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口),下图可以大致描述 JVM 的结构: 在这里插入图片描述 JVM 是执行 Java 程序的虚拟计算机系统,java代码的执行过程:首先需要准备好编译好的 Java 字节码文件(即class文件),计算机要运行程序需要先通过一定方式(类加载器)将 class 文件加载到内存中(运行时数据区),但是字节码文件是JVM定义的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执行,这个过程中会需要调用到一些不同语言为 Java 提供的接口(例如驱动、地图制作等),这就用到了本地 Native 接口(本地库接口)。

  • ClassLoader:负责加载字节码文件即 class 文件,class 文件在文件开头有特定的文件标示,并且 ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • Runtime Data Area:是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。几乎所有的关于 Java 内存方面的问题,都是集中在这块。
  • Execution Engine:执行引擎,也叫 Interpreter。Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。
  • Native Interface:负责调用本地接口的。他的作用是调用不同语言的接口给 JAVA 用,他会在 Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地 lib。原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。

java虚拟机的生命周期

虚拟机启动

Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

虚拟机执行

一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。执行一个所谓的Java程序的时候,实际上正在执行的是一个叫做Java虚拟机的进程。

虚拟机退出

有如下的几种情况:

  1. 程序正常执行结束
  2. 程序在执行过程中遇到了异常或错误而异常终止
  3. 由于操作系统出现错误而导致Java虚拟机进程终止
  4. 某线程调用Runtime类或system类的exit方法,或Runtime类的halt方法,并且Java'安全管理器也允许这次exit或halt操作。
  5. 除此之外,JNI ( Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。

java虚拟机的运行流程

JVM的启动过程分为如下四个步骤:

1、JVM的装入环境和配置 java.exe负责查找JRE,并且它会按照如下的顺序来选择JRE:

  • 自己目录下的JRE;
  • 父级目录下的JRE;
  • 查注册中注册的JRE。

2、装载JVM 通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。

3、初始化JVM,获得本地调用接口 调用InvocationFunction -> CreateJavaVM,也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。

4、运行Java程序

  • JVM运行Java程序的方式有两种:jar包 与 class。
  • 运行jar 的时候,java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。
  • 运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。

java程序的运行流程

编写好的 Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。如下图:

在这里插入图片描述

什么是类字节码文件?

class文件本质上是一个以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。 Class文件采用一种伪结构来存储数据,它有两种类型:无符号数和表。

Class文件的结构属性

在理解之前先从整体看下java字节码文件包含了哪些类型的数据: 在这里插入图片描述 代码实例:

public class Main {
    public static void main(String[] args) {
        int a = 100;
        System.out.println(a ++);
    }
}

使用javac Main.java编译,在当前目录下会有一个Main.class的文件,使用二进制工具打开查看可以得出下面的: 在这里插入图片描述

文件开头的4个字节("cafe babe")称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。

0000是编译器jdk版本的次版本号0,0037转化为十进制是55,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为11。

反编译字节码文件

使用到java内置的一个反编译工具javap可以反编译字节码文件, 用法: javap < options > < classes > 其中< options >选项包括:

用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

输入命令javap -verbose -p Main.class查看输出内容:

Classfile /D:/CodeProcess/JavaAlgorithm/src/GenericsTest/JVMTest/Main.class
  Last modified 2022-8-16; size 409 bytes
  MD5 checksum fad58d99a3d5b3103ce7329a3ec22f41
  Compiled from "Main.java"
public class GenericsTest.JVMTest.Main
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // GenericsTest/JVMTest/Main
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               Main.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               GenericsTest/JVMTest/Main
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{
  public GenericsTest.JVMTest.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: bipush        100
         2: istore_1
         3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: iload_1
         7: iinc          1, 1
        10: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        13: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 13
}
SourceFile: "Main.java"

字节码的文件信息

开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。 然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下: 在这里插入图片描述

常量池

常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符号(Descriptor)
  • 方法的名称和描述符

JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。 关于字节码的类型对应如下:在这里插入图片描述

方法表集合

在常量池之后的是对类内部的方法描述,在字节码中以表的集合形式表现,暂且不管字节码文件的16进制文件内容如何。代码来源

public com.rhythm7.Main();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: return
     LineNumberTable:
       line 3: 0
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       5     0  this   Lcom/rhythm7/Main;

这里是构造方法:Main(),返回值为void, 公开方法。 code内的主要属性为:

  • stack: 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1

  • locals :局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。

  • args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this

  • attribute_info:方法体内容,0,1,4为字节码"行号",该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的java/lang/Object."< init >":()V, 然后执行返回语句,结束方法。

  • LineNumberTable:该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。 -LocalVariableTable:该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。

  • start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。

jvm和类

当调用 Java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里 。 在同一个jvm的所有线程,所有变量都处于同一个进程里,它们使用jvm的内存区。不同的jvm内存区信息不共享。 jvm进程终止的几种原因:

1、程序运行到最后正常结束。
2、程序运行到System.exit()或Runtime.getRuntime().exit()代码区结束。
3、程序运行过程中遇到未捕获的异常或者错误而退出。
4、程序所在的平台强制结束了jvm的进程(命令行或者任务管理器强制杀死该进程)

两个jvm的运行是独立的,数据不互通

在java虚拟机中,运行两个不同的类,所运行的过程是独立的,它们的数据不互通。 实例代码:

public class B{
    static int a = 6;
    public B(){
        a++;
    }
}
public class ATest {
    public static void main(String[] args) {
        B.a++;
        System.out.println(B.a);
    }
}
public class BTest {
    public static void main(String[] args) {
        B b = new B();
        B.a++;
        System.out.println(b.a);
    }
 }

代码分析: ATest的输出是7,BTest的输出是8,ATest调用B.a++后没有在B类里面运行B类的构造函数,直接是静态域的a自加为7,BTest的输出是8,而不是9的原因是main函数里面的第一行代码创建了一个B的对象,创建的过程中默认调用了B的无参数构造器,所以a的值从6变成7,在main函数里面的第二行代码,再次引用了B类里面的变量自加,从7变成8。所以两个jvm的运行过程是独立的,数据不互通。

类的使用

java程序对类的使用分主动使用被动使用

主动使用

主动使用分为以下几种情况:

1、创建类的实例 2、访问某一个类或者接口的静态变量,或者对该静态变量赋值。 3、调用类的静态方法 4、反射(比如Class.forName("com.temp.Test01")) 5、初始化一个类的子类 6、java虚拟机启动时被标明是启动类的类时。 7、JDK7开始提供的动态语言支持,java.lang.invoke.MethodHandle实例的解析结果。REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化。

基本上除了以上七种情况,其他使用java类的方式都被看作类的被动使用,都不会导致类的初始化。

被动使用

实例代码1:

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}
// 通过子类引用父类的静态字段,不会导致子类初始化
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

代码分析: 上述代码运行之后,只会输出“SuperClass init! ”,而不会输出“SubClass init! ”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

package GenericsTest.JVMTest.Test816;
/**
* 通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

运行之后发现没有输出“SuperClassinit! ”, 说明并没有触发类GenericsTest.JVMTest.Test816.SuperClass的初始化阶段。

实例代码2:

public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

代码分析: 上述代码运行之后,也没有输出“ConstClass init! ”,这是因为虽然在Java源码中确实引用了ConstClass类的常量HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值“hello world”直接存储在NotInitialization类的常量池中,以后NotInitialization类对常量ConstClass.HELLOWORLD的引用,实际都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件后就已不存在任何联系了。

类的生命周期

类的加载时机 一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) (过程比较简单,实际上是:类访问方法区内的数据结构的接口, 对象是Heap区的数据。) 和卸载(Unloading) (卸载:java虚拟机结束生命周期) 七个阶段,其中验证、准备、解析三个部分统称为连接(Linking),如下图所示: 在这里插入图片描述 需要注意的是,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始注意这个开始,因为这些阶段通常是互相交叉进行的,会在一个过程执行中激活调用另外一个阶段),而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

类的加载: 查找并加载类的二进制数据

类加载指的是将类的 class 文件读入内存,并为之创建一个 java.lang. Class 对象,也就是说, 当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象 。当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化 。 加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

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

在这里插入图片描述 类的加载大致过程: 在这里插入图片描述

小结

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。 加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

连接

当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到 jre 中 。类连接又可分为如下三个阶段。

验证

验证阶段用于检验被加载的类是否有正确的内部结构,并和其他的类协调一致,这样不会危害到虚拟机的安全,验证主要包含以下几种:

  • 文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证: 对字节码描述的信息进行语义分析(注意: 对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证: 确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-X verifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
  • 假设一个类变量的定义为: public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的put static指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

另外还需要注意以下的几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
  • 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。假设上面的类变量value被定义为: public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析操作是随jvm执行完初始化后再执行。,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 在这里插入图片描述

初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值。
  • 使用静态代码块为类变量指定初始值。

clinit()方法

类的初始化阶段是执行类的构造器方法clinit()的过程,这个方法不需要定义,当类变量中不存在静态变量或者是静态代码块的时候,这个方法就不会被执行,这个是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块的语句合并来的。构造器方法中的指令按语句在源文件中出现的顺序来执行的。clinit()方法不同于类的构造器init()。如果该类拥有父类,jvm会保证子类的clinit()执行之前,父类的clinit()会先执行完毕。java虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。 实例代码1:

public class ThreadClassInitTest {
    public static void main(String[] args) {
        Runnable run = () ->{
            System.out.println(Thread.currentThread().getName()+"开始");
            ThreadClass threadClass = new ThreadClass();
            System.out.println(Thread.currentThread().getName()+"结束");
        };
        Thread thread1 = new Thread(run,"线程1");
        Thread thread2 = new Thread(run,"线程2");
        thread1.start();
        thread2.start();
        // java虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。
        // 线程1或者线程2加载类后出不来了,然后后续的代码块都没法执行了。
    }
}

class ThreadClass{
    static {
        if(true){
            System.out.println(Thread.currentThread().getName()+"初始化当前类");
            while (true){}
        }
    }
}

结果: 在这里插入图片描述 实例代码2:

public class ClassInitTest {
    static class Father{
        private static int a = 10;
        static {
            a = 20;
        }
    }
    static class Son extends Father {
      private static int b = Father.a;
    }

    public static void main(String[] args) {
        System.out.println(Son.b);// 20
        //clinit()方法不同于类的构造器init()。
        //如果该类拥有父类,jvm会保证子类的clinit()执行之前,
        //父类的clinit()会先执行完毕。
    }
}

初始化的步骤

(1)假如这个类还没有被加载和连接,程序先加载并连接该类。 (2)假如该类的直接父类还没有被初始化,则先初始化其直接父类。 (3)假如类中有初始化语句,则系统依次执行这些初始化语句。

jvm初始化的时机

  • 创建类的实例。为某个类创建实例的方式包括使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  • 调用某个类的静态方法。
  • 访问某个类或接口的静态属性,或为该静态属性赋值。
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName("Person")。
  • 初始化某个类的子类,当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类。
  • final型的静态属性,如果该属性可以在编译时就得到属性值,则可认为该属性可被当成编译时常量。当程序使用编译时常量时,系统会认为这是对该类的被动使用,所以不会导致该类的初始化。

实例代码:

public class ClassInitTest {
    private static int a = 10;

    static {
        a  = 20;// linking的prepare: a = 0 ---> initial: 10 --> 20;
        b = 10;//linking的prepare: b = 0 ---> initial: 10 --> 1;
         //System.out.println(number);//报错,非法的向前引用。
        System.out.println(ClassInitTest.number);// 正确
    }

    private static int number = 20;
    private static int b = 1;
    static {
        number = 100;// linking的prepare: number = 0 ---> initial: 20 --> 100;
    }
    
    public static void main(String[] args) {
        System.out.println("a = "+ClassInitTest.a);
        System.out.println("number = "+ClassInitTest.number);
        System.out.println("b = "+ClassInitTest.b);
    }
} 

结果: 在这里插入图片描述

使用

类访问方法区内的数据结构的接口, 对象是Heap区的数据。

卸载

Java虚拟机将结束生命周期的几种情况

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

ClassLoader类

loadClass(String name, boolean resolve)根据指定的二进制名称来加载类,该方法为ClassLoader的入口点,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。 findClass(String name)根据二进制名称来查找类

如果需要实现自定义的ClassLoader,可以通过重写以上两个方法来实现,优先推荐重写findClass(String name)方法。因为重写 findClassO方法可以避免覆盖默认类加载器的父类委托 和缓冲机制两种策略 ; 如果重写 loadClassO方法 ,则实现逻辑更为复杂。

loadClass()方法的执行步骤如下 :

1、用 findLoadedClass(String) 来检查是否己经加载类 , 如果已经加载则直接返回 。 在父类加载器上调用 2、 loadClassO方法。 如果父类加载器为 null ,则使用根类加载器来加载 。 3、 调用 findClass(String)方法查找类。

ClassLoader 里还包含如下一些普通方法:

1、 findSystemClass(String name): 从本地文件系统装入文件 。 它在本地文件系统中寻找类文件,如果存在,就使用 defineClassO方法将原始字节转换成 Class 对象,以将该文件转换成类。

2、static getSystemClassLoaderO: 这是一个静态方法,用于返回系统类加载器

3、getParent(): 获取该类加载器的父类加载器 。

4、resolveClass(Class<?> c): 链接指定的类 。 类加载器可以使用此方法来链接类 c 。

5、findLoadedClass(String name): 如果此 Java 虚拟机己加载了名为 name 的类,则直接返回该类对应的Class 实例,否则返回 null。该方法是 Java 类加载缓存机制的体现。

源码展示:

import java.io.*;
import java.lang.reflect.*;

public class CompileClassLoader extends ClassLoader
{

	private byte[] getBytes(String filename)
		throws IOException
	{
		File file = new File(filename);
		long len = file.length();
		byte[] raw = new byte[(int)len];
		try(
			FileInputStream fin = new FileInputStream(file))
		{

			int r = fin.read(raw);
			if(r != len)
			throw new IOException("无法读取全部文件"
				+ r + " != " + len);
			return raw;
		}
	}

	private boolean compile(String javaFile)
		throws IOException
	{
		System.out.println("CompileClassLoader:正在编译 "
			+ javaFile + "...");
		// 调用系统的javac命令
		Process p = Runtime.getRuntime().exec("javac " + javaFile);
		try
		{
			// 其他线程等待这个线程完成
			p.waitFor();
		}
		catch(InterruptedException ie)
		{
			System.out.println(ie);
		}

		int ret = p.exitValue();

		return ret == 0;
	}

	protected Class<?> findClass(String name)
		throws ClassNotFoundException
	{
		Class clazz = null;

		String fileStub = name.replace("." , "/");
		String javaFilename = fileStub + ".java";
		String classFilename = fileStub + ".class";
		File javaFile = new File(javaFilename);
		File classFile = new File(classFilename);

		if(javaFile.exists() && (!classFile.exists()
			|| javaFile.lastModified() > classFile.lastModified()))
		{
			try
			{

				if(!compile(javaFilename) || !classFile.exists())
				{
					throw new ClassNotFoundException(
						"ClassNotFoundExcetpion:" + javaFilename);
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}

		if (classFile.exists())
		{
			try
			{

				byte[] raw = getBytes(classFilename);

				clazz = defineClass(name,raw,0,raw.length);
			}
			catch(IOException ie)
			{
				ie.printStackTrace();
			}
		}

		if(clazz == null)
		{
			throw new ClassNotFoundException(name);
		}
		return clazz;
	}

	public static void main(String[] args) throws Exception
	{

		if (args.length < 1)
		{
			System.out.println("缺少目标类,请按如下格式运行java源文件:");
			System.out.println("java CompileClassLoader ClassName");
		}

		String progClass = args[0];

		String[] progArgs = new String[args.length-1];
		System.arraycopy(args , 1 , progArgs
			, 0 , progArgs.length);
		CompileClassLoader ccl = new CompileClassLoader();

		Class<?> clazz = ccl.loadClass(progClass);

		Method main = clazz.getMethod("main" , (new String[0]).getClass());
		Object[] argsArray = {progArgs};
		main.invoke(null,argsArray);
	}
}

URLClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,而不是父类加载器,这里是类与类之间的继承关系),URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。 应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader类提供了如下两个构造器:

URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询、并加载类。

URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能前一个构造器相同。

类加载器

java虚拟机的类加载器一共有三种,分别是启动类加载器(引导类加载器(根类加载器):Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(系统类加载器 AppClassLoader)。还有就是在java程序中,我们可以自行定义一个类加载器,这个类加载器被划分为自定义类加载器。如图: 在这里插入图片描述

实例代码:

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统类加载器:"+systemLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 获取系统类加载器的上层,扩展类加载器
        ClassLoader parent = systemLoader.getParent();
        System.out.println(parent); // sun.misc.Launcher$ExtClassLoader@61bbe9ba

        Enumeration<URL> eml = null;
        try {
            eml = systemLoader.getResources("");
        } catch (IOException e) {
            e.printStackTrace();
        }
        while(eml.hasMoreElements()){
            System.out.println(eml.nextElement()); // 代码路径/java/AnnotationAndReflection/target/classes/
        }
        //获取系统类加载器的父类加载器,得到扩展类加载器
        ClassLoader extensionLader = systemLoader.getParent();
        System.out.println( " 扩展类加载器 : " + extensionLader);// sun.misc.Launcher$ExtClassLoader@61bbe9ba
        System.out.println(" 扩展类加载器的加载路径: "
                + System.getProperty (" java.ext.dirs " ));
        System.out.println("扩展类加载器的 parent: "
                + extensionLader . getParent());
    }
}

结果: 系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是 null,扩展类加载器的父加载器是 null,并不是根类加载器。这是 因为根类加载器并没有继承 ClassLoader 抽象类 ,所以扩展类加载器的getParent()方法返回 null 。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器并不是 Java 实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回 null 。 在这里插入图片描述

启动类加载器

  • 这个类加载器使用C/C++实现的,内嵌在jvm的内部(一般的类加载器访问不到)
  • 它用来加载java的核心类库,(比如JAVA_HOME/jre/lib/resources.jar或者是sun.boot.class.path路径下的内容)用于提供jvm需要的类
  • 不继承自java.lang.ClassLoader,没有父类加载器。(它最大)
  • 加载扩展类加载器和应用程序类加载器,并且为他们指定父类加载器。
  • 为了安全考虑,BootstrapClassLoader只是会加载sun、javax、java等开头的类。
  • 启动类加载器是无法被Java程序直接引用的。

扩展类加载器

  • java 语言编写的,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或者是从jdk安装的目录jre/lib/ext子目录的(扩展类库)下加载类库,如果用户创建jar文件放在此目录下也会自动有扩展类加载器加载。

应用程序类加载器

  • java 语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或是系统属性,java.class.path路径下的类库。
  • 该类是程序中默认的类加载器。java的应用的类都是由该加载完成。
  • 通过ClassLoader.getSystemClassLoader()可以获取该类加载器。
  • 开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类的加载器

在java程序开发的过程中,类的加载基本上都是由上述三类加载器加载完成,在必要的时候可以自己定义类加载器。 自定义类加载器可以实现一下几种常见的功能:

执行代码前自动验证数字签名 根据用户提供的密码解密代码,从而实现代码混淆器来避免反编译*.class文件 根据用户需求来动态地加载类 根据应用需求把其他数据以字节码的形式加载到应用中。

自定义类加载器好处:

1、隔离加载类
2、修改类加载的方式
3、扩展加载源 4、防止源码泄露

类加载器的数据来源

类的加载器加载的数据来源有以下几种:

1、从本地文件系统加载 class 文件,这是绝大部分示例程序的类加载方式 。
2、从 JAR 包加载 class 文件,jvm可以从 JAR 文件中 直接加载该 class 文件 。
3、通过网络加载class文件
4、把一个java的源文件动态编译并执行加载
5、从专有的数据库中提取class文件,比较少见。
6、从加密文件中获取,这是典型的防止class文件被反编译的措施。
7、由其他文件生成,典型的场景:jsp应用。

类加载器加载class的步骤

  1. 检测此 Class 是否载入过(即在缓存区中是否有此 Class ) ,如果有则直接进入第 8 步,否则接着执行第 2 步 。
  2. 如果父类加载器不存在( 如果没有父类加载器 ,则要么 parent 一定是根类加载器,要么本身就是根类加载器) ,则跳到第 4 步执行 :如果父类加载器存在 ,则接着执行第 3 步。
  3. 请求使用 父类加载器去载入目标类 ,如果成功载入则跳到第 8 步 , 否则接着执行第 5 步。
  4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第 8 步 , 否则跳到第 7 步。
  5. 当前类加载器尝试寻找 Class 文件 ( 从与此 ClassLoader 相关的类路径中寻找),如果找到则执行第 6 步 , 如果找不到则跳到第 7 步。
  6. 从文件中载入 Class , 成功载入后跳到第 8 步。
  7. 抛出 ClassNotFoundException 异常。
  8. 返回对应的 java .lang.Class 对象。

其中 ,第 5 、6 步允许重写 ClassLoader的 findClassO方法来实现自己的载入策略 ,甚至重写 loadClass()方法来实现自己的载入过程 。

类的加载机制

类的加载机制一共有四种,分别是全盘负责机制,父类委托机制,缓存机制,还有最重要的双亲委派机制。

全盘负责

所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

父类委托

所谓父类委托则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制

缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,并存入cache。这就是为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。(因为此时缓存中不存在修改后的Class对象)

双亲委派机制

java虚拟机对于class文件是按需加载也就是说需要该类时才会把它的class文件加载进内存里生成class对象,而且加载某个class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。 双亲委派机制的代码实现:

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;
        }

双亲委派机制的工作原理为:

  1. 当一个类加载器收到类加载请求,它不会自己先去加载,而是委派父类加载器去加载这个类。
  2. 如果父类加载器之上还存在父类加载器,则进一步委托,依次递归,最后到达顶层的引导类加载器(这个加载器是由C/C++书写的,一般是无法访问的。)
  3. 如果父类加载器可以成功加载这个类,则返回加载成功的数据,如果父类加载器无法加载,那子加载器会尝试加载这个类。

双亲委派机制的过程:

  1. 当AppClassLoader(应用程序类加载器)加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader(扩展类加载器)加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader(引导类加载器(根类加载器))去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

具体过程如图所示: 在这里插入图片描述

实例代码:

package GenericsTest.JVMTest;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        System.out.println("parent ClassLoader: "+ myLoader.getParent());
        Class<?> clazz = myLoader.loadClass("GenericsTest.JVMTest.Main");
        System.out.println("class name: " + clazz.getSimpleName() + " \nclass hashcode: " + clazz.hashCode() + " \nloader: " + clazz.getClassLoader().getClass().getSimpleName());

    }
}

结果: 可以看到的是打印出来的类加载器是APPClassLoader(应用程序类加载器),而非自定义的类加载器,为什么呢?这就涉及到了双亲委派机制, 在这里插入图片描述 当我们用 myLoader 去加载 GenericsTest.JVMTest.Main.java 这个类的时候,根据双亲委托机制,自定义的类加载器 myLoader 会委托它的父加载器 AppClassLoader 去加载, AppClassLoader 应用类加载器又会委托它的父类加载器 Bootstrap ClassLoader 启动类去加载。而 Bootstrap ClassLoader 找不到这个类,然后让 AppClassLoader 去加载,而 AppClassLoader 加载的路径是项目的 ClassPath, 这时候找到了 Main类并加载了它,并没有让 myLoader 去加载。

双亲委托机制还有下面的一些特点:

  1. 如果没有显示地传递一个双亲类装载器给用户自定义的类装载器的构造方法,系统装载器就默认被指定为双亲。
  2. 如果传递到构造方法的是一个已有的用户自定义类型装载器的引用,该用户自定义装载器就被作为双亲。
  3. 如果传递的方法是一个 null, 启动类装载器就是双亲。
  4. 在类装载器之间具有了委派关系,首先发起装载要求的类装载器不必是定义该类的类装载器。

沙箱安全机制

这个机制主要是保护java源代码信息的。如果我们自己建立和源代码相同的包,例如创建类名相同的String类,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这就是沙箱保护机制。

实例代码:

package java.lang;

/**
 * 报错:
 * 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
 *    public static void main(String[] args)
 * 否则 JavaFX 应用程序类必须扩展javafx.application.Application
 * 
 */
public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

结果分析: 这个程序可以运行,但是它不会加载当前的java.lang.String,而是会加载默认的java.lang.String包,在那个包下没有main方法,而当前程序需要运行main方法,因此会报错。

文章参考