4.1内存模型
(1)程序计数器:线程私有,CPU调度的基本单位,用于保证线程切换(程序能够在多线程环境下连续执行);
(2)栈(服务Java方法虚拟机栈、服务Native方法的本地方法栈):线程私有,局部变量/引用,栈深度(SOF)/无法申请内存(OOM);
(3)堆(Java代码可及的Java堆和JVM自身使用的方法区):线程共享,对象分配和回收主要区域,OOM;
4.2垃圾回收机制
(1)Stop-the-World
JVM由于要执行GC而停止了应用程序的执行称之为Stop-the-World,该情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。
(2)JAVA堆的回收
关于Java对象的回收主要考虑以下两个问题:哪些对象可以被回收、怎么回收(有哪些回收算法以及有哪些垃圾回收器)。
①判断对象是否可被回收:引用计数法(相互引用)、可达性算法(对象的引用链是否可达,GCRoots)
②垃圾回收算法:标记-清除算法(内存碎片)、复制算法(垃圾回收较为频繁、对象存活率较低的新生代)、标记-整理算法(垃圾回收不频繁、对象存活率较高的老年代)、分代收集算法。
③垃圾回收器:串行收集器(新生代、老年代)、并行收集器(新生代、老年代)、并行清除收集器(并发,新生代,追求高吞吐)、CMS收集器(并发,老年代,标记-清除算法,追求低停顿)、G1垃圾收集器(整个堆,追求低停顿)
附:
GC Roots一般包括虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象、方法区中常量引用的对象。
附:
GC Roots一般包括虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象、方法区中常量引用的对象。
分代收集算法的基本思想是:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。Minor GC 发生频率较高,不一定等 Eden区满了才触发;Major GC在老年代满时触发,对年轻代和老年代进行回收。
(3)方法区回收
①对常量池的回收。
②对类型的卸载:该类的所有实例被回收,该类的ClassLoader被回收,不存在对该类的Class对象的引用。
4.3 OOM/SOF
(1)OOM for Heap:内存泄露(GC Roots的引用链,对象的生命周期超出预期)或者内存溢出(调节JVM参数 -Xms,-Xmx 等)
(2)OOM for Stack:一般在单线程程序中不会出现;在多线程环境下,无法申请到足够的内存去创建线程
(3)SOF for Stack:程序是否有深度递归。
(4)OOM for Perm :用到像Spring等框架的时候,常常会动态地生成大量的类导致永久代不够用而导致OutOfMemoryError: PermGen Space异常(调大 -XX:MaxPermSize)
5、JVM 调优
JVM 调优的主要目标是使系统具有高吞吐、低停顿的特点,其优化手段应从两方面着手:Java虚拟机和Java应用程序。前者指根据应用程序的设计通过虚拟机参数控制虚拟机逻辑内存分区的大小以使虚拟机的内存与程序对内存的需求相得益彰;后者指优化程序算法,降低GC负担,提高GC回收成功率。以下是一些常用的JVM调优工具:
(1)Jconsole 与 Visual VM
JConsole 与 Visual VM 都是JDK自带的 Java 性能分析器,可以从命令行或在 GUI shell 中运行,从而可以轻松使用 JConsole来监控 Java 应用程序性能和跟踪 Java 中的代码,其可以从JAVA_HOME/bin/这个目录下找到。使用 Jconsole 监测死锁示例如下:
传统责任链(CoR)模式的缺点在于:具体处理角色存在着共同的实现责任链结构的行为行为,即责任链的建立和指派包含在实现角色的类中,并没有抽象出来,这直接导致责任链的指派不够灵活。因此,改进的CoR模式为:使用AOP理念将责任链结构的实现用切面抽象出来,使得各个对象只关注自身必须实现的功能性需求,准确地分离出责任链模式中不同角色的共同行为,例如,
改进后责任链(CoR)模式的应用是比较广泛的,包括 Java Web Filter(链式调用),Struts2 Interceptor(Action代理)和SpringMVC等。
7、单例模式
单例模式核心在于为整个系统提供一个唯一的实例,为整个系统提供一个全局访问点。单例模式从实现上可以分为饿汉式单例和懒汉式单例两种,前者天生就是线程安全的,后者则需要考虑线程安全性,常见的线程安全的懒汉式单例的实现有内部类式和双重检查式两种。下面给出单例模式几种常见的形式:
(1) 饿汉式单例
(1) 懒汉式单例
(1) 线程安全的懒汉式单例 —— 内部类方式
内部类方式线程安全懒汉式单例的内在原理在于:虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为在同一个类加载器下,一个类型只会被初始化一次。
(4) 线程安全的懒汉式单例——双重检查方式
8、类的生命周期及其初始化时机
类的生命周期主要包括加载、链接、初始化、使用和卸载五个阶段,如下图所示:
(1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时:注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
①使用new关键字实例化对象的时候;②读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;③调用一个类的静态方法的时候。
(2) 对类进行反射调用时:使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3) 初始化子类时:当初始化一个类时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4) 虚拟机启动时:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
(5) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
9、类加载过程中各阶段的作用
9.1加载(Loading)
(1)通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3)在内存中(对于HotSpot虚拟机而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
9.2链接(Linking)
(1)验证:验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(2)准备(Preparation):准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。
(3)解析(Resolution):解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
9.3初始化
初始化阶段是执行类构造器<clinit>()方法的过程。虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为在同一个类加载器下,一个类型只会被初始化一次。
10、对象的创建过程
在Java中,创建一个对象常常需要经历如下几个过程:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的实例构造器(成员变量和实例代码块,父类的构造函数) -> 子类的实例构造器(成员变量和实例代码块,子类的构造函数)。其中,类构造器<clinit>()由静态变量和静态语句块组成,而类的实例构造器<init>()类的实例变量/语句块以及其构造函数组成。
11、双亲委派模型
双亲委派模型很好地解决了类加载器的统一加载问题:越基础的类由越上层的加载器进行加载,进而保证Java类型体系中最基础的行为,防止应用程序变得混乱。比如,java.lang.Object 类总是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类型(是否是同一类型由类加载器与类本身共同决定)。
(1)BootStrap:引导类加载器:加载都是最基础的文(JRE/lib/rt.jar)
(2)ExtClassLoader:引导类加载器:加载都是最基础的文件(JRE/lib/ext/*.jar)
(3)AppClassLoader:ClassPath指定的所有jar或目录。
12、异常机制
Java体系中异常的组织分类如下图所示,所有异常类型的根类为 Throwable,具体包括两大类:Error 与 Exception。其中,Error是指程序无法处理的错误,表示运行应用程序中较严重问题;Exception是指程序本身可以处理的错误,具体可分为运行时异常(派生于 RuntimeException 的异常)和其他异常。
此外,从异常是否必须需要被处理的角度来看,异常又可分为不受检查异常和受检查异常两种情况:
(1)不受检查异常:派生于 Error 或 RuntimeException 的所有异常。
(2)受检查异常:除去不受检查异常的所有异常。
finally子句,在对应的try子句执行的前提下,finally 子句总会被执行。并且,finally子句 总是在诸如return、break、throw和continue等控制转移语句之前执行。
13、六大设计原则
(1)单一职责原则:高内聚,一个类只做它该做的事情;
(2)接口隔离原则:接口小而专,避免大而全;
(3)依赖倒置原则:依赖抽象而非实现,面向接口编程;
(4)里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能;
(5)开闭原则:Open for Extension, Closed for Modification,例如AOP,代理模式,适配器模式就是其经典应用;
(6)迪米特法则:高内聚,低耦合;
14、代理模式
根据代理类的创建时机和创建方式的不同,我们可以将代理模式分为静态代理和动态代理两种形式,其中,在程序运行前就已经存在的编译好的代理类是为静态代理,在程序运行期间根据需要动态的创建代理类及其实例来完成具体的功能是为动态代理。其中,代理对象的作用如下:
(1) 代理对象存在的价值主要用于拦截对真实业务对象的访问;
(2) 代理对象应该具有和目标对象(真实业务对象)相同的方法,即实现共同的接口或继承于同一个类;
(3) 代理对象应该是目标对象的增强,否则我们就没有必要使用代理了。
JDK 动态代理是动态代理模式的经典实现,主要包括三个角色对象:Subject (接口)、被代理的类以及InvocationHandler接口(一般持有被代理对象),例如:
(1)实现 InvocationHandler 接口
(2)Proxy.newProxyInstance
但是,JDK动态代理只能完成对接口的代理,而不能完成对类的代理,关键原因为:Java只允许单继承。具体地,代理对象proxySubject的类型为“com.sun.proxy.$Proxy0”,这恰好印证了proxySubject对象是一个代理对象。除此之外,我们还发现代理对象proxySubject所对应的类继承自java.lang.reflect.Proxy类,这也正是JDK动态代理机制无法实现对class的动态代理的原因。
15、迭代器模式
迭代器模式是与集合共生共死。一般来说,我们实现一个容器,就需要同时提供这个容器的迭代器,使用迭代器的好处:封装容器内部的实现细节,对于不同的集合,可以提供统一的遍历方式,简化客户端的访问和获取容器内数据。
特别需要注意的是,在迭代器模式中,具体迭代器角色和具体容器角色是耦合在一起的 —— 遍历算法是与容器的内部细节紧密相关的。为了使客户程序从与具体迭代器角色耦合的困境中脱离出来,避免具体迭代器角色的更换给客户程序带来的修改,迭代器模式抽象了具体迭代器角色,使得客户程序更具一般性和重用性,这被称为多态迭代。
在 Java Collection FrameWork中,提供的具体迭代器角色是定义在容器角色中的内部类,这样便保护了容器的封装。但是,同时容器也提供了遍历算法接口,并且你可以扩展自己的迭代器。大家考虑一个问题,为什么一定要去实现 Iterable 这个接口呢? 为什么不直接实现 Iterator接口呢?
看一下 JDK 中的集合类,比如 List一族或者Set一族,都是实现了 Iterable 接口,但并不直接实现 Iterator 接口。仔细想一下这么做是有道理的:因为 Iterator接口的核心方法 next() 或者 hasNext() 是依赖于迭代器的当前迭代位置的。若 Collection 直接实现 Iterator 接口,势必导致集合对象中包含当前迭代位置的数据(指针)。当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么 next() 方法的结果会变成不可预知。除非再为 Iterator接口 添加一个 reset() 方法,用来重置当前迭代位置。但即使这样,Collection 也只能同时存在一个当前迭代位置(不能同时多次迭代同一个序列:必须要等到当前次迭代完成并reset后,才能再一次从头迭代)。 而选择实现 Iterable 接口则不然,每次调用都会返回一个从头开始计数的迭代器(Iterator),因此,多个迭代器间是互不干扰的。
16、适配器模式
适配器模式将一个类的接口转换成客户期望的另一个接口,让原本不兼容的接口可以合作无间。也就是说,适配器模式用于实现新、老接口之间的转换与适配,其魅力在于:不改变原有接口,却还能使用新接口的功能。