JVM和类
当调用Java命令运行某个Java程序时,该命令将会启动一个java虚拟机进程,不管该java程序有多么复杂,该程序启动了多少个线程,它们都处于该java虚拟机进程里。同一个JVM的所有线程,所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止。
- 程序运行到最后正常结束
- 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序
- 程序执行过程中遇到未捕获的异常或错误而结束
- 程序所在平台强制结束了JVM进程
当java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。
下面看一段程序代码:
public class A {
/**
* 定义一个类变量
*/
public static int value = 6;
}
public class A1 {
public static void main(String[] args) {
A.value ++;
// 7
System.out.println("A's value = " + A.value);
}
}
public class A2 {
public static void main(String[] args) {
// 6
System.out.println("A's value = " + A.value);
}
}
相信大家结果没有异议,因为A1和A2都有main()方法,运行了两次JVM。第一次运行A1的main()方法结束后,JVM也结束了,它对A类所做的修改将全部丢失,A2运行main()方法时,JVM将再次初始化A类
我们再来看看这段代码
public class A3 {
public static void main(String[] args) {
A.value ++;
// 7
System.out.println("A3 A's value = " + A.value);
A4 a4 = new A4();
a4.getValue();
}
}
public class A4 {
// 7
public void getValue(){
System.out.println("A4 A's value = " + A.value);
}
}
现在A3,A4中value的值是一样的,因为只有运行了一个main()方法,A3,A4访问到的A的value处于同一个JVM进程中,值是一样的。
类的加载
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以也将这三个步骤统称为类加载或类初始化。
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何的类时,系统都会为之建立一个java.lang.Class对象。系统中所有的类实际上也是实例,它们都是java.lang.Class的实例。
类的加载由类加载器完成,类加载通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载class文件,这是前面绝大java程序的类加载方式
- 从JAR包加载class文件,这种方式也很常见。我们使用第三方jar包时,一般先下载下来,放在lib目录下,如JDBC编程时用到的数据库驱动类
- 通过网络加载class文件
- 把一个java源文件动态编译,并执行加载
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
所谓的加载就是读取class文件。
类的连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接分为三个 阶段。 - 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
- 准备:类准备阶段则负责为类的类变量(static)分配内存,并设置默认初始值
- 解析:将类的二进制数据中的符号引用替换为直接引用(可以理解为将变量替换成真正的值,一般是指向存储地址)
1.符号引用(Symbolic References):
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、
CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类
将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
2.直接引用(Direct References):
直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在Java类中对类变量指定初始值有两种方式:
- 声明类变量时指定初始值
- 使用静态初始化块为类变量指定初始值
public class ClassInit {
/**
* 声明时指定初始值
*/
static int a = 1;
static int b;
static {
/**
* 使用静态初始化块指定初始值
*/
b = 2;
}
}
JVM初始化一个类包含如下几个步骤:
-
假如这个类还没有被加载和连接,则程序先加载并连接该类
-
假如该类的直接父类还没有被初始化,则先初始化其直接父类(所以JVM最先初始化得总是java.lang.Object类)
-
假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化的时机
当Java程序首次通过下面6中方式来使用某个类或接口时,系统就会初始化该类或接口。 -
创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例
public class B {
static {System.out.println("B is init……");}
}
public class C implements Serializable {
static {System.out.println("C is init……");}
}
public class Test {
public static void main(String[] args) throws Exception{
// 反射
Class<B> b = B.class;
// 输出 B is init……
B b1 = b.newInstance();
/**
* 序列化,先将C类序列化后写入c.txt
* 序列化的目的是将java对象转换成二进制编码,方便在网络中传输,
* 如果为了安全,怕类中的某个属性值在网络传输中被篡改或窃取,用transient修饰该属性
*/
/*ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("c.txt"));
C c = new C();
outputStream.writeObject(c);*/
// 反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("c.txt"));
// 输出 C is init……
C bankUp = (C)inputStream.readObject();
}
}
- 调用某个类的类方法(静态方法)
- 访问某个类或接口的类变量,或为该类变量赋值
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
public class D {
static {System.out.println("D is init……");}
}
public class Test {
public static void main(String[] args) throws Exception{
/**
* name要补全全路径,否则会抛ClassNoFound异常
*/
Class<?> d = Class.forName("cn.crazy.reflect.D");
// 输出 D is init……
D d1 = (D)d.newInstance();
}
}
- 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化
- 直接使用java.exe命令来运行某个主类,当运行某个主类时,程序会先初始化该主类
对应一个final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量也不会导致类的初始化。
public class A {
static {System.out.println("A is init……");}
/**
* 定义一个类变量
*/
public static int value = 6;
public static final int finalValue = 1;
public static void test(){
System.out.println("A's test()");
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 输出 A's finalValue = 1
System.out.println("A's finalValue = " + A.finalValue);
/** 输出:
* A is init……
* A's value = 6
*/
System.out.println("A's value = " + A.value);
}
}
public class Time {
static {
System.out.println("Time is init……");
}
static final String compileConstant = System.currentTimeMillis() + "";
}
public class TimeTest {
public static void main(String[] args) throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();
/**
* 加载,不会进行初始化
*/
Class<?> time = loader.loadClass("cn.crazy.reflect.Time");
/**
* 初始化
* 输出 Time is init……
*/
Class.forName("cn.crazy.reflect.Time");
}
}
从运行结果可以看出,必须等到执行Class.forName(“cn.crazy.reflect.Time”)时才完成对Time类的初始化
类加载器
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生对应的java.lang.Class对象。
类加载器简介
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识,在java中,一个类用器全限定类名(包含包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这就意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、k2)是不同的、它们所加载的类也是完全不同、互不兼容的。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。
- Bootstrap ClassLoader:根类加载器
Boostrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载java的核心类。在Sun的JVM中,当执行java.exe
命令时,使用-Xbootclasspath选项或使用-D选项指定sun.boot,class.path系统属性值可以指定加载附加的类;
根加载器比较特殊,它并不是java.lang.ClassLoarder的子类,而是由JVM自身实现的,
public class BootstrapLoader {
public static void main(String[] args) {
/**
* 获取根类加载器所加载的全部URL数组
*/
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
/**
* 遍历输出
*/
for (URL url : urLs) {
System.out.println("url = " + url);
}
}
}
输出结果:
url = file:/D:/java/jre/lib/resources.jar
url = file:/D:/java/jre/lib/rt.jar
url = file:/D:/java/jre/lib/sunrsasign.jar
url = file:/D:/java/jre/lib/jsse.jar
url = file:/D:/java/jre/lib/jce.jar
url = file:/D:/java/jre/lib/charsets.jar
url = file:/D:/java/jre/lib/jfr.jar
url = file:/D:/java/jre/classes
程序中可以使用String、System这些核心类库--因为这些核心类库都在D:\java\jre\lib\rt,jar
- Extension ClassLoader: 扩展类加载器
Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)
中JAR包的类
通过这种方式,就可以为java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放在%JAVA_HOME%/jre/lib/ext路径即可
- System ClassLoader:系统类加载器
System ClassLoader 被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,
或CLASSPATH环境变量所指定的JAR包和路径()。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别
指定,则用户自定义的类加载器都以类加载器作为父加载器。
类加载机制
JVM的类加载器机制主要有如下三种:
- 全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示指定使用另一个类加载器进行加载。
- 父类委托。所谓父类委托,则是先让parent(父)类加载器试图加载该Class,只有在父加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存区不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改Class后,必须重新启动JVM。程序所做的修改才会生效的原因。
除了可以使用Java提供的类加载器外,开发者可以自定义类加载器。自定义的类加载器通过继承ClassLoader来实现。JVM中这4中类加载器的层次结构如下:
双亲委派机制
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
这就是双亲委派模式。
来看一段代码:
public class SystemLoader {
public static void main(String[] args) throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器: " + loader);
/**
* 获取系统类加载器的加载路径--通常由CLASSPATH环境变量指定
* 如果操作系统没有指定CLASSPATH环境变量。则默认当前路径作为
* 系统类加载器的加载路径(我没有指定CLASSPATH)
* (我的项目放在F:/Java)
*/
Enumeration<URL> resources = loader.getResources("");
while (resources.hasMoreElements()){
System.out.println("resource = " + resources.nextElement());
}
/**
* 获取系统类加载器的父类加载器
*/
ClassLoader extensionLoader = loader.getParent();
System.out.println("SystemClassLoader's parent is : " + extensionLoader);
/**
* 扩展类加载器的加载路径
*/
System.out.println("扩展类加载器的加载路径: " + System.getProperty("java.ext.dirs"));
/**
* 扩展类加载器的父类加载器
*/
System.out.println("args = " + extensionLoader.getParent());
}
}
输出:
系统类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
resource = file:/F:/Java/out/production/Java/
SystemClassLoader's parent is : sun.misc.Launcher$ExtClassLoader@135fbaa4
扩展类加载器的加载路径: D:\Java\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
args = null
我的项目路径:
从上面的运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是%JAVA_HOME%/jre/lib/ext,此处看到扩展类加载器的父类加载器是null,并不是根加载器。这是因为根加载器没有继承ClassLoader抽象类,所以扩展类加载器的getParent()返回null。但实际上,扩展类加载器的父类加载器是根类加载器,只是根类加载器不是Java实现的。
下面来看看类加载器的关系:
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
void addExtURL(URL var1) {
super.addURL(var1);
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
public String findLibrary(String var1) {
var1 = System.mapLibraryName(var1);
URL[] var2 = super.getURLs();
File var3 = null;
for(int var4 = 0; var4 < var2.length; ++var4) {
File var5 = (new File(var2[var4].getPath())).getParentFile();
if (var5 != null && !var5.equals(var3)) {
String var6 = VM.getSavedProperty("os.arch");
File var7;
if (var6 != null) {
var7 = new File(new File(var5, var6), var1);
if (var7.exists()) {
return var7.getAbsolutePath();
}
}
var7 = new File(var5, var1);
if (var7.exists()) {
return var7.getAbsolutePath();
}
}
var3 = var5;
}
return null;
}
private static AccessControlContext getContext(File[] var0) throws IOException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
}
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
protected PermissionCollection getPermissions(CodeSource var1) {
PermissionCollection var2 = super.getPermissions(var1);
var2.add(new RuntimePermission("exitVM"));
return var2;
}
private void appendToClassPathForInstrumentation(String var1) {
assert Thread.holdsLock(this);
super.addURL(Launcher.getFileURL(new File(var1)));
}
private static AccessControlContext getContext(File[] var0) throws MalformedURLException {
PathPermissions var1 = new PathPermissions(var0);
ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
return var3;
}
static {
ClassLoader.registerAsParallelCapable();
}
}
AppClassLoader 类和ExtClassLoader类 都是Launcher类的静态内部类。
从上面的结果可以看出,系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例,它们俩个都是URLClassLoader类的实例。
类加载器加载Class大致要经过以下8个步骤:
-
检测此Class是否载入过(即在缓存区是否有此Class),如果有则直接进入第8步,否则接着执行第2步
-
如果父类加载器不存在(如果没有父加载器,要么父类加载器是根类加载器,要么本身就是根加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步
-
请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步
-
请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步
-
当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步
-
从文件中载入Class,成功载入后跳到第8步
-
抛出ClassNotFoundException异常
-
返回对应的java.lang.Class对象
其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。
自定义类加载器我在下了篇文章继续描述。