读书笔记-类和类加载器

类加载器阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作被放到了Java虚拟机外部去实现,以便让应用程序自己定义如何获取所需要的类,即,可在自己的代码中实现一个java.lang.ClassLoader类作为自定义的类加载器;

不同的类加载器加载上来的类并不相等,即使是来源于同一个二进制字节流的class定义;换言之,比较两个类是否相等,只有在这两个类是同一个类加载器的前提下才有意义;

如果不自定义类加载器,则会有系统应用程序类加载器来加载类,除了虚拟机中的“启动类加载器”之外,其他独立于虚拟机之外的类加载器都继承自java.lang.ClassLoader;

启动类加载器:负责<java_home>lib中的类加载,如rt.jar;

扩展类加载器:负责<java_home>libext中的类加载,或者被java.ext.dirs指定的路径中的类加载,开发者可以直接使用这个加载器;sun.misc.Launcher$ExtClassLoader来实现;

应用程序类加载器:java.lang.ClassLoader.getSystemClassLoader()返回的就是这个加载器,这也是程序中的默认类加载器,sun.misc.Launcher$AppClassLoader来实现;

双亲委派模型的意思是,除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器,这种继承关系用组合来实现,如果一个类加载器收到了类加载请求,先把这个请求委派给父类加载器去完成,最终一层层把请求传送给了顶层的启动类加载器中,当父类加载器无法加载时再自己加载;

这样就可以保证,基础类,比如java.lang.Object在程序中都被交给了启动类加载器从rt.jar中加载的同一个类;

越基础的类由越上层的加载器进行加载;

天下所有的类都继承于java.lang.Object类,不错,但是,如果不保证用同一个加载器的话,将是不同的java.lang.Object;

所以说,也可以自己定义一个类叫java.lang.Object;

注意,上面必须是java.lang.Object,而不是Object,因为即使是同一个加载器下,也可以定义一个非java.lang包下的Object类;


Class文件怎么存储、类型何时加载、如何连接、虚拟机如何执行字节码指令,这些都是虚拟机直接控制的,用户程序无法改变;

用户程序可以操控的是:字节码生成与类加载;

图是Tomcat服务器的类加载架构;

Web服务器无非是要解决以下的问题:

1,部署在一个服务器上的两个Web应用程序所使用的Java类库可以互相隔离;

2,部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享;

3,服务器要保证自身的类库不被Web应用程序影响,要保持独立;

4,支持JSP的Web服务器要支持HotSwap功能,因为JSP经常会在运行期被修改,并且要不重启服务就支持新的JSP编译而成的Class文件可以被加载上来,同包同名的class要被加载的前提就是它们使用了不同的类加载器,所以图中的JasperLoader就是用来被丢弃的,加载一次,丢弃一次;

图解:

CommonClassLoader:/common,加载的类库可以被Tomcat和所有Web应用使用;

CatalinaClassLoader:/server,Tomcat可见,Web应用不可见;

SharedClassLoader:/shared,Web应用可见,Tomcat不可见;

WebappClassLoader:/WebApp/WEB-INF,仅被当前Web应用可见,其他Web应用可Tomcat不可见;

根据双亲委派的原则,每个类都能找到最合适它的加载器;

也有上层目录的类要调用下层的类,比如/common的某个类要调用/WEB-INF下的类,双亲委派无法保证这个,因为CommClassLoader无法加载WepappClassLoader本来加载的类,这时候就用到“破坏双亲委派“;


编译器的前端:javac

前端编译器:javac,把.java转变为.class;

后端运行编译器:JIT,把字节码转变成机器码;

静态提前编译器:AOT,把*.java编译成机器码;

javac本身就是Java实现的,可在OpenJDK中找到,可load javac工程的源码到Eclipse中编译出一个javac,入口是com.sun.tools.javac.main.JavaCompiler类;

javac的过程就是:

