目录
10.1 "String s = new String(“xyz”);"创建了几个字符串对象?
10.2 Student s=new Student();"在内存中创建对象的过程?
12.3.5 哪些 ClassLoader 负责加载上面几类 Class?
1.java的八种基本数据类型
boolean(1字节,false)byte(1字节,0) short(两字节,0)char(2个字节,'\u0000 null')('\u0020'空格,UNIDode字符) int(4字节) long (8个字节)float(4字节 0.0f)double(8个字节0.0d)
2.接口:
[public] interface interface_name [extends interface1_name[, interface2_name,…]] {
// 接口体,其中可以包含定义常量和声明方法
[public] [static] [final] type constant_name = value; // 定义常量
[public] [abstract] returnType method_name(parameter_list); // 声明方法
}
3.重写:
static,final,构造函数不可以被重写;重载一般是在一个类中。
4.==和equals的区别:
前者运算符,后者函数。
前者可以比较基本数据类型的值,后者不可以;
前者和后者都可以比较对象的值即引用地址,但是后者可以被重写,例如String重写了hashcode和equals方法;
5.JMM内存模型
首先声明JMM和内存区域不是一回事。
大体:JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,JMM是java内存模型,JMM定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节,并提供了内置解决方案(happen-before原则)及其在外部可应用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中应有的原子性、可视性及其有序性。
JMM规定了所有的变量都存储在主内存中。每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般,实际上是强制将副本刷新到主内存)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要主内存来完成。
5.1 JMM中的happen-before规则
happen-before规则内容如下:
程序顺序原则:在一个线程内必须保存语义串行性,也就是说按照代码顺序执行。
锁规则:解锁操作必然发生在后续的同一个锁的加锁之前。
volatile的强制刷新读取主存最新写入主存原则;
线程启动原则:start()方法必须是最先的,即如果在线程A在B进行start()之前就修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
传递性规则;
线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作 用是等待当前执行的线程终止。假设在线程 B 终止之前,修改了共享变量,线 程 A 从线程 B 的 join 方法成功返回后,线程 B 对共享变量的修改将对线程 A 可见。
线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测线程是否中断。
对象终结规则:对象的构造函数执行,结束先于 finalize()方法。
6.java内存分区:
6.1 内存分区
6.1.1 运行时数据区:
(1)线程共享区域:方法区,堆;(2)线程私有区域:虚拟机栈,本地方法栈,程序计数器;
6.1.2 本地方法库,本地库接口,执行引擎;
6.2 运行时数据区:
6.2.1堆:
存放对象的实例,数组,虚拟机启动时创建初始堆,可以由用户申请分配内存,可以扩建,内存不足以扩建时发生outofmemory错误,逻辑上连续,是用链表存储空闲地址,遍历方向自底向上,
- -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
- -Xmn — 堆中年轻代的大小
- -XX:-DisableExplicitGC — 让 System.gc()不产生任何作用
- -XX:+PrintGCDetails — 打印 GC 的细节
-
-XX:+PrintGCDateStamps — 打印 GC 操作的时间戳
-
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-
-XX:NewRatio — 可以设置老生代和新生代的比例
-
-XX:PrintTenuringDistribution — 设置每次新生代 GC 后输出幸存者乐园中对象年龄的分布
-
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
6.2.2 方法区(永久代):
存放虚拟机加载的类信息,常量(String等),静态变量,(即时编译器编译后的代码),会发生内存溢出错误;
(1)运行时常量池
方法区的一部分:存放编译期生成的字面量和符号引用;也会出现内存溢出错误;
6.2.3 虚拟机栈:
存储局部变量表,函数返回地址,基本数据类型,对象引用。虚拟机栈自动扩展内存不够时出现内存溢出错误,线程请求栈深度大于虚拟机所允许深度会发生栈溢出错误。
6.2.4 本地方法栈:
与虚拟机栈类似,区别在于为本地方法服务;
6.2.5 程序计数器:
记录正在执行的虚拟机字节码指令地址;
执行本地方法是计数器为空(undefined);
线程私有,唯一一个无内存溢出错误的区域;
7.Java的GC什么时候回收垃圾?
7.1回收什么对象?
虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的引用;
7.2 如何判断一个对象已经死去?
(1)被废弃的答案是:给一个对象添加引用计数器,因为会出现循环引用问题,计数器的值永远不会为0,因此造成内存泄漏。
(2)正确答案:对象得可达性分析(GC Roots树);从树根开始向下搜索,搜索所经过的路径成为引用链,当一个对象不再任何引用链上时,就称对象是不可达的,即没有被引用的;
对象是否存活和引用有关:强引用,除非引用取消;软引用:描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象引入回收范围进行二次回收。
7.3 GC算法
7.3.1 标记-清除方法:
标记要回收对象,标记完成后统一回收所有被标记对象,缺点是标记和清除效率低,而且清除后会产生大量的不连续的内存碎片,导致分配较大对象时无法找到足够的连续内存空间,而提前触发另一次垃圾回收。
7.3.2 复制算法:
将内存分为大小相等的两块,每次只用其中一块,一块内存用完之后,将其中不需要回收的对象复制到另一块内存区域,然后将原来的半块内存区域全部回收。
实现简单,效率高,但是将内存缩小为原来的一半,代价高。
大多数对象存活时间很短,不需要1:1划分两块区域。
一块较大的Eden和2块较小的Survivor空间,每次使用eden和2块较小的Survivor空间,每次使用Eden和一块幸存区之后将存活的对象复制到另一块幸存区中。
7.3.3 标记整理算法:
标记需要回收对象,将存活对象移动到一端,然后将端边界以外的内存回收。
7.3.4 分代收集算法:
将堆分为新生代和老生代,
新生代对象存活率低,用标记-复制算法,新生代中又分为了 eden 区和 survivor 区,survivor 区又分成了 S0 和 S1, 或则是 from 和 to,其中 eden,from 和 to 的内存大小默认是 8:1:1;
老年代对象存活率高,用标记整理算法。
7.4 GC是什么,为什么要有GC
GC代表垃圾收集,是java自动检测对象是否超过作用域从而达到自动回收内存的目的。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或 Runtime.getRuntime().gc() ,但 JVM 可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通 常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死 亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回 收器对某个对象或所有对象进行垃圾回收。
在 Java 诞生初期,垃圾回收是 Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过 境迁,如今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得 iOS 的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就在于 Android 系统中垃圾回收的不可预知性。
7.5 如何减少GC的次数?
- 1.对象不用时最好显示置为 NULL 一般而言,为 NULL 的对象都会被作为垃圾处理,所以将不用的对象置为 NULL,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。
- 2.尽量少使用 System,gc() 此函数建议 JVM 进行主 GC,会增加主 GC 的频率,增加了间接性停顿的次数。
- 3.尽量少使用静态变量
- 静态变量属于全局变量,不会被 GC 回收,他们会一直占用内存
- 4.尽量使用 StringBuffer,而不使用 String 来累加字符串
- 5.分散对象创建或删除的时间
- 集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存, JVM 在这种 情况下只能进行主 GC 以回收内存,从而增加主 GC 的频率。
- 6.尽量少用 finaliza 函数
- 它会加大 GC 的工作量。
- 7.如果有需要使用经常用到的图片,可以使用软引用类型,将图片保存在内存中, 而不引起outofmemory
- 8.能用基本类型入 INT 就不用对象 Integer
- 9.增大-Xmx 的值
7.6 内存溢出问题(常考,待补充)
如果我们一个项目,理论上需要 1.5G 的内存就足够,但是项目上线后发现隔了几个星期,占用内存到了 2.5G,这时候你会考虑是什么问题?怎么解决? 可能造成内存泄漏的原因有哪些?检查内存泄漏的工具有哪些?你平时是怎么检查内存泄 漏的?
jvm 多态原理。invokestatic invokeinterface 等指令。常量池中的符号引用找到直接引用。在堆中找到实例对象,获取到偏移量,由偏移量在方法表中指出调用的具体方法。接口是 在方法表中进行扫描)等等扯了半天。
7.7 Java存在内存泄露问题吗?
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无 用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如 Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露:
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾 回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过 期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的, 这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起 来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象, 即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外, 从而对性能造成重大影响,极端情况下会引发 Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成 OutOfMemoryError。
8.JDK,JRE,JVM
8.1概念区分
JDK:java开发工具包;最大;
JRE:java运行时环境;其次大;
JVM:java虚拟机;最小;
JDK包含JRE包含JVM;
8.2 JVM参数:
- -Xmx3550m:设置 JVM 最大堆内存为 3550M。
- -Xms3550m:设置 JVM 初始堆内存为 3550M。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。
- -Xss128k:设置每个线程的栈大小。JDK5.0 以后每个线程栈大小为 1M,之前每个线程栈大小为 256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成, 经验值在 3000~5000 左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
- -Xmn2g:设置年轻代大小为 2G。在整个堆内存大小确定的情况下,增大年轻代将会减小 年老代,反之亦然。此值关系到 JVM 垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的 3/8。
- -XX:NewSize=1024m:设置年轻代初始值为 1024M。
- -XX:MaxNewSize=1024m:设置年轻代最大值为 1024M。
- -XX:PermSize=256m:设置持久代初始值为 256M。
- -XX:MaxPermSize=256m:设置持久代最大值为 256M。
- -XX:NewRatio=4:设置年轻代(包括 1 个 Eden 和 2 个 Survivor 区)与年老代的比值。表 示年轻代比年老代为 1:4。
- -XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的比值。表示 2 个 Survivor 区 (JVM 堆内存年轻代中默认有 2 个大小相等的 Survivor 区)与 1 个 Eden 区的比值为 2:4, 即 1 个 Survivor 区占整个年轻代大小的 1/6。
- -XX:MaxTenuringThreshold=7:表示一个对象如果在 Survivor 区(救助空间)移动了 7 次 还没有被垃圾回收就进入年老代。如果设置为 0 的话,则年轻代对象不经过 Survivor 区, 直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为 一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象在年轻代存 活时间,增加对象在年轻代被垃圾回收的概率,减少 Full GC 的频率,这样做可以在某种程 度上提高服务稳定性。 -XX:PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
- -XX:MaxTenuringThreshold 每次 minorGC 就增加一次,超过这个值,在 from 中的对象直 接进入到老年代
9.用final的好处:
- final 变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用 final 关键字,JVM 会对方法、变量及类进行优化。
- final 关键字提高了性能。JVM 和 Java 应用都会缓存 final 变量。
10.String相关内容
10.1 "String s = new String(“xyz”);"创建了几个字符串对象?
答: 两个对象,一个是静态区的”xyz”,一个是用 new 创建在堆上的对象。前提是运行时常量区即前述静态区没有这个字符串对象。
10.2 Student s=new Student();"在内存中创建对象的过程?
- 加载Student.class文件进入内存;
- 在栈内存为s开辟空间;
- 在堆内存中为学生对象开辟空间;
- 对学生对象的成员变量进行默认初始化;
- 对学生对象的成员变量进行显示初始化;
- 通过构造方法对学生对象的成员变量赋值;
- 学生对象初始化完毕,把对象地址赋值给s变量;
11. Java四个基本特征以及Java实现多态的机制?
Java四个基本特征:封装、继承、多态、抽象;
多态靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动 态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
12.java的类加载
12.1 java的类加载器有哪些?
根类加载器(C++看不到源码);扩展类加载器(jre\lib\ext中);系统(应用)类加载器(classpath中);自定义加载器(必须继承classloader);
12.2 类初始化的时间
1)创建类的实例,new一个对象;
2)访问某个类或者接口的静态变量,或者对该静态变量进行赋值;
3)调用类的静态方法
4)反射(Class.forName("com.lyj.load"));
5)初始化一个类的子类(会首先初始化子类的父类);
6)JVM的启动类启动;
12.3 类加载分析
Java 虚拟机一般使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),然后类加载器会读取这个 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。
ClassLoader 是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不同来源的 Class 文件。
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
12.3.1 加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对象实例存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
12.3.2链接
链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
1)、验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
- 格式验证:验证是否符合class文件规范
- 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
- 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
2)、准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内);
被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值。
3)、解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;
静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)。
12.3.3 初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由JVM调用。
如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
初始化的具体顺序:
public class 类初始化测试 {
public static void main(String[] args){
//通过调用静态变量进行类加载;
//System.out.println(Son.c);
//对象调用实例化;
Son p=new Son(3);
/*
D:\Java\jdk\bin\java.exe "-javaagent:D:\install\x64\JetBrains\IntelliJ IDEA 2020.1\lib\idea_rt.jar=2178:D:\install\x64\JetBrains\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jdk\jre\lib\charsets.jar;D:\Java\jdk\jre\lib\deploy.jar;D:\Java\jdk\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk\jre\lib\ext\cldrdata.jar;D:\Java\jdk\jre\lib\ext\dnsns.jar;D:\Java\jdk\jre\lib\ext\jaccess.jar;D:\Java\jdk\jre\lib\ext\jfxrt.jar;D:\Java\jdk\jre\lib\ext\localedata.jar;D:\Java\jdk\jre\lib\ext\nashorn.jar;D:\Java\jdk\jre\lib\ext\sunec.jar;D:\Java\jdk\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk\jre\lib\ext\sunmscapi.jar;D:\Java\jdk\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk\jre\lib\ext\zipfs.jar;D:\Java\jdk\jre\lib\javaws.jar;D:\Java\jdk\jre\lib\jce.jar;D:\Java\jdk\jre\lib\jfr.jar;D:\Java\jdk\jre\lib\jfxswt.jar;D:\Java\jdk\jre\lib\jsse.jar;D:\Java\jdk\jre\lib\management-agent.jar;D:\Java\jdk\jre\lib\plugin.jar;D:\Java\jdk\jre\lib\resources.jar;D:\Java\jdk\jre\lib\rt.jar;C:\Users\admin\IdeaProjects\T01_TestObject\target\classes;C:\Users\admin\.m2\repository\org\projectlombok\lombok\1.18.12\lombok-1.18.12.jar;C:\Users\admin\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;C:\Users\admin\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\admin\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\admin\.m2\repository\com\alibaba\fastjson\1.2.73\fastjson-1.2.73.jar;C:\Users\admin\.m2\repository\org\openjdk\jol\jol-core\0.11\jol-core-0.11.jar com.javabasis.面试.类加载测试
a=2
c=2
b=2
Parent():3
Parent(int b):3
d=2.0
ef:3
*/
}
}
//调用顺序:
//类初始化时期:父类静态变量,父类静态代码块,子类静态变量,子类静态代码块;
//实例化后:父类非静态代码块,父类构造函数,子类非静态代码块,子类构造函数;
//上述父类构造函数必定会执行的,必定执行父类();
//也可以在子类构造函数中通过super(参数)添加父类执行的构造函数;
class Parent{
static int a=1;
static{
a++;
System.out.println("a="+a);
}
int b=1;
{
b++;
System.out.println("b="+b);
}
public Parent(){
a++;
System.out.println("Parent():"+a);
}
public Parent(int b){
this();
this.b=b;
System.out.println("Parent(int b):"+b);
}
}
class Son extends Parent{
static int c=1;
double d=1;
int ef=1;
static {
c++;
System.out.println("c="+c);
}
{
d++;
System.out.println("d="+d);
}
public Son(){
d++;
System.out.println("Son():"+d);
}
public Son(int ef){
super(ef);
this.ef=ef;
System.out.println("ef:"+ef);
}
public Son(double d){
//super(d);编译报错;
this.d=d;
System.out.println("Son(double d):"+d);
}
}
12.3.4 Class 文件有哪些来源呢?
上文提到了 ClassLoader 可以去加载多种来源的 Class,那么具体有哪些来源呢?
首先,最常见的是开发者在应用程序中编写的类,这些类位于项目目录下;
然后,有 Java 内部自带的核心类如 java.lang、java.math、java.io 等 package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里;
另外,还有 Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下。开发者也可以把自己编写的类打包成 jar 文件放入该目录下;
最后还有一种,是动态加载远程的 .class 文件。
既然有这么多种类的来源,那么在 Java 里,是由某一个具体的 ClassLoader 来统一加载呢?还是由多个 ClassLoader 来协作加载呢?
12.3.5 哪些 ClassLoader 负责加载上面几类 Class?
实际上,针对上面四种来源的类,分别有不同的加载器负责加载。
首先,我们来看级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader 加载器负责加载,它也被称作 根加载器/引导加载器。注意,BootstrapClassLoader 比较特殊,它不继承 ClassLoader,而是由 JVM 内部实现;
然后,需要加载 Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的 jar 文件。这些文件由 ExtensionClassLoader 负责加载,它也被称作 扩展类加载器。当然,用户如果把自己开发的 jar 文件放在这个目录,也会被 ExtClassLoader 加载;
接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader 加载器进行加载,它也被称作系统类加载器 System ClassLoader;
最后,如果想远程加载如(本地文件/网络下载)的方式,则必须要自己自定义一个 ClassLoader,复写其中的 findClass() 方法才能得以实现。
因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的 Class。
12.3.6 不同加载器是如何工作的?什么是双亲委托模型及双亲委托存在的意义?
String 类是 Java 自带的最常用的一个类,现在的问题是,JVM 将以何种方式把 String class 加载进来呢?
我们来猜想下。
首先,String 类属于 Java 核心类,位于 $JAVA_HOME/jre/lib 目录下。有的朋友会马上反应过来,上文中提过了,该目录下的类会由 BootstrapClassLoader 进行加载。没错,它确实是由 BootstrapClassLoader 进行加载。但,这种回答的前提是你已经知道了 String 在 $JAVA_HOME/jre/lib 目录下。
那么,如果你并不知道 String 类究竟位于哪呢?或者我希望你去加载一个 unknown 的类呢?
有的朋友这时会说,那很简单,只要去遍历一遍所有的类,看看这个 unknown 的类位于哪里,然后再用对应的加载器去加载。
是的,思路很正确。那应该如何去遍历呢?
比如,可以先遍历用户自己写的类,如果找到了就用 AppClassLoader 去加载;否则去遍历 Java 核心类目录,找到了就用 BootstrapClassLoader 去加载,否则就去遍历 Java 扩展类库,依次类推。
这种思路方向是正确的,不过存在一个漏洞。
假如开发者自己伪造了一个 java.lang.String 类,即在项目中创建一个包java.lang,包内创建一个名为 String 的类,这完全可以做到。那如果利用上面的遍历方法,是不是这个项目中用到的 String 不是都变成了这个伪造的 java.lang.String 类吗?如何解决这个问题呢?
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
类加载器的应用:自定义类加载器
自定义类加载器,它允许我们在运行时可以从本地磁盘或网络上动态加载自定义类。这使得开发者可以动态修复某些有问题的类,热更新代码。
自定义类加载器需要继承抽象类ClassLoader,实现findClass方法,该方法会在loadClass调用的时候被调用,findClass默认会抛出异常。不是loadClass()方法,因为ClassLoader提供了loadClass()(如上面的源码),它会基于双亲委托机制去搜索某个 class,直到搜索不到才会调用自身的findClass(),如果直接复写loadClass(),那还要实现双亲委托机制
findClass方法表示根据类名查找类对象
loadClass方法表示根据类名进行双亲委托模型进行类加载并返回类对象
defineClass方法表示跟根据类的字节码转换为类对象
13. java反射
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任何一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法称为java的反射机制。
获取class对象的方法:(1)this.getClass();(2)Class.forName("类名");(3)String.class;
待续;
14.static和final的区别
static:
修饰变量:静态变量随着类加载时被完成初始化,内存只有一个,且JVM只会为它分配一次内存,所有类共享静态变量。
修饰方法:在类加载的时候就存在,不依赖任何实例,static必须实现,不能用abstract修饰;
修饰代码块:在类加载完后就会执行代码块中的内容;
代码块执行顺序如前述;
final:
修饰变量:编译器常量:类加载的时候完成初始化,编译后带入到任何计算式中,只能是基本类型。
运行时常量:基本数据类型或者引用数据类型。引用不可变,但是引用的对象的内容可变。
修饰方法:不能被继承,不能被子类修改。
修饰类:不能被继承;
修饰形参:final形参不可变;
final的好处:
- final关键字提高了性能。JVM和java应用都会缓存final变量;
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销;
- 用final关键字,JVM会对方法、变量以及类进行优化;
15.设计模式相关内容
15.1 设计模式原则
15.1.1 里氏替换原则
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的已实现的方法。
根据上述理解,对里氏替换原则的定义可以总结如下:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类中可以增加自己特有的方法;
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松;当子类的方法重载父类的方法时,方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等;
通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。 如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
15.1.2 开闭原则
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
15.1.3 单一职责原则
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高类的内聚性。
如果遵循单一职责原则将有以下优点。 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。 提高类的可读性。复杂性降低,自然其可读性会提高。 提高系统的可维护性。可读性提高,那自然更容易维护了。 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
单一职责原则的实现方法:单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。 http://c.biancheng.net/view/1327.html
注意:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
15.1.4 接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
2002 年罗伯特·C.马丁给“接口隔离原则”的定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。
该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。
以上两个定义的含义是:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
15.1.5 依赖倒置原则
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
15.2 设计模式分类
所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经 过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计 和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
设计模式共23种,分为以下三类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
15.3 常见设计模式
15.3.1 工厂方法模式
public interface FactorySender {
public void send();
}
class MailSender implements FactorySender{
@Override
public void send() {
System.out.println("this is a mail sender!");
}
}
class SmsSender implements FactorySender{
@Override
public void send() {
System.out.println("this is a sms sender");
}
}
class SendFactory{
public FactorySender produce(String type){
if("mail".compareTo(type)==0){
return new MailSender();
}else if("sms".compareTo(type)==0){
return new SmsSender();
}else{
System.out.println("请输入正确的类型!");
return null;
}
}
}
public class usualFactory {
public static void main(String[] args){
SendFactory sendFactory=new SendFactory();
FactorySender sender=sendFactory.produce("sms");
sender.send();
}
}
public interface FactorySender {
public void send();
}
public class MultiFactory {
public static void main(String[] args){
MultiSendFactory multiFactory=new MultiSendFactory();
FactorySender factorySender1=multiFactory.produceMail();
FactorySender factorySender2=multiFactory.produceSms();
factorySender2.send();
}
}
//为了预防字符串传递错误的情况;
class MultiSendFactory{
public FactorySender produceMail(){
return new MailSender();
}
public FactorySender produceSms(){
return new SmsSender();
}
}
15.3.2 抽象工厂方法模式
public interface AbstractFactorySender {
public void send();//可以有多种行为,不只send();
}
public interface AbstractRoleFactory {
public AbstractFactorySender produce();
}
//类的创建依赖工厂类,也就是说如果要脱产程序,必须对工厂类进行修改,这违背了闭包原则。
class AbstractMailSender implements AbstractFactorySender{
@Override
public void send() {
System.out.println("Abstract Mail Sender");
}
}
class AbstractSmsSenderFactory implements AbstractRoleFactory{
@Override
public AbstractFactorySender produce() {
return new AbstractSmsSender();
}
}
class AbstractSmsSender implements AbstractFactorySender{
@Override
public void send() {
System.out.println("Abstract Sms Sender");
}
}
class AbstractMailSenderFactory implements AbstractRoleFactory {
@Override
public AbstractFactorySender produce() {
return new AbstractMailSender();
}
}
public class AbstractFactory {
public static void main(String[] args){
AbstractRoleFactory provider=new AbstractMailSenderFactory();
AbstractFactorySender sender=provider.produce();
sender.send();
}
}
15.3.3 单例模式
public class 单例模式 {
}
//懒汉式单例;
class Singleton1 {
private static volatile Singleton1 singleton=null;
/**
* 构造函数私有,禁止外部实例化
* DCL双重检查;
*/
private Singleton1() {};
public static Singleton1 getInstance() {
if (singleton == null) {
synchronized (Singleton1.class) {
if (singleton == null) {
singleton = new Singleton1();
}
}
}
return singleton;
}
}
//饿汉式单例;本身是线程安全的,不需要像懒汉式单例一样考虑线程安全问题;
class Singleton2{
private static final Singleton2 singleton=new Singleton2();
private Singleton2(){}
public static Singleton2 getInstance(){
return singleton;
}
}
15.3.4 建造者模式
public class Product{
String ProductName;
public Product(String name){
this.ProductName=name;
}
public String getProductName() {
return ProductName;
}
}
public interface Producer {
public void makeProduct();
}
public interface ProducerFactory {
public Producer getProducer();
}
public class Producer1 implements Producer{
private String productName="Product1";
@Override
public void makeProduct() {
Product product=new Product(productName);
System.out.println(product.getProductName());
}
}
public class Producer2 implements Producer{
private String productName="Product2";
@Override
public void makeProduct() {
Product product=new Product(productName);
System.out.println(product.getProductName());
}
}
public class Producer1Factory implements ProducerFactory{
@Override
public Producer getProducer() {
return new Producer1();
}
}
public class Producer2Factory implements ProducerFactory{
@Override
public Producer getProducer() {
return new Producer2();
}
}
15.3.5 适配器模式
public class AdapterSchema {
/*
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,
目的是消除由于接口不匹配所造成的类的兼容性问题。
主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
*/
}
public interface TargetTable {
public void method1();
public void method2();
}
class Source{
public void method1(){
System.out.println("this is an original method");
}
}
class Adapter1 extends Source implements TargetTable{
@Override
public void method2() {
System.out.println("this is the targetable method");
}
}
public class ClassAdapterSchema {
public static void main(String[] args){
//目标是在满足开闭原则的情况下,
// 将TargetTable里的函数和Source里的函数融合到一起;
//因此选用extends Source 继承新的接口的方式;
//但是TargetTable的新的函数可能不同适配器实现不同,
//所以出现了不同的适配器;
//其中TargetTable可以被理解为新类,Source是原类;
TargetTable targetTable=new Adapter1();
targetTable.method1();
targetTable.method2();
}
}
15.3.6 装饰器模式
public interface Sourceable {
public void method();
}
/*
顾名思义,
装饰模式就是给一个对象增加一些新的功能,而且是动态的,
要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
*/
public class Source implements Sourceable{
@Override
public void method() {
System.out.println("the original method!");
}
}
class Decorator implements Sourceable{
private Sourceable source;
//接口对象可以被声明,但是不可以被用接口实例化;
public Decorator(Sourceable source){
this.source=source;
}
@Override
public void method() {
System.out.println("before decorator");
source.method();
System.out.println("after decorator");
}
}
public class DecoratorSchema {
public static void main(String[]args){
Sourceable source=new Source();
Sourceable obj=new Decorator(source);
obj.method();
}
}
15.3.7 代理模式
/*写一个ArrayList的动态代理类*/
final List<String> list = new ArrayList<String>();
List<String> proxyInstance = (List<String>)Proxy.newProxyInstance(
list.getClass().getClassLoader(),
list.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return method.invoke(list, args);
}
});
proxyInstance.add("你好");
System.out.println(list);
/*静态代理代码*/
/* 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.
这里使用到编程中的一个思想(开闭原则):不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。
举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子。*/
/*代理模式的关键点是:代理对象与目标对象。代理对象是对目标对象的扩展,并会调用目标对象。*/
/**
* 接口
*/
public interface IUserDao {
void save();
}
/**
* 接口实现
* 目标对象
*/
public class UserDao implements IUserDao {
public void save() {
System.out.println("----已经保存数据!----");
}
}
/**
* 代理对象,静态代理
*/
public class UserDaoProxy implements IUserDao{
//接收保存目标对象
private IUserDao target;
public UserDaoProxy(IUserDao target){
this.target=target;
}
public void save() {
System.out.println("开始事务...");
target.save();//执行目标对象的方法
System.out.println("提交事务...");
}
}
静态代理(装饰器模式)总结:
可以做到在不修改目标对象的功能前提下,对目标功能扩展。
缺点:
- 因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护。
- 且事先如果不知道要代理什么,就无法确定共同接口是什么。
如何解决静态代理中的缺点呢?答案是可以使用动态代理方式。
动态代理有以下特点:
- 代理对象,不需要事先实现接口,直接编写代理类;
- 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
- 动态代理也叫做:JDK代理,接口代理;
JDK中生成代理对象的API
代理类所在包:java.lang.reflect.Proxy;
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:
ClassLoader loader,
:指定当前目标对象使用类加载器,获取加载器的方法是固定的;Class<?>[] interfaces,
:目标对象实现的接口的类型,使用泛型方式确认类型;InvocationHandler h
:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入;
public class ProxyFactory{
//维护一个目标对象
private Object target;
public ProxyFactory(Object target){
this.target=target;
}
//给目标对象生成代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务2");
//执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务2");
return returnValue;
}
}
);
}
}
/**
* 测试类
*/
public class App {
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDao();
// 【原始的类型 class cn.itcast.b_dynamic.UserDao】
System.out.println(target.getClass());
// 给目标对象,创建代理对象
IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
// class $Proxy0 内存中动态生成的代理对象
System.out.println(proxy.getClass());
// 执行方法 【代理对象】
proxy.save();
}
}
15.3.8 策略模式
15.3.9 观察者模式
public interface Subject {
public void add(Observer observer);
public void del(Observer observer);
public void notifyObservers();
public void Operation();
}
public abstract class AbstractSubject implements Subject{
private Vector<Observer>observerVector=new Vector<>();
@Override
public void add(Observer observer) {
observerVector.add(observer);
}
@Override
public void del(Observer observer) {
observerVector.remove(observer);
}
@Override
public void notifyObservers() {
Enumeration<Observer> enumeration=observerVector.elements();
while(enumeration.hasMoreElements()){
enumeration.nextElement().ObserveAndUpdate();
}
}
}
public class ConcreteSubject extends AbstractSubject{
@Override
public void Operation() {
System.out.println("update start");
notifyObservers();
}
}
public interface Observer {
public void ObserveAndUpdate();
}
class Observer1 implements Observer{
@Override
public void ObserveAndUpdate() {
System.out.println("Observer1 has received this change");
}
}
class Observer2 implements Observer{
@Override
public void ObserveAndUpdate() {
System.out.println("Observer2 has received this change");
}
}
public class ObserverSchema {
/*
观察者模式很好理解,类似于邮件订阅和 RSS 订阅,当我们浏览一些博客或 wiki 时,经常会看到 RSS 图标,就
这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。
其实,简单来讲就一句话:
当一个对象变化时,
其它订阅即依赖该对象的对象都会收到通知,并且随着变化。
对象之间是一种一对多的关系。
*/
public static void main(String[] args){
Subject subject=new ConcreteSubject();
subject.add(new Observer1());
subject.add(new Observer2());
subject.Operation();
}
}
16.JAVA虚拟机JVM体系结构
(1)类加载器:JVM启动时或者类运行时将需要的class加载到JVM中。每个被装载的类的类型对应一个Class实例,唯一表示该类,存于堆中。
(2)执行引擎:负责执行JVM的字节码指令。执行引擎是JVM的核心部分,作用是解析字节码指令,得到执行结果(实现方式:直接执行,JIT(just in time)即时编译转成本地代码执行,寄存器芯片模式执行,基于栈执行)。本质上就是一个个方法串起来的流程。每个Java线程就是一个执行引擎的实例,一个JVM实例中会有多个执行引擎在工作,有的执行用户程序,有的执行JVM内部程序(GC)。
(3)内存区:模拟物理机的存储、记录和调度等功能模块,如寄存器或者PC指针记录器。存储执行引擎执行时所需要存储的数据。
(4)本地方法接口:调用操作系统本地方法返回结果。