读书笔记-类和类加载器
类加载器阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作被放到了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!";
}
|