• 读书笔记-类和类加载器


    类加载器阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作被放到了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!";
    
    }
    
  • 相关阅读:
    报错:require_once cannot allocate memory----php,以前自己弄的稍微有点特殊的开发环境
    wget下载整个网站---比较实用--比如抓取Smarty的document
    Linux必须会的命令---也是以前记录的,ctrl+z fg 啥的 jobs 比较实用
    最全的ORACLE-SQL笔记
    UML十四图打油诗记忆法
    Spring整合Hibernate总结
    MyEclipse6.5优化
    windows XP 各个文件夹详细介绍
    hibernate所需jar包
    struts2所需的jar包
  • 原文地址:https://www.cnblogs.com/mosthink/p/5288846.html
Copyright © 2020-2023  润新知