JVM就是Java虚拟机,Java的跨平台机制就是建立在强大的Java虚拟机的基础上。Java是一种先编译后解释型的语言,当我们写了一段Java代码,在运行之前,它还是一个.java文件,里面是Java语言编写的源码,当Java文件被IDE执行,或者被显式利用javac命令进行编译后,java文件会首先被编译成字节码
文件,也就是.class结尾的文件。然后字节码文件再由不同平台的JVM虚拟机去解释/编译成机器识别的语言去让CPU运行。
Java文件编译成class文件之后,在不同平台上运行时(比如windows、Linux或者IOS等)不需要再次编译,因为JVM屏蔽了操作系统底层的差异,会把字节码解释/编译成不同平台上的机器指令。
这就是为什么Java语言有平台无关的特性,就归功于JVM的强大,一次编译,跨平台可用。
既然JVM是对class文件进行操作的,那么JVM是如何将.class文件加载到内存中的呢?主要利用的是Class Loader
来加载文件,下面看一张JVM组成的抽象结构图:
上图中:
Class Loader 字节码加载器
:也被称作为类加载器
,JVM通过类加载器将class文件,也就是类加载到JVM中。同时,Class Loader
也负责将加载后的字节码解析成JVM统一要求的对象格式。Excution Engine 执行引擎
:是JVM的核心组成部分之一,负责将字节码解释/编译成系统对应的机器指令。Native Interface 本地库接口
:融合不同开发语言的原生库为JVM所用,比如C++/C语言的库。JVM就是通过类加载器将字节码加载进内存当中的(JVM运行在内存中)。
在前文展示的JVM结构抽象图中,可以看到有一个区域被称为运行时数据区,这一部分是JVM运行最核心的部分。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域各自有各自的用途,以及创建和销毁的时间,有的区域会随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。
线程私有区域
:线程私有区域的内存空间是和线程的创建与销毁绑定的,每个线程都有各自独立的私有区域空间。
程序计数器
:
Java虚拟机栈
:
局部变量表,操作数栈、动态链接、方法出口
等一系列信息。
局部变量表
: 用来存储方法执行过程中的所有变量,可能是基本数据类型,也可能是对象引用等。操作数栈
:存储入栈、出栈、复制、交换、产生消费变量等信息。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。StackOverflowError
和OutOfMemoryError
异常。
本地方法栈
:
线程公有区域
: 线程公有区域的内存空间是随着虚拟机创建而创建的,其生命周期与虚拟机一致,所有线程共享公有区域空间,共享公有区域的数据。
Java堆
:
方法区
:
说到方法区,就很容易引出“永久代
”这个概念,尤其是在JDK1.8之前,在很多程序员眼中,永久代就是方法区。这是因为大部分的Java程序员都会在HotSpot虚拟机上进行开发和部署程序。
方法区
是《Java虚拟机规范》中对内存的逻辑划分,而“永久代
”这个概念,是JDK1.8之前HotSpot对方法区的一种实现方式。本质上这二者并不是等价的。对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。
永久代(PermGen space”)
:永久代的设计中,使用的是JVM虚拟机内存的一部分,这种设计导致了Java更容易遇到内存溢出的问题。因为永久代中存储的有字符串常量池、静态变量等数据,这部分数据很容易溢出。
元空间(MetaSpace)
:既然永久代有这么多缺点,那么Java虚拟机的开发人员自然需要想办法进行解决。
元空间(MetaSpace)
对其进行替代。元空间与永久代最大的不同就是使用了本地内存,不需要再去占用JVM内存空间了,这样就保证了方法区很少会出现内存溢出的问题。永久代中剩余的存储数据被完整移到了元空间中,永久代从此退出历史舞台。下面来具体讲解一下ClassLoader,ClassLoader负责将Class文件(这里的Class文件是指一种二进制字节流,包括但不限于在磁盘上的文件、网络、数据库或者动态生成的)加载到内存中,并转换成JVM统一要求格式的对象。
比如我们新建了一个Robot.java的文件,这个文件承载了我们写的源码——一个Robot类,然后经过javac指定的编译,就变成了一种承载二进制字节流的.class文件,也叫字节码文件,此时如果JVM正在运行的程序依赖这个字节码文件,ClassLoader就会加载Robot.class文件到内存中,并将其转换为Class
所以ClassLoader在Java中有着非常重要的作用,其主要工作在Class装载的加载阶段(一个Class的生命周期分为加载、验证、准备、解析、初始化、使用、卸载七个阶段,下文有具体分析),其主要作用是从系统外部获得Class二进制数据流。是Java的核心组件,在JVM中,所有的Class都是由ClassLoader进行加载的。在ClassLoader将二进制数据流加载到内存中后,JVM才能进行剩下的链接、初始化等操作。
ClassLoader一共有四种:
BootStrap ClassLoader
(启动类加载器):C++编写,用来加载核心库java.*
中的Class。
Extension ClassLoader
(扩展类加载器):Java编写,用来夹在扩展库javax.*
中的Class。
Application ClassLoader
(应用类加载器):Java编写,用来加载程序所在目录中的Class。
自定义ClassLoader
(自定义类加载器):Java编写,定制化加载策略。
实现一个自己的类加载器,自定义类加载器代码演示:
首先在桌面创建一个java文件Wali.java:
public class Wali{
static{
System.out.println("Hello Wali!");
}
}
然后调用javac编译成字节码文件Wali.class,最后在项目中创建一个自己的类加载器类MyClassLoader.java:
import java.io.*;
/**
* 自定义类加载器要继承自ClassLoader
*/
public class MyClassLoader extends ClassLoader {
// 加载路径
private String path;
// 自定义加载器的名字
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
// 覆盖findClass方法来自定义加载策略
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = classLoaderData(name);
return defineClass(name, b, 0, b.length);
}
/**
* 用来加载类文件
* @param name
* @return
*/
private byte[] classLoaderData(String name) {
// 生成文件全路径名
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream outputStream = null;
try {
in = new FileInputStream(new File(name));
outputStream = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
outputStream.write(i);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return outputStream.toByteArray();
}
}
最后对类加载器进行验证:
public class ClassLoderChecker {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader mc = new MyClassLoader("C:/Users/SMM/Desktop/", "myClassLoader");
Class c = mc.loadClass("Wali");
System.out.println(c.getClassLoader());
c.newInstance();
}
}
在JVM中,不同加载器之间是有层次关系的,下图展示的不同类加载器之间的关系就被称为类加载器的“双亲委派模型”
:
双亲委派模型要求除了顶层的启动类加载器之外,别的类加载器都应该有自己的父类加载器。不过这里的父类加载器一般不是以继承的关系实现的,而是通常使用组合的关系来复用父加载器的代码。即在子类中创建一个父类实例,通过委派的机制将加载的任务发给父类来做。
双亲委派机制加载类的流程如下,先是自底向上查找,然后自顶向下加载:
该流程描述:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的加载请求最终都会汇聚到顶层加载器,即启动类加载器中去尝试执行,只有当父类加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到这个类)时,子类才会尝试自己去加载。
加载流程的核心源码在ClassLoader
抽象类中的loadClass(String name, boolean resolve)
方法中,源码分析如下:
类加载器的双亲委派模型在JDK1.2时期被引入,并被广泛应用于此后几乎所有的Java程序中,但双亲委派机制并不是有强制性约束力的要求,而是Java设计者们推荐给开发者的一种类加载的最佳实践策略。
使用双亲委派模型来维护类加载器之间的关系,一个最明显的好处就是Java中的类随着它的类加载器一期具备了一种带有优先级的层次关系。比如java.lang.Object
这个类,是Java中非常重要的源码类之一,存放在rt.jar之中,无论是哪一个类加载器要加载这个类,最终都是要委派给顶层的启动类加载器
进行加载,因此Object类在程序的各种类加载环境中都可以稳定保证是同一个类。
反之,如果没有双亲委派模型,都任由各个类加载器自行去加载的话,就会出现类型体系紊乱的问题,比如自定义加载器A加载了Object类,自定义加载器B又加载了Object类,那么在JVM内存中这两个Object类对象是不一样的,依据类对象实例化的业务对象也会出现类型不匹配的问题。
或者用户自定义了一个java.lang.Object
类,并放在Classpath路径中,如果不使用双亲委派模型,那么系统中也会出现多个不同的Object类。
读者可以尝试自定义一个与rt.jar类库中已有类重名的Java类,会发现它虽然可以正常编译,但却永远无法被加载运行。
Java中的类加载机制是指把一个class二进制流加载到内存中并最终形成JVM可用的Java类型对象的流程机制,其中一共分为五个阶段
:加载、验证、准备、解析和初始化
。其中验证、准备、解析阶段
可以统称为链接
阶段。
而Java类的生命周期从被加载到JVM内存中开始,到卸载出内存为止,整个生命周期在类加载流程的基础上又多出了使用
和卸载
两个阶段。即:加载、验证、准备、解析、初始化、使用、卸载
。
加载
:加载(loading)是类加载(ClassLoading)的一个阶段,加载阶段就是类加载器将Class二进制字节流(一般是.class文件)加载进JVM内存,并生成一个代表这个类的Class对象。具体流程分为三步:验证
:验证是连接的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的约束。其中又包含了四种验证步骤:准备
:准备阶段是正式为类中定义的变量(类变量即静态变量,被static修饰的变量)分配内存并设置类变量初始的值的阶段。解析
:解析阶段是JVM虚拟机将常量池内的符号引用替换为直接引用的过程。初始化
:类的初始化阶段是类加载过程的最后一个阶段,在准备阶段的时候类变量已经被赋了初始值,在初始化阶段就会执行变量赋实际值值,并且该阶段会执行静态代码块中的内容。至此,类加载
的流程就结束了,经过加载、连接和初始化
,JVM内存中就会出现被加载类对应的Class对象了。
使用
:使用阶段是类的生命周期的一部分,但是已经不是类加载的阶段了,该阶段就是使用一个类提供的功能,包括主动引用和被动引用,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。卸载
:在类使用完之后,如果满足下面的情况,类就会被卸载:该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类的加载方式有两种:
显示加载中最常用的两种方式:loadClass()
和forName()
:首先,不管是loadClass()还是forName(),都可以获取类的属性和方法,也可以调用它的任意一个方法和属性(枚举类除外),这也是实现放射的重要方式。
loadClass方法
:在前文中,我们对loadClass的源码进行了分析,其中有一个参数是布尔值的resolve,这个参数默认是false的。
然后再loadClass真正干活的方法中会进行判断,如果resolve为true,就执行resolveClass()方法:
这个方法有什么用呢,点进去可以看到JDK的注释,非常清晰:
而默认传入的是false,也就是说,默认是不执行这一步的,也就是说,使用默认的loadClass()方法获得的Class对象时还没有进行连接的,只是执行了类装载机制中的第一步:加载。
forName方法
:同loadClass()一样,我们直接去看源码,源码之前没有秘密,forName()方法是Class类提供的一个方法,点进去就可以找到,可以发现在forName()的执行函数参数中有一个布尔值initalize:
forName0()
这个方法是一个Native的方法,在JDK中是看不到的,需要去OpenJDK的官网看源码,实际上当initialize为true的时候,就会执行连接和初始化操作。
也就是说Class.forName()得到的Class对象是已经初始化完成的,而Classloader.loadClass()得到的Class对象是只进行了加载还未连接的对象。
二者的使用场景:
forName()
:由于forName()会执行完初始化阶段,也就是说类中的静态代码块会被执行完毕,当我们的类中有静态代码块需要执行的时候,就适合使用forName()。比如著名的JDBC驱动在使用的时候就需要用forName(),这是因为JDBC驱动中有一个静态代码块是向DriveManager中注册自己,所以必须初始化。loadClass()
:而loadClass()虽然不会连接,但是因为只加载类,执行速度就非常快,当我们需要大量加载类,并且暂时用不到的时候,就可以使用loadClass()来进行延迟加载。比如著名的SpringIOC容器,就是使用loadClass()来进行延迟加载,提高加载速度,当用到类的时候,再进行初始化。Java的反射机制是指:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
在实际的开发中,我们很少会直接使用反射,但是反射在各种框架中经常使用,理解反射有利于我们理解框架的原理。下面贴一段反射使用的示例代码:
public class Robot {
// 私有成员变量
private String name;
// 公有方法
public void sayHi(String helloSentence) {
System.out.println(helloSentence + "" + name);
}
// 私有方法
private String throwHello(String tag) {
return "Hello" + tag;
}
}
import com.Robot;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectRobot {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 装载类,获取类对象
Class robotClass = Class.forName("com.Robot");
// 根据类对象实例化出对象
Robot robot = (Robot)robotClass.newInstance();
// 查看类对象的名字
System.out.println("Class Name Is " + robotClass.getName());
// 利用反射获取私有方法,参数为需要获取的方法名字和参数类的类型
Method getThrowHello = robotClass.getDeclaredMethod("throwHello", String.class);
// 强制允许访问私有方法
getThrowHello.setAccessible(true);
// 调用私有方法,传参为对象实例和方法需要的参数
Object str = getThrowHello.invoke(robot, "ReflectVisitPrivate");
System.out.println("Do throwHelle by reflect " + str);
// 利用反射获取公有方法
Method getSayHi = robotClass.getMethod("sayHi", String.class);
getSayHi.invoke(robot, "ReflectVisitPublic");
// 利用反射获取私有变量
Field name = robotClass.getDeclaredField("name");
// 强制允许访问私有变量
name.setAccessible(true);
// 给私有变量赋值
name.set(robot, "ReflectSetPrivateName");
getSayHi.invoke(robot, "ReflectVisitPublic2");
}
}
可以,可以通过构造一个自定义加载器,并且覆盖父类的loadClass
方法来加载指定的类,此时JVM虚拟机中就会有同一个类加载出的两个类对象,一个是自定义加载器加载的,一个是应用类加载器加载的。
而由于在JVM中,对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立在JVM中的唯一性,JVM中每一个类加载器都拥有一个独立的类名称空间。也就是说,只有在用一个类加载器加载的情况下,才能比较两个类是否“相等”(包括equals(), isAssignableFrom()等方法)。
而由于自定义类加载器和应用类加载器不是同一个类加载器,所以被这两个类加载器加载的同一个类,在JVM中实例出了两个不相等的类对象,互相独立。
Xms
:设定了堆空间的初始值。Xmx
:设定了堆空间能达到的最大值。Xss
:规定了每个线程的虚拟机栈的大小。java -Xms 128m -Xmx 128m -Xss 256k -jar xxxx.jar
静态存储
:编译时确定每个数据目标在运行时的存储空间需求。栈式存储
:数据区需求在编译时为止,运行时模块入口前确定。堆式存储
:编译时或者运行时模块入口都无法确定,此时需要动态分配。String s1 = new String("a");
和String s2 = "a";
二者相等么?""
中声明的字符串会在字符串常量池中直接创建出来。new
出来的字符串对象会先尝试在字符串常量池中创建,然后在堆空间中创建出来字符串对象。intern()
:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象的引用添加到池中,并返回此String对象的引用。
可以看另外一篇文章具体的分析:Java对象的结构与对象在内存中的结构。