编译机制
编译主要是把 .Java文件转换为 .class 文件。其中转换后的 .class 文件就包含了元数据,方法信息等一些信息。比如说元数据就包含了 Java 文件中声明的常量,也就是我们所说的常量池。
泛型实现原理
Java泛型实现原理:类型擦除
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。
Java中的泛型基本上都是在编译器这个层次来实现的。
在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
如在代码中定义的List<object>和List<String>等类型,在编译后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。
编译时期和运行时期类型检查
Java中的许多对象(一般都是具有父子类关系的父类对象)在运行时都会出现两种类型:编译时类型和运行时类型,例如:Person person = new Student();
这行代码将会生成一个person变量,该变量的编译时类型是Person,运行时类型是Student。
Java的引用变量有两个类型,一个是编译时类型,一个是运行时类型,编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
多态实现原理
基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可以是通过实现接口并覆盖接口中同一方法的几不同的类体现的。
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法,但是它仍然要根据继承链中方法调用的优先级来确认方法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
A继承B,加载顺序
- 若要加载类A,则先加载执行其父类B(Object)的静态变量以及静态语句块(执行先后顺序按排列的先后顺序)。
- 然后再加载执行类A的静态变量以及静态语句块。(并且1、2步骤只会执行1次)
- 若需实例化类A,则先调用其父类B的构造函数,并且在调用其父类B的构造函数前,依次先调用父类B中的非静态变量及非静态语句块.最后再调用父类B中的构造函数初始化。
- 然后再依次调用类A中的非静态变量及非静态语句块.最后调用A中的构造函数初始化。( 并且3、4步骤可以重复执行)
- 而对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。
serizeble有什么用
序列化就是把一个对象保存到一个文件或数据库字段中去,反序列化就是在适当的时候把这个文件再转化成原来的对象使用。我想最主要的作用有:
- 在进程下次启动时读取上次保存的对象的信息
- 在不同的AppDomain或进程之间传递数据
- 在分布式应用系统中传递数据
序列化
- 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
- 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
- static,transient后的变量不能被序列化;
抽象类和接口区别:
- 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类是声明方法的存在而不去实现它的类。
- 接口可以多继承,抽象类不行
- 接口定义方法,不能实现,而抽象类可以实现部分方法。
- 接口中基本数据类型为static 而抽类象不是的。
JVM虚拟机结构
JVM主要包括四个部分:
- 类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。
- 执行引擎:负责执行class文件中包含的字节码指令(执行引擎的工作机制,这里也不细说了,这里主要介绍JVM结构);
-
内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域
方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等(JDK7 永久代,JDK metaspace)。虽然JVM规范把方法区描述为堆的一个逻辑部分,但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。这部分区域不是线程所私有,而是各个线程所共享的。
java堆(Heap):存储java实例或者对象(对象和数组等实例)的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。
java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。
程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。 - 本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
双亲委派模型
工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成, 每一个层次的类加载都是如此 ,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法加载这个加载请求的时候,子加载器才会尝试自己去加载。
使用这种机制,可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次(试想如果有人编写了一个恶意的基础类,比如String类,并装载到JVM中将会引起多么可怕的后果呢。但是,由于有了全盘负责委托机制,String类 永远是有根装载器装载,这样就避免了事件的发生)。
ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。
每当 JVM 启动的时候,就会产生 三个 ClassLoader,它们分别是Bootstrap Loader, ExtClassLoader 和 AppClassLoader,ClassLoader就是用来动态加载class文件到内存当中用的。
Bootstrap Classloader启动类加载器,主要负责java_home/lib下的核心api或者-Xbootstrap选项指定的jar包装入工作。
Extension ClassLoader扩展类加载器,主要负责java_home/lib/ext下jar包。
App CLassLoader 系统类加载器,主要负责Java -classpath/所指的目录下的类与jar包的装入工作。
UserCustom ClassLoader用户自定义类加载器,在程序运行期间,通过Java.lang.Classloader的子类动态加载class。
ExtClassLoader的父类加载器是null,只不过在默认的ClassLoader 的 loadClass 方法中,当parent为null时,是交给BootStrapClassLoader来处理的,而且ExtClassLoader 没有重写默认的loadClass方法,所以,ExtClassLoader也会调用BootStrapLoader类加载器来加载,这就导致“BootStrapClassLoader具备了ExtClassLoader父类加载器的功能”。
查看classloader的源码可以发现三个重要的方法:
loadClass。classloader加载类的入口,此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从父ClassLoader中寻找,如仍然没找到,则从BootstrapClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法,如加载顺序相同,则可通过覆盖findClass来做特殊的处理,例如解密、固定路径寻找等,当通过整个寻找类的过程仍然未获取到Class对象时,则抛出ClassNotFoundException。如类需要resolve,则调用resolveClass进行链接。
findClass。它接受要加载的类作为它的参数,在该方法中会找到class文件并且读取文件中的内容到一个 byte 数组。此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
defineClass。此方法负责将二进制的字节码转换为Class对象,这个方法对于自定义加载类而言非常重要,如二进制的字节码的格式不符合JVM Class文件的格式,抛出ClassFormatError;如需要生成的类名和二进制字节码中的不同,则抛出NoClassDefFoundError;如需要加载的class是受保护的、采用不同签名的或类名是以java.开头的,则抛出SecurityException;如需加载的class在此ClassLoader中已加载,则抛出LinkageError。
导致Gc的情况:
- tenured被写满
- perm被写满
- System.gc()的显式调用。
- 上一次GC之后heap的各域分配策略动态变化。
JVM分别对新生代和旧生代采用不同的垃圾回收机制
将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)
常见检测出垃圾算法:
引用计数法
可达性分析算法
新生代的GC(Minor GC): 指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。新生代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代。
旧生代的GC(Major GC / Full GC):指发生在老年代的 GC。旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。 MajorGC 的速度一般会比 Minor GC 慢 10倍以上。Thinking in java给Java gc取了一个罗嗦的称呼:“自适应、分代的、停止-复制、标记-扫描”式的垃圾回收器。
JVM调优
从以下几个方面进行:
线程池:解决用户响应时间长的问题
连接池
JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
程序算法:改进程序逻辑算法提高性能
内存泄露:
概括地说,这就是内存托管语言中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用。
- 全局集合
- 缓存
典型的算法是:
检查结果是否在缓存中,如果在,就返回结果。
如果结果不在缓存中,就进行计算。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。
该算法的问题(或者说是潜在的内存泄漏)出在最后一步。如果调用该操作时有相当多的不同输入,就将有相当多的结果存储在缓存中。很明显这不是正确的方法。
为了预防这种具有潜在破坏性的设计,程序必须确保对于缓存所使用的内存容量有一个上限。因此,更好的算法是:
检查结果是否在缓存中,如果在,就返回结果。
如果结果不在缓存中,就进行计算。
如果缓存所占的空间过大,就移除缓存最久的结果。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用 - ClassLoader
ClassLoader的特别之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如:字段、方法和类。这意味着只要有对字段、方法、类或ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。因为ClassLoader本身可以关联许多类及其静态字段,所以就有许多内存被泄漏了。
volatile关键字怎么实现
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
如果大家有兴趣查看代码JIT生成后的汇编指令,会发现针对volatile的变量的写操作,会有一个Lock指令,这是用来实现内存屏障的,保证如果一个处理器修改了变量值,会直接将值写回到内存,其他的处理器对应的缓存也会失效,需要重新从内存中读取,这样就保证所有的处理器读到的值,都是最近的变量值。将当前处理器缓存行的数据会写回到系统内存。这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。