词法、语法分析:com.sun.tools.javac.parser.Scanner和com.sun.tools.javac.parse.Parser,前者是字符转成Token,后者是挂语法树;

填充符号表:com.sun.tools.javac.comp.Enter;

注解处理器:注解可以看作是编译器的插件;

语义分析和字节码生成:检查是否符合java语法和逻辑,标注检查;数据及控制流分析;解语法糖;字节码生成;

com.sun.tools.javac.jvm.ClassWriter;

:实例构造器;默认 { }

:类构造器;默认 static { }


语法糖:对语言的功能没有影响,但是更方便程序员使用,增强程序的可读性;

1.泛型:Java中是伪泛型,编译后的字节码中,泛型已经被擦除了,用原生语法替代了;

所以一个类中:

public static void method<list list>) { … }

public static void method<list list>) { … }

这两个方法重载失败,因为参数的泛型在编译之后被擦除,是一样的;

但public static String method<list list>) { … }

public static int method<list list>) { … }

编译却通过了,并且貌似重载成功了,JVM在运行时调用了正确的方法;

而重载Overload只要求方法具备不同的签名,返回值并不是方法的特征签名(方法名称、参数顺序和参数类型)之一;但在Class文件格式中,只要描述符不完全一致的两个方法就可以共存,所以这个编译通过了;

2.自动装箱、拆箱、遍历循环、变长参数:

自动装箱、拆箱->编译后->对应的包装和还原方法(Integer.valueOf && Integer.intValue);

遍历->编译后->迭代器,所以被遍历的类要实现Iterable接口;

变成参数:数组;


类加载和对象实例化

static变量是在类加载过程中的准备阶段被在方法区上分配内存的,实例变量是对象实例化时随着对象一起分配在Java堆中的;

两个层次的概念,类可以有实例被分配到堆上,而接口和抽象类就不会被分配到堆上,因为无法实例化,但接口和抽象类仍然会被分配到方法区上,在需要加载的时候;

静态变量出现的时机早,是在类加载就出现了,而实例变量直到对象被new出来才会出现;所以静态变量也只有一份,而实例变量可以有多份;


类一定要存在.class文件中吗?

不是的,类加载器是“通过一个类的权限定名来获取定义此类的二进制字节流”,途中所示的途径都可以获取二进制字节流;

类的二进制字节流被加载存储到堆上的方法区,成为一个java.lang.Class类对象;

对加载上来的二进制字节流进行验证,就是要查看该字节流是否符合Class文件的存储格式,能否被当前版本的jvm处理等问题,这叫文件格式验证,之后还有元数据验证、字节码验证、符号引用验证;

验证并不是必须的,如果确认被反复调用和验证过的代码没问题,在实施阶段可以用-Xverify:none来关闭大部分验证措施,从而节省时间;


javap -verbose 查看字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
>javap -verbose TestClass

Compiled from "TestClass.java"

public class com.demo.jvm.TestClass extends java.lang.Object

  SourceFile: "TestClass.java"

  minor version: 0

  major version: 51

  Constant pool:

const #1 = Method       #4.#15; //  java/lang/Object."<init>":()V

const #2 = Field        #3.#16; //  com/demo/jvm/TestClass.n:I

const #3 = class        #17;    //  com/demo/jvm/TestClass

const #4 = class        #18;    //  java/lang/Object

const #5 = Asciz        n;

const #6 = Asciz        I;

const #7 = Asciz        <init>;

const #8 = Asciz        ()V;

const #9 = Asciz        Code;

const #10 = Asciz       LineNumberTable;

const #11 = Asciz       inc;

const #12 = Asciz       ()I;

const #13 = Asciz       SourceFile;

const #14 = Asciz       TestClass.java;

const #15 = NameAndType #7:#8;//  "<init>":()V

const #16 = NameAndType #5:#6;//  n:I

const #17 = Asciz       com/demo/jvm/TestClass;

const #18 = Asciz       java/lang/Object;

