.Class
文件,文件须要有特定的标识(cafe babe
)。ClassLoader
只负责.Class
文件的加载,至于它是否能够运行,由执行引擎决定。.Class
文件中常量池部分的内存映射).Class
文件被解析加载到 JVM,类的对象加载到堆区,类信息被加载到方法区(java8 中方法区的实现是“元空间”)。这部分工做是类加载子系统完成的假设定义了一个类,名为HelloWorld
,运行其 Main 方法,流程如图:
html
加载(Loading)是狭义上的加载,“类加载”中的加载是广义上的。java
java.lang.Class
对象,做为方法区这个类的各类数据的访问入口。加载.class文件的方式:数据库
连接可细分为三步:验证(verify)准备(prepare)、解析(resolve)编程
static int a = 2;
final
修饰的static
,由于final
修饰的变量再也不是变量而是不可更改的常量,在编译期就已经分配内存,准备阶段会将其显示初始化。如上面的赋值语句,加上final
后,a的值就被初始化为2。final static int a = 2;
<clinit>()
的过程。static int a = 1;
或者静态代码块static {}
时才会建立并执行该方法。<clinit>()
中指令按语句在源文件中出现的顺序执行。具体表现就是一个静态变量的最后的取值决定于最后一行它的赋值语句。public class ClassInitTest { private static int num = 1; static{ num = 2; number = 20; System.out.println(num); //System.out.println(number);//报错:非法的前向引用。 } private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10 public static void main(String[] args) { System.out.println(ClassInitTest.num);//2 System.out.println(ClassInitTest.number);//10 } }
在以上代码中,num和number的最终值分别为2和10。
4. <clinit>()
不一样于咱们常说的类的构造函数。类的构造函数在虚拟机中是<init>()
,在任什么时候候都是会建立的,由于全部的类都至少有一个默认无参构造函数。
5. 若该类具备父类,JVM会保证子类的 <clinit>()
执行前,父类的 <clinit>()
已经执行完毕。
6. 虚拟机必须保证一个类的 <clinit>()
方法在多线程下被同步加锁。保证同一个类只被虚拟机加载一次(只调用一次 <clinit>()
),后续其它线程再使用类,只须要在虚拟机的缓存中获取便可。bootstrap
类加载分为启动类加载器、扩展类加载器、应用程序类加载器(系统类加载器)、自定义加载器。以下图:
api
须要注意的是,它们四者并不是子父类的继承关系。如下展现了如何获取类加载器:数组
public class ClassLoaderTest { public static void main(String[] args) { //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //获取其上层:扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d //获取其上层:获取不到引导类加载器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader);//null //对于用户自定义类来讲:默认使用系统类加载器进行加载 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);//null } }
关于几种类加载器具体的加载对象和双亲委派机制能够参考:玩命学JVM-类加载机制缓存
步骤安全
java.lang.ClassLoade
r的方式,实现本身的类加载器。loadClass()
,而是重写findClass()
。URLClassLoade
r,这样能够避免本身去编写findClass()方法以及其获取字节码流的方式,使其自定义类加载器编写更加简洁。public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if(result == null){ throw new FileNotFoundException(); }else{ return defineClass(name,result,0,result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } private byte[] getClassFromCustomPath(String name){ //从自定义路径中加载指定类:细节略 //若是指定路径的字节码文件进行了加密,则须要在此方法中进行解密操做。 return null; } public static void main(String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader(); try { Class<?> clazz = Class.forName("One",true,customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
它是一个抽象类,其后全部的类加载器都继承自ClassLoader
(不包括启动类加载器)。
API网络
getParent()
loadClass(String name)
findClass(String name)
findLoaderClass(String name)
defineClass(String name, byte[] b, int off, int len)
resolveClass(Class<?>c)
详见 http://www.javashuo.com/article/p-cloinoxk-mo.html
自定义的一个java.lang.String不能加载到的JVM中,缘由:
使用自定义的java.lang.String时,首先是应用类加载器向上委托到扩展类加载器,而后扩展类加载器向上委托给引导类加载器,引导类加载接收到类的信息,发现该类的路径时“java.lang.String”,这在引导类加载器的加载范围内,所以引导类加载器开始加载“java.lang.String”,只不过此时它加载的是jdk核心类库里的“java.lang.String”。这就是双亲委派机制中的向上委托。在完成向上委托以后,如到了引导类加载器,引导类加载器发现待加载的类不属于本身加载的类范围,就会再向下委托给扩展类加载器,让下面的加载器进行类的加载。
优点
沙箱安全机制
上文中提到的java.lang.String就是沙箱安全机制的表现,保证了对java核心源代码的保护。
首先来看一下百度百科的定义:
在计算机科学中, 字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎全部计算机编程语言都具备对基本值的字面量表示, 诸如: 整数, 浮点数以及字符串; 而有不少也对布尔类型和字符类型的值也支持字面量表示; 还有一些甚至对枚举类型的元素以及像数组, 记录和对象等复合类型的值也支持字面量表示法.
这段话不太好理解,咱们来拆解下(注意下面这段话纯属我的理解):
“字面量(literal)是用于表达源代码中一个固定值的表示法(notation)”,这里说明了两点:第一,字面量是体如今源码中的;第二,字面量是对一个固定值的表示。接下来它提到了“几乎全部计算机编程语言都具备对基本值的字面量表示”,并以一些基本数据类型、枚举、数组等数据类型举例。它们都有一个特色,就是它们的赋值是能够作到“代码可视化的”。你能够在代码中给以上提到的类型进行赋值。而咱们赋值时所给出的“值”,更准确来讲是一种“表示”(好比给数组赋值时,约定了须要用大括号括起来)就是字面量的含义。
举个例子:
int i = 1; String s = "abs"; int[] a = {1, 3, 4}; // 以上 1,“abc”,{1,3,4}均是字面量
一样,先来看一下书面定义:
符号引用:符号引用以一组符号来描述所引用的目标,符号引用能够是任何形式的字面量,只要使用时可以无歧义的定位到目标便可。好比org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,所以只能使用符号org.simple.Language(假设是这个,固然实际中是由相似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各类虚拟机实现的内存布局可能有所不一样,可是它们能接受的符号引用都是一致的,由于符号引用的字面量形式明肯定义在Java虚拟机规范的Class文件格式中。
直接引用: 直接引用能够是
(1)直接指向目标的指针(好比,指向“类型”【Class对象】、类变量、类方法的直接引用多是指向方法区的指针)
(2)相对偏移量(好比,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不一样的虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那引用的目标一定已经被加载入内存中了。
说实话看这种书面化语言抽象晦涩,下面给出一些本身的理解吧。首先找到一个.class文件(来源:玩命学JVM(一))反编译后的结果中常量池的部分:
Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // Hello World #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // Main #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Main.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 Hello World #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 Main #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V
为何只看常量池呢,由于在“解析”的定义中提到了:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。咱们来看看常量池中有什么:
常量池:常量池指的是字节码文件中的Constant pool部分。它是静态的,当编译生成字节码文件直接就不变了。常量池中包括各类字面量和对类型、域和方法的符号引用。几种在常量池内存储的数据类型包括:数量值、字符串值、类引用、字段引用、方法引用。
由此咱们能够看出,上面咱们给出的常量池中都属于“符号引用”(符号引用自己就是一种字面量)或字面量。咱们不由要问了了,那直接引用在哪呢?
我找到了《深刻理解Java虚拟机》中的一句话:
对同一个符号引用进行屡次解析请求是很常见的事情,除invokedynamic指令外,虚拟机实现能够对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动做重复进行。
关键是括号中话给了启发,说明直接引用是放在运行时常量池中的,接下来咱们看看运行时常量池的一些定义或特性。
运行时常量池是方法区的一部分。常量池用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。而加载类和接口到虚拟机后,就会建立对应的运行时常量池。
运行时常量池与常量池的不一样点在于:
1. 运行时常量池中包括了编译期就已经明确的数值字面量,也包括在运行期解析后才能得到的方法或字段引用。但,请注意,此时的方法或字段引用已经再也不是常量池中的“符号引用”,而是“直接引用”。
2. 运行时常量池具有“动态性”。
至此,关于“符号引用”和“直接引用”的解释就差很少了。最后我再多说一句,再分析的时候,我一直在疑惑“直接引用”究竟是什么,我能不能像看到常量池中的内容同样看到“直接引用”。实际上,咱们并不能拿到这样一个文件,里面整齐地写了直接引用的具体内容,由于直接引用不是所谓的“字面量”。但咱们能够回到“直接引用”的最初定义:直接引用能够是指向目标的指针、相对偏移量或是能间接定位到目标的句柄,能够想象一下在运行时,在内存中存放的直接引用大概是什么内容。
JVM 中两个Class对象是否为同一个类的必要条件
类的主动使用和被动使用
主动使用和被动使用的区别是,主动使用会致使类的初始化。
主动使用有如下七种状况:
参考文献:
https://blog.csdn.net/u011069294/article/details/107489721
http://www.javashuo.com/article/p-huylhhxc-mq.html