一、Java为什么是一种跨平台的语言?
通常,我们编写的java源代码会被JDK的编译器编译成字节码文件,再由JVM将字节码文件翻译成计算机读的懂得机器码进行执行;因为不同平台使用的JVM不一样,所以不同的JVM会把相同的字节码文件翻译成不同操作系统认识的机器码,这样就实现了跨平台;
二、Java代码的执行流程
解释执行为主,编译执行为辅:
JIT编译器:当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为(Hot Spot Code 热点代码,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并保存到虚拟机内存中;
三、类加载的过程
3.1、加载
通过类的完全限定名称获取定义该类的二进制字节流。将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。
类加载器(就是加载类的)分为:
3.1.1、启动类加载器(Bootstrap ClassLoader):
加载java核心类库,不继承ClassLoader,只加载包名为java,javax,sun开头的类;
3.1.2、扩展类加载器(Extension ClassLoader):
加载javax开头的扩展类库,继承自ClassLoader,父类加载器为启动类加载器,从java.ext.dirs指定的路径下加载类库;或者从JDK安装目录的jre/lib/ext目录下加载类库,如果用户自定义的jar包放在jre/lib/ext下,也会自动由扩展类加载器加载;
3.1.3、应用程序类加载器(Application ClassLoader):
加载用户自定义或者第三方jar包,继承自ClassLoader,父类加载器为扩展类加载器,负责加载环境变量classpath或系统属性java.class.path指定的类库,java中自己写的类都是由应用程序类加载器加载的,可以通过ClassLoader.getSystemClassLoader()方法获取该类加载器;
双亲委派模型:
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
3.2、验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3.3、准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。初始值一般为 0 值。(如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。)
3.4、解析
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
3.5、初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
四、 主动引用
4.1、字节码指令
当 jvm 执行 new指令时会加载类。即:当程序创建一个类的实例对象。
当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值。
当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法。
4.2、反射
使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。
4.3、父类加载
加载一个类,如果其父类还未加载,则先触发该父类的加载。
4.4、主类
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main() 方法的类),虚拟机会先加载这个类。
4.5、接口的实现类
当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
五、被动引用
1,通过子类引用父类的静态字段,不会导致子类加载。
2,通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
3,常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
六、对象的创建过程
6.1、类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
6.2、分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。(选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。)
6.3、初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
6.4、设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
6.5、执行 init 构造方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java 程序的视角来看,对象创建才刚开始,<init> 构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。