{

public com.demo.jvm.TestClass();

  Code:

   Stack=1, Locals=1, Args_size=1

   0:   aload_0

   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V

   4:   return

  LineNumberTable:

   line 3: 0
}

类的初始化,是jvm加载类阶段的内容,不是实例化对象的阶段,所以用来测试的代码不是用构造函数是否被调用来判断,而是用类的一个静态区域是否被执行了来判断类是否被加载了;

除此之外的所有引用类的方式,都不会触发初始化,成为被动引用:

1,通过子类引用父类的静态字段,子类不会初始化,对于静态字段,只有直接定义这个字段的类才会被初始化;

2,通过数组定义来引用类,不会触发此类的初始化,比如Demo[] demo = new Demo[10];Demo类是不会被初始化的;

3,常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;


Java中无法定义超过64KB以上的英文字符的变量或方法名,是因为class文件中的常量池部分,用来标识变量、方法名的CONSTANT_Utf8_info类型的name_index是u2长度的;

Java中,类型的加载和连接是在运行期而非编译器完成的,这就是动态扩展,这样可以使一个接口在运行期再被指定其具体的实现;


四种引用reference

JDK 1.2之后,扩种的四种references:

Strong Reference:在程序代码中普遍存在的,类似Object obj = new Object()的引用,只要强引用还在,GC永远不会回收;

Soft Reference:用来描述一些还有用,但并非必需的对象,对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出OOM。1.2之后,有专门的类SoftReference来实现软引用;

Weak Reference:同上,也是用来描述非必需对象,比软引用更弱,被弱引用关联的对象只能生存到下一次GC之前。当GC工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。1.2之后,专门的类WeakReference用来实现弱引用;

Phantom Reference:虚引用是最弱的引用,不会对对象的生存时间有影响,也无法通过虚引用来取得对象实例。虚引用的唯一目的是在这个对象被GC时收到一个系统通知。1.2之后,专门的类PhantomReference用来实现虚引用。


关于Compile Time:运行期为什么会编译?为了解决Java字节码解释执行的速度问题,虚拟机内置了运行期编译器,如果一段Java方法被多次调用,则判定为Hot方法,交给JIT编译器编译为本地代码以提高运行速度;

如果没有指定老年代的大小,则每次FULL GC都有可能扩容,反过来说,扩容就需要FUll GC一次,线程就要停顿,所以提高性能的一个方法就是把Java堆(新生区,老年代。。)的容量固定下来;并且可以-XX:+DisableExplicitGC禁止代码显示调用的System.gc();

新生区的Minor GC耗时短,但发生频率高;老年代的FullGC耗时长,但发生频率低;

除了调节堆、新生区、Eden和Survivor的比例、Old Gen、Perm Gen的大小等措施,还可以给虚拟机指定垃圾收集器来提高性能,比如用并发收集器而非串行收集器;

并发收集器的GC时间还要加上并发本身的时间才可与串行收集器的GC时间进行比较;

-XX:UserConcMarkSweepGC和-XX:UserParNewGC,指定虚拟机在新生区和老年代分别使用ParNew和CMS收集器进行GC;


虚拟机:平台无关性—>语言无关性

平台无关性:一次编写,到处运行,Write Once, Run Anywhere;字节码ByteCode代替机器码NativeCode;借助于虚拟机,将代码一一编译为与平台密切相关的二进制码不是唯一的选择了;

语言无关性:虚拟机不再关心ByteCode是由何种语言编译而来,只要符合其规范;


运行的时候NoIntitialization与ConstClass已经没有关系

编译阶段会将常量“Hello World!”存储到NoIntitialization的常量池中:

1
2
3
4
5
6
7
8
9
10
11
class ConstClass {

static {

System.out.println("Constant init!");

}

public static final String HELLOWORLD = "Hello World!";

}
原文地址:https://www.cnblogs.com/mosthink/p/5288846.html