在Java代码中,类型的加、连接与初始化过程都是在程序运行期间完成的。其中类型指我们定义的一个class、interface、enum,此时并未包含对象。这一点提供了更大的灵活性、增加了更多的可能性。每一个类都是由类加载器class loader 加载到内存当中的。
JVM虚拟机最最本质上是一个进程,所以JVM和普通的进程一样,都是有生命周期的。Java虚拟机和程序的生命周期,在如下几种情况下,Java虚拟机的将结束生命周期:
对于上述给出的第三点给出进一步详细的例子描述,因为其十分常见,比如说我们调用一个程序其会向上抛出异常,但是直至main方法我们还是采取向上抛出异常的异常处理机制,那么此时程序就会结束运行,这是一种很常见的JVM结束生命周期的方式。
示例代码:
System.exit()
:可以看出,在此方法之后所定义的方法并没能够被有效执行,因为此时虚拟机以及被关闭了。
class GfG
{
public static void main(String[] args)
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
for (int i = 0; i < arr.length; i++)
{
if (arr[i] >= 5)
{
System.out.println("exit...");
// Terminate JVM
System.exit(0);
}
else
System.out.println("arr["+i+"] = " +
arr[i]);
}
System.out.println("End of Program");
}
}
控制台输出:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
exit...
静态变量:就是在Java代码中使用static修饰的值。
默认值:int类型的默认值为0,boolean类型的默认值为false, 引用的默认值为null。注意,如果我们使用static int a =10;
,但是在连接阶段的准备阶段,a变量的值被赋值为0;
下图和上图表达的意思实际上是一样的:
号引用/直接引用之间的区别:如果想仔细了解这两个概念的区别,不妨查看R大对此的回答R大。如果简单点说,就是JVM在加载完二进制数据之后,并未完成类的内存分配问题,这样一来我们就不能通过内存偏移量来查找方法以及变量了。符号引用(以方法为例)是一个包含类信息、方法名、方法参数的字符串,例如:java/io/PrintStream.println:(Ljava/lang/String;)V
,我们根据这个字符串就可以准确地找到相关类。但是,此实现方式速度上还是不够快,所以就出现了基于内存地址偏移量的直接引用:运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
备注:在IDEA中查询类有无被加载的方法,请移步第7.3小节。
类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.class对象(其即是被我们俗称的类对象,JVM虚拟机规范并没有说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中),类对象用来封装类在方法区内的数据结构。JVM规范也没有指定从哪里来加载Class文件。
整个过程可以使用下图表示:
类的加载的最终产品是位于内存中的Class对象(不是我们在Java代码中调用构造方法所产生的对象)。 Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内数据结构的接口。Class对象是整个反射的入口,它就好像是一面镜子一样,能够洞悉出类文件中的所有信息。
注意:这些都是可能的源头,实际上在不同的开发环境下应当会有不同的源头,而不同的源头就要求我们使用不同的字节处理流,比如说文件流:FileInputStream
有两种类型的类加载器:
默认情况下,我们自定义的类的加载器是系统类加载器。
java虚拟机自带的几种加载器: (1) 根(Bootstrap)类加载器:该类加载器没有父加载器,他负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,他并没有继承java.lang.ClassLoader类。比如说java.lang.Object就是由根类加载器加载的。 (2)扩展(Extension)类加载器:它的父类加载器为根类加载器。他从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动有扩展类加载器加载。扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。 (3) 系统(System)类加载器:也称为应用加载器,他的父类加载器为扩展类加载器。他从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。他是用户自定义的类加载器的默认父加载器。系统类加载器是纯java类,是java.lang.ClassLoader子类。
上图是类加载器的层次关系图。从表象上看这些加载器是一种继承关系,但是实际上是一种包含关系。比如说,系统类加载器加载一个类,首先会委托给扩展类加载器,后者又委托给根类加载器,如果根类加载器加载失败,那么就委托回扩展类加载器,如果还不行,那么就系统类加载器加载,最后还不行,则抛出异常。但是实际上系统类加载器包含了扩展类加载器,后者又包含了根类加载器。
上述类加载器父子(非继承中的父子关系)结构的代码证明:
public class MyTest13 {
public static void main(String[] args) {
/**
* 默认情况下,系统类加载器是用户自定义加载器的双亲,典型情况下其是应用启动的类加载器。
*
*/
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
while (null != classLoader) {
classLoader = classLoader.getParent();
System.out.println(classLoader);
}
}
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
注意:应用类加载器就是系统类加载器,启动类加载器被null表示,所以就证明了这个加载器的层次关系(但注意:这不是子类和父类关系)。
类加载完成时类初始化的必要条件,但是类被加载了不意味着其一定会被初始化。类加载器并不需要等到某个类被“首次主动使用”时再加载它。JVM规范允许类加载器再预料某个类将要被使用时就预先加载它,如果再预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须再程序首次主动使用该类时才报告错误(LinkageError链接错误)。如果一个类一直没有被程序主动使用,那么类加载器就不会报告错误(即使我们已经将类文件预先加载了)。
小总结:获得ClassLoader的途径,有:
clazz.getClassLoader();
Thread.currentThread().getContextClassLoader();
ClassLoader.getSystemClassLoader();
DriverManager.getCallerClssLoader();
两个概念(了解即可,基于3.5小节中的双亲委托机制才有这两种叫法):
下面我们给出一个使用根类加载器的一段代码:
public class MyTest7 {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz;
clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());
}
}
上述方法在控制台输出:null
,预示着这个String类由根类加载器加载。
每个类在被加载时都会对应一个类对象,而Class.forName()方法就是返回一个类对象。
getClassLoader()
是Class对象(类对象)的方法,有以下性质:
第二个代码块:
public class MyTest7 {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz;
clazz = Class.forName("classloader.C");
System.out.println(clazz.getClassLoader());
}
}
class C{
}
控制台输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
可见这是一个引用类加载器完成的类加载(其为Launcher类的内部类,且Launcher类为反编译出来的类)。
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用了父类委托机制,这种机制能够更好第保证Java平台的安全性。在此委托机制中,除了Java虚拟机自带的根加载器之外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载loader1加载Sample类时,loader1首先委托父加载器区加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。下面举一个例子:
备注:我们自定义的加载器当然也可以有所谓的父子关系:
我们想通过loader1加载器来加载Sample类,但是实际上loader1不会直接去进行加载Sample类的相关操作,而是交给系统类加载器来加载,而后者又交给扩展类加载器来完成,接着又交给了跟类加载器来尝试加载。但是实际上根类加载器并不能我们自定义的Sample类,于是加载失败,就委托给扩展类加载器加载。而扩展类加载器也会失败(这两个加载器加载Sample类失败的原因待会儿会说)。实际上一般最终会由系统类加载器来加载Sample类,而loader1并没有加载Sample类。
小节:
除了根类加载器,每个加载器被委托加载任务时,总是第一时间选择让其父类加载器来执行相关加载操作,最终总是会让根类加载器来尝试加载,如果加载失败,则再次依次返回加载,只要这个过程有一个加载器加载成功,那么这个加载任务就会执行完成(这是Oracle公司的Hotpot虚拟机默认执行的类加载机制,并且大部分虚拟机都是如此执行的),整个过程如下图所示:
上图中所含的系统自带类加载的概念可以参看3.2小节。
首先介绍自定义类的应用场景:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
package classloader;
import java.io.*;
/**
* @author Fisherman
* @date 2019/10/19
*/
public class MyTest16 extends ClassLoader {
private String classLoaderName; //定义一个类加载器的名字,既然这里是类加载器类,那么这个名字就取决于对象的设计好了
private final String fileExtension = ".class";//需要用此来进行本地文件的后缀定位
public MyTest16(String classLoaderName) {
super(); //默认无参构造器会将系统类加载器当作该类加载器的父加载器(参看此源代码既能理解这个含义)
this.classLoaderName = classLoaderName;//得到自定义加载器的名字
}
public MyTest16(ClassLoader parent, String classLoaderName) {
super(parent);//将传入的指定类加载器当作该类加载器的父加载器
this.classLoaderName = classLoaderName;//得到自定义加载器的名字
}
/**
* 重写toString方法
*
* @return 标准格式的返回类加载器对象的名字字符串
*/
@Override
public String toString() {
return "[" + this.classLoaderName;
}
/**
* 如果你仔细查看类加载过程的需要完成工作的话,你会知道这个过程需要进行类.class文件的查找
* loadClassData 方法:这是自定义的方法,用于返回对应类名的字节数组
* defineClass方法:通过字节数字以及类名(与系统无关)返回一个类对象,此方法是由JDK实现的,是一个本地方法
*
* @param className
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = this.loadClassData(className);
return defineClass(className, data, 0, data.length);
}
/**
*
* @param name 输入的本地硬盘上的类名
* @return 对应类名的.class文件的字节数组
*/
private byte[] loadClassData(String name) {
byte[] data = null;
byte[] temp = new byte[128];//设置中间操作字节数组
/**
* 这里只是相当于将类类加载器的二进制名字转为Windows系统文件路径,
* 如果是MacOS系统将"\\"替换为“.”即可,并将其赋值给className字符串变量
*/
this.classLoaderName = this.classLoaderName.replace(".", "\\");
/**
* 下面则是使用try-with-resources语句块来进行读取.class文件于字节数组中
* 只有读语句块使用了缓冲流,因为只有其涉及了硬盘读写操作
*/
try (InputStream is = new FileInputStream(name + this.fileExtension);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis = new BufferedInputStream(is);) {
int EffectiveLength = 0;
while (-1 != (EffectiveLength = bis.read(temp,0,temp.length))) {
baos.write(temp,0, EffectiveLength);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest16 loader = new MyTest16("loader1");//得到类加载器对象
test(loader);
}
/**
* 下面是一个测试方法,用于得到由类加载生成的普通对象,并在控制台上打出其类信息
* loadClass方法会调用我们MyTest14所实现的方法findClass方法
*
* 输出:classloader.MyTest1@1b6d3586
* 其中classloader为我给此类的包名而已
* @param classLoader
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> clazz = classLoader.loadClass("classloader.MyTest1");
Object object = clazz.newInstance();
System.out.println(object);
}
}
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
类的验证中的重要内容:
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如以下代码:
public class Sample{
private static int a =1;
public static long b;
public static String str;
static{
b=2;
str="hello world"
}
}
在此例的准备阶段中,我们会把上述int类型的静态变量 a 分配4个字节(32位)的内存空间,并赋值为默认值0;为long类的静态变量 b 分配8个字节(64位)的内存空间,并默认赋值为0;为String类型的静态变量 str 默认赋值为null。
在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。列如:如在Worker类的gotoWork()方法钟会引用Car类的run()方法。
public void gotoWork(){
car.run();//这段代码在worker类的二进制数据中表示为符号引用
}
在Worker类的二进制数据中,包含了一个父Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()
方法在方法区内的位置,这个指针就是一个直接引用。
可见静态变量的声明语句,以及静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。
JVM对于类初始化的判断执行逻辑:
所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化它们(要求有俩:1.首次 2.主动使用,而被动使用不会触发类的初始化),关于主动使用的概念请看第7小节。、
final关键字修饰静态变量:
public class MyTeest2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2{
public static final String str ="hello world";
static{
System.out.println("MyParent2 static block");
}
}
控制台输出:
hello world
如果将final
修饰符去掉,那么控制台输出:
MyParent2 static block
hello world
可以看到,仅仅一个final修饰就可以产生如此巨大的区别,那么究其原因是为何呢?
final
修饰的变量在Java代码的编译阶段就会被存入到调用这个变量方法所在的类的常量池当中,就这个例子来说,字符串"hello world"作为一个常量就会被存入在MyTest2类所在的常量池当中,本质上,调用方法的类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。注意:这里指的是将常量存放到了调用类MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了,甚至,我们可以将MyParent2的class文件删除!我们使用反编译的操作来验证此说法:
在windows系统中,在IDEA编译器自带的Terminal中输入:
cd build\classes\java\main\classloader
dir
javap -c MyTest2.class
dir
:此指令只是单纯输出此文件夹下有多少文档,这也可以给我们提供是否进入了正确的文件夹下的判断依据。
在MacOS系统中只需要更改前两句指令,\
换成/
以及dir
换成ls
:
cd build/classes/java/main/classloader
ls
然后Terminal会输出:
Compiled from "MyTest2.java"
public class classloader.MyTest2 {
public classloader.MyTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
首先,第一个被调用的方法是类:MyTest2
的构造方法,这个不是我们此次学习关注的重点。接下来就是main方法的分析:
getstatic
:其对应着main方法中调用的System.out
静态方法,返回一个PrintStream对象(之前也说了此JVM助记符代表的就是访问静态的成员变量);ldc
:其后面已经跟着Sting hello world 了,代表的就是此时MyParent2.str已经成为一个固定的hello world了;助记符ldc的含义:表示将int,float或者是String类型的常量值从常量池中推送至栈顶,所谓栈顶就是结下的代码马上就要用的代码。如果类型为short,那么则需要bipush了,其表示将单字节(-128-127)的常量推送至栈顶,反编译结果如下所示:
public class classloader.MyTest2 {
public classloader.MyTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 100
5: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
8: return
}
sipush
则是表示将一个短整型常量值(-32768-32767)推送至栈顶。
inconst_1
表示将int类型1推送至栈顶(-1,0,2,3,4,5都类似于此,比如inconst_3就是将整型数组3推入栈顶,但是6及以上则是使用sipush
,再大点,就使用ldc
了)
3: iconst_5
3: bipush 6
3: sipush 32767
3: ldc #4 // int 32768
再举一个例子,代码如下:
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static code.");
}
}
控制台输出:当然,每次运行的UUID都是一个不同大小的值。
MyParent3 static code.
7c380593-cfcf-466b-8aa0-2ca483b59cb3
同样是final
修饰的静态变量,这里就需要MyParent3类进行初始化操作了,可见final并未有关键的作用,而更为关键的作用是这个静态不可变变量在编译阶段能够确定取值,如果不能,则需要进行类的初始化,如果可以进行确定,那么就不需要进行类的初始化。
总结一下:当一个常量的值并非编译期间可以确定,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,自然就会导致这个类会进行初始化。
下面的代码块则是为了验证只有在第一次主动使用类的时候,才会进行初始化操作:
public class MyTest4 {
public static void main(String[] args) {
MyParent4 myParent4 = new MyParent4();
System.out.println("第二次创建对象");
MyParent4 myParent4_2 = new MyParent4();
}
}
class MyParent4{
static{
System.out.println("MyParent4 static block");
}
}
控制台输出:
MyParent4 static block
第二次创建对象
虽然我们调用了MyParent4类的构造方法两次,对应于主动调用该类两次,但是只有第一次主动使用类的时候才进行类的初始化操作。
数组例子:
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[1];
System.out.println(myParent4s.getClass());
MyParent4[][] myParent4s1 = new MyParent4[1][1];
System.out.println(myParent4s1.getClass());
System.out.println(myParent4s.getClass().getSuperclass());
System.out.println(myParent4s1.getClass().getSuperclass());
}
}
class MyParent4 {
static {
System.out.println("MyParent4 static block");
}
}
控制台输出:
class [Lclassloader.MyParent4;
class [[Lclassloader.MyParent4;
class java.lang.Object
class java.lang.Object
对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示为:class [Lclassloader.MyParent4;
这种以方括号加大写L形式。动态生成的类型,其父类就是Object类。正因为这样动态生成类型的特性所以创建数组实例并不属于主动地使用类,所以静态代码块没有得到执行。
对于数组来说,JavaDoc经常将构成数组的元素为Component,实际上就是将数组降低一个维度后的类型。
但是上面的数组是引用类型的数组,但是对于原生类型的数组又是如何呢?
int[] arr = new int[10];
System.out.println(arr.getClass());
System.out.println(arr.getClass().getSuperclass());
控制台输出:
class [I
class java.lang.Object
此时类型是[I
,父类还是Object,类似的有char类型的数组,其class类型用:[C
来表示。
我们对整个程序反编译一下,主要还是看看助记符:
Compiled from "MyTest4.java"
public class classloader.MyTest4 {
public classloader.MyTest4();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #2 // class classloader/MyParent4
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_1
9: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: iconst_1
16: iconst_1
17: multianewarray #6, 2 // class "[[Lclassloader/MyParent4;"
21: astore_2
22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
25: aload_2
26: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
29: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
32: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
35: aload_1
36: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
39: invokevirtual #7 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
42: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
45: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
48: aload_2
49: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
52: invokevirtual #7 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
55: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
58: bipush 10
60: newarray int
62: astore_3
63: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
66: aload_3
67: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
70: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
73: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
76: aload_3
77: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
80: invokevirtual #7 // Method java/lang/Class.getSuperclass:()Ljava/lang/Class;
83: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
86: return
}
助记符:
anewarray
:表示创建一个引用类型的(如类、接口、数组)数组,
newarray
:表示创建一个原始类型(int、float、char等)的数组,并将其压入栈顶
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
Java程序对类的使用方式可以分为两种:
主动使用类的七种方式,即类的初始化时机:
Class.forName("com.test.Test")
); 除了上述所讲七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,比如:调用ClassLoader类的loadClass()
方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
注意:初始化单单是上述类加载、连接、初始化过程中的第三步,被动使用并不会规定前面两个步骤被使用与否,也就是说即使被动使用只是不会引起类的初始化,但是完全可以进行类的加载以及连接。例如:调用ClassLoader类的loadClass方法加载一个类,这并不是对类的主动使用,不会导致类的初始化。
需要铭记于心的一点:只有当程序访问的静态变量或静态变量确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用,通过子类调用继承过来的静态变量算作父类的主动使用。
class文件的助记符(此处先列具一些简单且常见的助记符):
getstatic
:读取类的静态变量putstatic
:写类的静态变量invokestatic
:调用类的静态方法类的主动使用和被动使用的区别案例:
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
}
}
class MyParent1 {
public static String str = "hello world";
static {
System.out.println("MyParent static block.");
}
}
class MyChild1 extends MyParent1 {
static {
System.out.println("MyChild1 static block.");
}
}
控制台输出:
MyParent static block.
hello world
我们可以发现,子类MyChild1中的静态代码块并没有得到执行,这是为什么呢?
我相信看本文的童鞋即时没学过JVM,也知道JAVA中静态域中若涉及继承,那么父类静态域加载早于子类静态域的加载,那么这也至少说明了一点,子类的静态域也是会得到执行的,那么为何在此例中,子类的静态域没有得到执行呢(况且是在mian方法中通过子类来进行静态域的调用的)?实际上回答很简单,就是我们是被动地使用了子类,对于静态字段来说只有直接定义了该字段的类才会被初始化,当一个类在初始化时,要求其父类全部都已经初始化完毕了。
如果在子类中重写了父类的str变量,将上述代码中的相关语句修改为以下语句:
public static String str = "hello world---------by child";
那么此时就会初始化子类,控制台则会输出
MyParent static block.
MyChild1 static block.
hello world-----------by child
当然还有很多方法使子类主动地被使用,比如构造子类对象。
现在我们知道了在这个例子中如果被动地使用子类,那么子类就不会进行初始化,那么问题来了:子类有被加载和连接吗?实际上这个JVM规范对此事没有相关规定的,所以我们需要使用虚拟机的运行选项来显示是否有此类的加载:
-XX:+TraceClassLoading
:用于追踪类的加载信息并打印出来,在IntelliJ IDEA中使用此虚拟机参数,那么只需修改运行时参数即可,软件点击的顺序大概如此:
注意,如果打错了VM options,那么虚拟机则会运行失败,程序自然也无法运行,并且,编译器会告诉你:你可能所需的VM optional参数值。
再次运行程序,可以控制台输出了许许多多的关于类加载的信息,如下所示,其顺序就是JVM真实加载这些类的顺序:其他的你可能不知道,至少我们发现JVM加载的第一个类为java.lang.Object
,因为其是所有类的父类。
我们在控制台输出行向上找找,可以发现我们感兴趣的类加载过程:
可以看到实际上虽然MyChlid1作为一个子类是被动使用,没有参与初始化,但还是有被加载。特别提一点,有main方法的类都是主动使用,所以可以看到第一个红框中显示了有main方法的MyTest1类的加载。
下面简单提一下JVM参数设置的三种方式(实际运行时无须加入<>
,这里只是为了显示方便):
-XX:+<option>
:表示开启option选项
-XX:-<option>
:表示关闭option选项
-XX:<option>=<value>
:表示将option选项的值设置为value
反射是对类的主动使用的例子:
public class TempTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> testReflection = Class.forName("classloader.TestReflection");
}
}
class TestReflection {
static {
System.out.println("类:" + TestReflection.class + "被初始化了!");
}
}
控制台输出:
类:class classloader.TestReflection被初始化了!
直接上代码吧:
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}
class Singleton {
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
这就是一个单例模式,由于变量都被修饰为static
静态,为类变量,所以再调用了构造方法后,两个数都被加1了,所以控制台输出值为:
1
1
你自然会认为这是理所当然的,的确也是,毕竟代码是按照我们平时写的顺序来的,那么如果做出以下改变呢?即把public static int counter2 = 0;
移动到私有的构造方法以下呢?即如下代码,结果还是这般吗?
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(Singleton.counter1);
System.out.println(Singleton.counter2);
}
}
class Singleton {
public static int counter1;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}
public static int counter2 = 0;
public static Singleton getInstance() {
return singleton;
}
}
这里不卖关子了,直接说出答案。控制台输出:
1
0
从表面上看,counter1的值成功加1,counter2的值却没有加1,保持了不变,但是事实真的如此吗?为了检验这个说明的正确性,我们在构造方法中加入:
System.out.println("为了验证counter2有无成功加1:"+counter2);
我们发现控制台输出了:
为了验证counter2有无成功加1:1
1
0
现在可以说了,counter2是曾被成功加1的,但是后面被以某种方式修该为1,那么究竟是如何做到的呢?
实际上,这个问题用本文中JVM类对加载以及初始化整个过程掌握的好的话就能够轻松回答了,以下说一下JVM的类加载顺序和缘由:
getInstance()
,这属于类的主动使用之一,所以触发了JVM对Singleton类的初始化操作;counter1++;
以及counter2++;
操作,所以在这counter1值更新为1,counter2的值被更新为1;public static int counter2 = 0;
指定了初始值0,所以这里counter2又被更新为0;所以类就这样被加载完成了,可以发现正是由于类静态变量准备阶段赋予默认值以及类加载过程中赋予正确初始值这两个过程的按代码编写的顺序执行这一特性,这才有上述所表现的反常理。
类的实例化按序完成的作用:
Java编译器在为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为<init>
。针对源代码中每一个类的构造方法,Java编译器都产生一个<init>
方法。
根据栈的使用原则,我们可以知道:一个元素或者其值被推向栈顶,意味着其即将被使用,或者如果新加入栈的元素,其应当放在栈顶,这是遵循了后进先出的内存分配原则。
助记符 | 含义 |
---|---|
ldc | 将int/float/String类型的常量值从常量池中推送至栈顶 |
bipush | 将单字节(-128 ~ 127)的常量值从常量池中推至栈顶 |
sipush | 将一个短整型(-32768 ~ 32767)的常量值从常量池中推至栈顶 |
inconst_1 | 将int型的常量值1从常量池中推至栈顶(jvm专门为0/1/2/3/4/5这5个数字开的助记符),iconst_m1则表示的是-1 |
anwearray | 创建一个引用类型(如类、接口、数组)的数组,并将其引用值推至栈顶 |
newarray | 创建一个指定的原始类型(如int/float)的数组,并将其引用值推至栈顶 |
上述说法中所涉及的基本数据类型的内存模型如下所示:
数据类型 | 表示范围 | 2进制表示范围 |
---|---|---|
byte | -128 ~ 127 | -2^7 ~ 2^7-1 |
short | -32768 ~ 32767 | -2^15 ~ 2^15 -1 |
int | -2,147,483,648 ~ 2,147,483,647 | -2^31 ~ 2^31 -1 |
实际上就是8位计数、16位计数、32位计数方法分为三种不同的操作方法:bipush
和sipush
以及ldc
,实际上用大小为100的整型,用byte/short/int修饰最终都是使用bipush
助记符,可见最终决定助记符的不是Java代码中对变量的修饰是用int/short…,而是实际需要多少位来进行数据的存储。