Java特性:
面向对象(封装,继承,多态)
平台无关性(JVM运行.class文件,每个java程序对应一个JVM实例)
语言(泛型,Lambda)
类库(集合,并发,网络,IO/NIO)
JRE(Java运行环境,JVM,类库)
JDK(Java开发工具,包括JRE,javac,诊断工具)
Java是解释执行吗?
不准确!
1、Java源代码经过Javac编译成.class文件
2、.class文件经JVM 解释或编译运行。
(1)解释:.class文件经过JVM内嵌的解释器转换为机器码。
(2)编译:存在JIT编译器(Just In Time Compile 即时编译器),JIT能够在运行时将热点代码编译成机器码,这种情况部分代码就属于编译执行,而不是解释执行。
(3)AOT编译器: Java 9提供的直接将所有代码编译成机器码执行。
参考:《java核心技术36讲》 第1讲 | 谈谈你对java平台的理解
理解面向对象:
面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;
面向对象有三大特征:封装性、继承性、多态性
封装性指的是隐藏了对象的属性和实现细节,仅对外提供公共的访问方式。好处是便于使用,提高了复用性和安全性。
继承性,就是两个类间存在着一定的所属关系,那么继承的类就可以从被继承的类中获得一些属性和方法。好处是提高了代码的复用性。继承是作为多态的前提的。
子类中所有的构造函数都会默认访问父类中的空参数的构造函数,默认第一行有super();若无空参数构造函数,子类中需指定;另外,子类构造函数中可自己用this指定自身的其他构造函数。
多态性是说父类或接口的引用指向了子类对象。好处是提高了程序的扩展性,只要实现一个接口或继承了一个类,那么就可以使用父类中相应的方法,但父类不能访问子类中的成员。前提:实现或继承关系;覆写父类方法。
多态即:事物在运行过程中存在不同的状态。多态可以分为编译时多态和运行时多态,编译时多态是指方法的重载,运行时多态是指方法的重写。
对于运行时多态需要满足以下三个条件:
那么多态实现的机制是什么呢?
其实就是依靠静态分派和动态分派。
静态分派即所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载。在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本。
动态分派是指根据变量的实际类型来分派方法的执行版本的,典型的例子就是方法的重写。在运行期间,根据对实例化子类的不同,调用不同子类中重写的方法。
参考:多态性实现机制——静态分派与动态分派
抽象类是一个可同时包含具体方法和抽象方法(方法未被实现)的类。抽象方法必须被该抽象类的子类实现。抽象类是可以继承的。
接口像是描述类的一张蓝图或者说是类的一种契约,它包含了许多空方法,这代表着它的所有的子类都应该拥有共同点。它的子类应该提供这些空方法的具体实现。一 个类需要用 implements 来实现接口,接口可以用 extends 来继承其他接口。
相同点:
1、都不能被实例化。
2、接口的实现类和抽象类的子类只有全部实现了接口或者抽象类中的方法后才可以被实例化。
不同点:
1、接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法,也可以实现方法。
2、单继承,多实现。接口可以实现多个,只能继承一个抽象类。
3、接口强调的是功能(like a),抽象类强调的是所属关系(is a)。
4、接口中的所有成员变量 为public static final, 静态不可修改,当然必须初始化。接口中的所有方法都是public abstract 公开抽象的。而且不能有构造方法。抽象类就比较自由了,和普通的类差不多,可以有抽象方法也可以没有,可以有正常的方法,也可以没有。
参考: 接口(Interface)与抽象类(Abstract Class)的区别?
重载
在同一个类中,方法名相同,参数列表不同(参数的个数不同、参数的对应的数据类型不同)。与返回值无关。
重写
是存在子父类之间的,子类定义的方法与父类中的方法具有相同的方法名字
参数列表必须完全与被重写方法的相同。
返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
父类的成员方法只能被它的子类重写。
声明为 final 的方法不能被重写。
声明为 static 的方法不能被重写,但是能够被再次声明。
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
构造方法不能被重写。
如果不能继承一个方法,则不能重写这个方法。
区别
不能,静态方法和类对象没有关系,在对象实例化之前这个方法就已经存在于内存。子类中如果定义了相同名称的静态方法,并不会重写,而应该是在内存中又分配了一块给子类的静态方法,没有重写这一说,只是单纯的名字问题。
可以被继承,不能不重写,原因同上
父类B静态代码块—>子类A静态代码块—>父类B非静态代码块—>父类B构造函数—>子类A非静态代码块—>子类A构造函数
静态代码块在程序运行过程中只会执行一次
作用:
参考:内部类, 静态内部类, 局部类, 匿名内部类的解析和区别
如果是静态内部类,只能引用包含类的静态成员。
非静态内部类,都可以引用。
修饰符/作用域 | 当前类 | 同一package | 子类 | 其它 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | x |
default | √ | √ | x | x |
private | √ | x | x | x |
1、==
基本类型则是判断内容值是否相等,
引用类型则是判断引用是否相等
2、equals
默认使用object的equals方法,和==等效,一般用来判断字符串内容是否相等(String重写了equals,比较步骤是:1.判断引用是否相同 2.判断是否是String类型 3.判断字符串长度 4.逐个字符对比)
当覆盖了equals方法时,比较对象是否相等将通过覆盖后的equals方法进行比较(判断对象的内容是否相等)
特性:
1、自反性:对任意引用值X,x.equals(x)的返回值一定为true.
2、对称性:对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3、传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true
4、一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
5、非空性:任何非空的引用值X,x.equals(null)的返回值一定为false
3、hashcode
hashcode方法只有在集合中用到,将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
hashcode 和 equals:
1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
2、如果两个对象不equals,他们的hashcode有可能相等。
3、如果两个对象hashcode相等,他们不一定equals。
4、如果两个对象hashcode不相等,他们一定不equals。
参考:java中equals,hashcode和==的区别
java语言是一个面向对象的语言,但是java中的基本数据类型却不是面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个基本数据类型对应的类统称为包装类(Wrapper Class)。
java语言提供了两种不同的类型:引用类型和基本数据类型,int是JAVA语言的基本数据类型,Integer是java为int提供的包装类,是引用数据类型。
java中的八个基本数据类型是:byte,short,int,long,float,double,char,boolean
对应的包装类:Byte,Short,Integer,Long,Float,Double,Character,Boolean.
包装类的主要用途:
可以通过Integer.valueOf(String s)方法将String转换为Integer
Integer.valueOf(String s) > Integer.valueOf(parseInt(s, 10)) > 调用Integer内部的parseInt(string str,10)方法,默认基数为10,parseInt内部首先判断字符串是否包含符号(-或者+),然后再循环字符串,对单个char进行数值计算 > 通过Character.digit(char ch, int radix) 这个方法,其中通过ASCII转换,拼接成int > 最后Integer.valueOf(int i)调用new Integer(int value)方法返回Integer
1、类型转换:基本数据类型转换原理
基本数据类型转换
1、boolean类型不参与转换
2、隐式类型转换
3、强制类型转换
自动装箱和拆箱的原理:
自动装箱和拆箱应用于基本类型数据和它的包装类之间,java自动将基本类型转换为包装类类型叫做自动装箱,反之叫做自动拆箱。
自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
例如:
Integer i1 = 40; 自动装箱,相当于调用了Integer.valueOf(40);方法。
首先判断i值是否在-128和127之间,如果在-128和127之间则直接从IntegerCache.cache缓存中获取指定数字的包装类;不存在则new出一个新的包装类。
IntegerCache内部实现了一个Integer的静态常量数组,在类加载的时候,执行static静态块进行初始化-128到127之间的Integer对象,存放到cache数组中。cache属于常量,存放在java的方法区中。
参考:Java 自动装箱与拆箱的实现原理
2、类型转换:引用数据类型转换原理
多态
如果一个对象与另一个对象没有任何的继承关系,那么他们就不能进行类型转换。如果要把一个派生类对象赋值给基类对象这个称为向上转型。如果要把基类对象赋值给派生类对象就需要强制类型转换,这称为向下转型,向下转型有一些危险,要安全的进行向下转型有一个前题,基类对象必须是从派生类对象中上溯过来的。
1、String对象可以通过"" 和 new来创建。String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。
2、String类其实是通过char数组(private final char value[])来保存字符串的。
3、String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象
4、JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串
5、用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,而是放入堆中,它们有自己的地址空间。new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址
6、字面量"+“拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的”+"拼接运算实在运行时进行的,新创建的字符串存放在堆中。
参考:深入理解Java中的String
字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.
安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面.
参考:为什么String要设计成不可变的?
(1)可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。
(2)是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。
(3)String、StringBuilder、StringBuffer三者的执行效率:
StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = “hello”+ "world"的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。
因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。
final 是一个修饰符,表示不可变。final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated。
static表示“静态”的意思,用来修饰成员变量和成员方法,亦可以形成静态代码块。只要这个类被加载,java虚拟机就能根据类名在运行时数据区的方法区内找到它们。因此,static成员可以再它的任何对象创建之前访问,无需引用任何对象。
1、流分类
字节流:InputStream,OutputStream
字符流:Reader,Writer
Reader:读取字符流的抽象类
BufferedReader:将字符存入缓冲区,再读取
LineNumberReader:带行号的字符缓冲输入流
InputStreamReader:转换流,字节流和字符流的桥梁,多在编码的地方使用
FileReader:读取字符文件的便捷类。
Writer:写入字符流的抽象类
BufferedWriter:将字符存入缓冲区,再写入
OutputStreamWriter:转换流,字节流和字符流的桥梁,多在编码的地方使用
FileWriter:写入字符文件的便捷类。
InputStream:字节输入流的所有类的超类
ByteArrayInputStream:含缓冲数组,读取内存中字节数组的数据,未涉及流
FileInputStream:从文件中获取输入字节。媒体文件
BufferedInputStream:带有缓冲区的字节输入流
DataInputStream:数据输入流,读取基本数据类型的数据
ObjectInputStream:用于读取对象的输入流
PipedInputStream:管道流,线程间通信,与PipedOutputStream配合使用
SequenceInputStream:合并流,将多个输入流逻辑串联。
OutputStream:此抽象类是表示输出字节流的所有类的超类
ByteArrayOutputStream:含缓冲数组,将数据写入内存中的字节数组,未涉及流
FileOutStream:文件输出流,将数据写入文件
BufferedOutputStream:带有缓冲区的字节输出流
PrintStream:打印流,作为输出打印
DataOutputStream:数据输出流,写入基本数据类型的数据
ObjectOutputStream:用于写入对象的输出流
PipedOutputStream:管道流,线程间通信,与PipedInputStream配合使用
2、流操作规律:
明确源和目的:
数据源:读取,InputStream和Reader
目的:写入:OutStream和Writer
数据是否是纯文本:
是:字符流,Reader,Writer
否:字节流,InputStream,OutStream
明确数据设备:
源设备:内存、硬盘、键盘
目的设备:内存、硬盘、控制台
是否提高效率:用BufferedXXX
3、转换流:将字节转换为字符,可通过相应的编码表获得
转换流都涉及到字节流和编码表
Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。
首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
参考:《java核心技术36讲》第11讲 | Java提供了哪些IO方式? NIO如何实现多路复用
Java 有多种比较典型的文件拷贝实现方式,比如:
利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作。
public static void copyFileByStream(File source, File dest) throws
IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
或者,利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。
public static void copyFileByChannel(File source, File dest) throws
IOException {
try (FileChannel sourceChannel = new FileInputStream(source)
.getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel
();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(
sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
当然,Java 标准类库本身已经提供了几种 Files.copy 的实现。
对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
参考:Java 集合框架
List:
有序、可重复、允许null元素
Set:
无序、不可重复、允许一个null元素
Map
键值对、键不可重复、允许一个null键,N个null值
使用选择
参考:List、Set、Map的区别
这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。
Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。
LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。
Vector与ArrayList在使用的时候,应保证对数据的删除、插入操作的减少,因为每次对改集合类进行这些操作时,都会使原有数据进行移动除了对尾部数据的操作,所以非常适合随机访问的场合。
LinkedList进行节点的插入、删除却要高效的多,但是随机访问对于该集合类要慢的多,因为LinkedList要移动指针。
LinkedList 实现线程安全方式如下:
List list = Collections.synchronizedList(new LinkedList(…));
LinkedList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
参考:《java核心技术36讲》第8讲 | 对比Vector、ArrayList、LinkedList有何区别
HashMap
HashTable
ConcurrentHashMap
参考:HashMap、Hashtable、ConcurrentHashMap的原理与区别
将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
序列化是一种将对象转换为字节流的过程,目的是传递对象或永久性保存对象数据。
Java 中,只有一种方式可以实现序列化,只需要实现 Serializable 接口。
在 Android 中,还有另外一种实现序列化的方式,实现 Parcelable,这个是 Android 独有的一种序列化方方式,相比较 Serializable,Parcelable 需要提供大量的模板代码,较为繁琐,但是效率比 Serializable 高出不少,因为 Serializable 实现的序列化利用反射,可能会进行频繁的IO操作,所以消耗比较大。而 Parcelable 则是在内存中进行序列化。
所以这里推荐的是:
参考:序列化的两种方式
用法:
修饰变量
作用:
1、transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。
2、被transient关键字修饰的变量不再能被序列化。
3、一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。也可以认为在将持久化的对象反序列化后,被transient修饰的变量将按照普通类成员变量一样被初始化。
参考:浅谈Java中transient关键字的作用
JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
以前的笔记:
1:反射的概念:
反射:就是通过Class文件对象,去使用该文件中的成员变量,构造方法,成员方法。
2:获取 Class 对象的三种方式:
3:反射获取构造方法
1、 获取所有
public Constructor[] getConstructors():所有公共构造方
法
public Constructor[] getDeclaredConstructors():所有构造方
法(包括私有)
2、 获取某个无参构造方法,获取的是对象
public Constructor getConstructor()
3、 获取某个有参构造,获取的是对象
public Constructor getConstructor(Class>…
parameterTypes)参数为参数的class类型,可变长度参数
4、 public T newInstance(Object… initargs)
通过获取的某个构造函数对象去实例化一个对象
类似于:Person p=new Person();
Object obj=构造函数对象.newInstance();
变量可以通过构造方法传的参数
4:获取方法
1、 获取所有的方法
Method[] methods = c.getMethods(); // 获取自己的包括父亲
的公共方法
Method[] methods = c.getDeclaredMethods(); // 获取自己的
所有的方法
2、 获取某个方法对象
public Method getMethod(String name,Class>…
parameterTypes)
第一个参数表示的方法名,第二个参数表示的是方法的参数的
class类型,如果是无参的第二个则不用填
3、 通过方法对象调用对象中的方法
public Object invoke(Object obj,Object… args)
返回值是Object接收,第一个参数表示对象是谁,第二参数表示调
用该方法的实际参数
5:获取成员变量
1、 获取所有的成员变量:
Field [] file=cl.getDeclaredFields();
2、 获取单个成员变量对象
Field f=cl.getDeclaredField(String name);
参数是成员变量名
3、 setAccessible(boolean)//暴力访问
若成员变量为私有的 ,就调用暴力访问
参数为true
4、 public void set(Object obj, Object value)
第一个参数为修改其字段的对象,第二个为被修改的字段的新值
6:特点
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
参考:Java反射
注解
从JDK 5开始, Java增加了注解,注解是代码里的特殊标记,这些标记可以在编译、 类加载、 运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、 处理或者进行部署。
注解分为标准注解和元注解
1.标准注解
标准注解有以下4种。
2.元注解
除了标准注解, 还有元注解, 它用来注解其他注解, 从而创建新的注解。 元注解有以下几种。
创建新注解通过@interface关键字
依赖注入
为了对象之间关系的解耦,出现了“控制反转”理论:借助于“第三方”实现具有依赖关系的对象之间的解耦。最终是获得依赖对象的过程被反转,获得依赖对象的过程由自身管理变为由IoC容器(IoC是Inversion of Control的缩写,即控制反转)主动注入。所以后来就有了个更合适的名字“依赖注入”(Dependency Injection, 简称DI)。 所谓依赖注入, 是指由IoC容器在运行期间, 动态地将某种依赖关系注入到对象中
依赖注入实现方式:
依赖注入框架
参考:《Android进阶之光》第9章
Java 泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
我们可以:
1、 自定义泛型接口
2、 自定义泛型类
3、 自定义泛型方法
涉及:
类型通配符,如List>
类型通配符上限,如List extends Number>
表示包括Number及它的子类
类型通配符下限,如List super Number>
表示包括Number及它的父类
参考:Java泛型
如上,extends一个表示类型通配符上限,super表示类型通配符下限
当Java程序违反了java的语义规则时,java虚拟机就会将发生的错误表示为一个异常。
违反语义规则包括2种情况,一种是java类库内置的语义检查,例如:角标越界,会引发IndexOutOfBoundsException;另一种情况是java允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用throw关键字引发异常。
java的异常分为Error和Exception。这两类都是接口Throwable的子类。Error及Exception及其子类之间的关系,大致可以用下图简述:
Error
仅在java的虚拟机中发生,用户无需在程序中捕捉或者抛出Error。
Exception
分类:
运行时异常(Unchecked 不检查异常)、非运行时异常(Checked 可检查异常)。
区别:
checked: 一般是指程序不能直接控制的外界情况,是指在编译的时候就需要检查的一类exception,用户程序中必须采用try catch机制处理或者通过throws交由调用者来处理。这类异常,主要指除了Error以及RuntimeException及其子类之外的异常。
unchecked:是指那些不需要在编译的时候就要处理的一类异常。在java体系里,所有的Error以及RuntimeException及其子类都是unchecked异常。再形象直白的理解为不需要try catch等机制处理的异常,可以认为是unchecked的异常。
常见异常:
+-----------+
| Throwable |
+-----------+
/ \
/ \
+-------+ +-----------+ +------------+
| Error | | Exception | ─ ─ ─|非运行时异常 |
+-------+ +-----------+ +------------+
/ | \ | / | \
\_______/ +------------------+ \_______/
unchecked | RuntimeException | checked
+------------------+
/ | | \
\_________________/
unchecked
捕获异常
try{
可能出现异常的语句;
}
catch(抛出的异常对象){
异常处理语句;
catch(抛出的异常对象){
异常处理语句;
}
finally{
最后需要处理的语句
}
注意:
throw 和 throws 的区别
自定义异常
jvm相关
解析:
1、解析阶段
我们知道,类的加载过程包含七个阶段:加载,验证,准备,解析,初始化,使用,卸载,七个阶段顺序开始,交叉进行**。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,是这七个阶段之一。**
2、解析调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。其中在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成,换句话说,编译器进行编译时就必须确定下来。这类方法的调用称为解析调用。
分派
1、静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。特殊的,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一的,往往只能确定一个“更加合适的”版本。产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。
2、动态分派
依赖实际类型来定位方法执行版本的分派动作称为动态分派。动态分派的典型应用是方法重写。动态分派发生在运行阶段,因此确定动态分派的动作是由虚拟机来执行的。
参考:深入理解Java解析和分派
代理模式是常用的Java设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。按照代理类的创建时期,代理类可分为两种。
静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。
区别:
参考:动态代理和静态代理到底有什么区别
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
参考:浅拷贝和深拷贝的区别?
并发,就是多任务交替进行,使得应用获得更多的CPU调度时和协同工作。
易混淆的两个概念:
并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
参考:关于Java并发编程的总结和思考
区别:
进程(一个进程对应一个应用程序)是资源分配的最小单位,线程(线程是一个进程中的执行场景。一个进程可以启动多个线程)是程序执行的最小单位。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
在Android中:
进程
空进程(Empty Process)
不包含任何活动应用程序组件,这种进程存在的唯一原因是做为缓存以改善组件再次于其中运行时的启动时间
后台进程(Background process)
不可见状态的activity进程,onstop被调用
服务进程(Service Process)
由 startService() 方法启动的服务,一般都在做着用户所关心的事情,系统会尽量维持它们的运行
可见进程(Visible Process)
activity不在前端显示,但也没有完全隐藏,能够看得见,比如弹出一个对话框
前台进程(Foreground Process)
有正在与用户交互的Activity
优先级依次提高
线程
主线程 (UI线程)
应用启动时,系统会为应用创建一个名为“主线程”的执行线程
工作线程 (子线程)
Android是单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。 如果执行的操作不能很快完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。
多线程就像是人体一样,一直在并行的做许多工作,例如,人可以同时呼吸,血液循环,消化食物等。多线程可以将一个程序划分成多个任务,他们彼此独立的工作,以方便有效的使用处理器和用户的时间
另可参考:一篇文章让你彻底征服多线程开发
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码;如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是有全局变量及静态变量引起的。多个线程共享同一数据,当某一线程执行多条语句时,其他线程也执行进来,导致数据在某一语句上被多次修改,执行到下一语句时,导致错误数据的产生。
因素:
多个线程操作共享数据;多条语句操作同一数据
解决:
原理:某一时间只让某一线程执行完操作共享数据的所有语句。
办法:使用锁机制:synchronized或lock对象
线程安全需要保证几个基本特性:
原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。
当两个或两个以上的线程需要共享资源,他们需要某种方法来确定资源在某一刻仅被一个线程占用,达到此目的的过程叫做同步(synchronization)。
分类:
同步代码块:synchronized(对象){},将需要同步的代码放在大括号中,括号中的对象即为锁。
同步函数:放于函数上,修饰符之后,返回类型之前。
如果数据将在线程间共享,使用同步编程。例如正在写的数据可能被另一个线程读到,或者正在读的数据可能正在被另一个线程写入,那么这些数据就是共享数据,必须进行同步存取。
当应用程序调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法返回时,则使用异步编程,在很多情况下采用异步途径往往更有效率。
参考:Java 多线程编程
停止线程的方式:
1、自然终止,run()方法执行结束后,线程自动终止。
2、使用stop()方法,已经被弃用(不安全)。
3、使用volatile 标志位(其实就是外部控制的自然死亡)。
4、使用interrupt()方法中断运行态和阻塞态线程。
参考:Java中如何正确而优雅的终止运行中的线程 、Java中断interrupt详解
区别:
Runnable 是接口。
Thread 是类,且实现了Runnable接口。
实现Runnable接口比继承Thread类所具有的优势:
适合多个相同的程序代码的线程去处理同一个资源
可以避免java中的单继承的限制
增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void.。
参考:Thread类的run()和start()方法的区别
yield方法只是让出分配给自己的CPU时间片,并且会立刻进入Runnable状态参与CPU时间的竞争,若程序中没有其它线程,那么该线程马上就会开始往下执行;
sleep会进入Blocked状态,等待时间结束事件的发生,然后进入Runnable状态参与CPU时间的竞争
wait()是object的方法,sleep()是Thread的方法
执行权和锁区分
wait:可指定等待的时间,不指定须由notify或notifyAll唤醒。
线程会释放执行权,且释放锁。
sleep:必须制定睡眠的时间,时间到了自动处于临时(阻塞)状态。
即使睡眠了,仍持有锁,不会释放执行权。
wait和notify都是object类的方法,和synchronized搭配使用完成线程同步
wait()与notify()是成对使用的, 是一种线程间的通信手段
wait用于线程释放锁和执行权,进入等待队列,处于闲置状态
当一个线程调用一个对象的notify()方法时, 调度器会从所有处于该对象等待队列(waiting queue)的线程中唤醒任意一个线程, 将其添加到入口队列( entry queue) 中. 然后在入口队列中的多个线程就会竞争对象的锁, 得到锁的线程就可以继续执行。 如果等待队列中(waiting queue)没有线程, notify()方法不会产生任何作用
参考:为什么wait()和notify()需要搭配synchonized关键字使用
用法:
同步静态方法、同步实例方法、同步代码块
作用:
线程同步
原理:
synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。
摘抄自《java核心技术36讲 - synchronized底层如何实现?什么是锁的升级、降级?》:
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
参考:深入理解Java并发之synchronized实现原理
用法:
修饰变量
作用:
保证变量在多个线程之间的可见性
实现原理:
可见性:
对volatile变量的读操作和普通变量没区别,写操作与普通变量的主要区别有两点:
(1)修改volatile变量时会强制将修改后的值刷新的主内存中。
(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
原子性:
volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
有序性:
内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。
参考:volatile关键字原理实现及应用
synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。
ReentrantLock,通常翻译为再入锁,是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。
synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
synchronized会自动释放锁,而Lock一定要求程序员手动释放,并且必须在finally从句中释放。
另参考:java中volatile、synchronized和lock解析
线程池:
一种线程使用模式。其实就是个线程队列,维护着多个线程,对线程进行管理。
为什么使用:
1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
2、线程并发数量过多,抢占系统资源从而导致阻塞
3、线程池会对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等
java中的线程池:
在 Java 中,线程池的概念是 Executor 这个接口,具体实现为ThreadPoolExecutor 类
ThreadPoolExecutor提供了四个构造函数
// 最长的七个参数的构造函数
public ThreadPoolExecutor(
int corePoolSize, // 该线程池中 核心线程数最大值
int maximumPoolSize, // 该线程池中 线程总数最大值
long keepAliveTime, // 该线程池中 非核心线程闲置超时时长
TimeUnit unit,// keepAliveTime的单位
BlockingQueue workQueue,// 该线程池中的任务队列:维护着等待执行的Runnable对象
ThreadFactory threadFactory,// 根据需要创建新线程的对象
RejectedExecutionHandler handler// 抛出异常专用的
)
添加任务:
通过 ThreadPoolExecutor.execute(Runnable command) 方法即可向线程池内添加一个任务
策略:
线程数量未达到corePoolSize,则新建一个核心线程执行任务
线程数量达到了corePoolSize,则将任务移入队列等待
队列已满,新建非核心线程执行任务
队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler抛出异常
常见四种线程池:
通过类Executors的静态方法获取:
CachedThreadPool()
可缓存线程池:
线程数无限制
有空闲线程则复用空闲线程,若无空闲线程则新建线程
一定程序减少频繁创建/销毁线程,减少系统开销
FixedThreadPool()
定长线程池:
可控制线程最大并发数(同时执行的线程数)
超出的线程会在队列中等待
ScheduledThreadPool()
可延迟或定期执行线程池:
SingleThreadExecutor()
单线程化的线程池:
有且仅有一个工作线程执行任务
所有任务按照指定顺序执行,即遵循队列的入队出队规则
参考:线程池,这一篇或许就够了
多线程下载:
在下载大文件的时候,我们往往要使用多线程断点续传,保证数据的完整性,首先说多线程,我们要多线程下载一个大文件,就有开启多个线程,多个connection,既然是一个文件分开几个线程来下载,那肯定就是一个线程下载一个部分,如果文件的大小是200M, 使用两个线程下载, 第一个线程下载1-100M, 第二个线程下载101-200M。
我们在请求的header里面设置
conn.setRequestProperty("Range", "bytes="+startPos+"-"+endPos);
这里startPos是指从数据端的哪里开始,endPos是指数据端的结束
根据这样我们就知道,只要多个线程,按顺序指定好开始跟结束,就可以解决下载冲突的问题了。
如何写文件
byte[] buffer = new byte[1024];
int offset = 0;
RandomAccessFile threadFile = new RandomAccessFile(this.saveFile,"rwd");
threadFile.seek(startPos);
threadFile.write(buffer,0,offset);
从上面代码可以看出,每个线程找到自己开始写的位置,就是seek(startPos)
这样就可以保证数据的完整性,也不会重复写入了
基本上多线程的原理就是这样,其实也很简单
断点续传
那么我们接着说断点续传,断点续传其实也很简单**,原理就是使用数据库保存上次每个线程下载的位置和长度**
例如我开了两个线程T1,T2来下载一个文件,设文件总大小为1024M,那么就是每个线程下载512M
可是我的下载中断了,那么我下次启动线程的时候(继续下载),是不是应该要知道,我原来下载了多少呢
所以是这样的,每下载一点,就更新数据库的数据,
参考:Android多线程断点续传原理解析
Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。可以使用更少的代码来实现同样的功能
参考:Java中Lambda表达式的使用
应用阶段(In Use)
对象至少被一个强引用持有着。
不可见阶段(Invisible)
简单说就是程序的运行已经超出了该对象的作用域了
该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
不可达阶段(Unreachable)
指该对象不再被不论什么强引用所持有。
收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”而且垃圾回收器已经对该对象的内存空间又一次分配做好准备时,则对象进入了“收集阶段”
终结阶段(Finalized)
当对象运行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
对象空间重分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间又一次分配阶段”。
参考:Java对象的生命周期
通常可以把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程唯一的。
第一,程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。
第二,Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。
前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。
栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。
第三,堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。
理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
第四,方法区(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。
第五,运行时常量池(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。
第六,本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。
导致OOM的原因:
除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError,简单总结如下:
堆内存不足是最常见的 OOM 原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。
而对于 Java 虚拟机栈和本地方法栈,这里要稍微复杂一点。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
直接内存不足,也会导致 OOM
参考:《java核心技术36讲》第25讲 | 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。当GC确定这个对象为"不可达"时,GC就会收集、回收这个对象。
垃圾收集主要就是两个方面,就是存储在堆上的对象实例和**方法区中的元数据等信息。**对于对象实例收集,主要有两种方法,
Java 选择的可达性分析,其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。(JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为 GC Roots。)
另一种是:引用计数算法
顾名思义,就是为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为 0,即表示对象可回收。这是很多语言的资源回收选择,例如因人工智能而更加火热的 Python,它更是同时支持引用计数和垃圾收集机制。具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
常见的垃圾收集算法:
复制(Copying)算法
我前面讲到的新生代 GC,基本都是基于复制算法,过程就如专栏上一讲所介绍的,将活着的对象复制到 to 区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化 这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销
标记 - 清除(Mark-Sweep)算法
首先进行标记工作,标识出所有要回收的对象,然后进行清除 这么做除了标记 清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现 Full GC,暂停时间可能根本无法接受
标记 - 整理(Mark-Compact)算法
类似于标记 - 清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间 注意,这些只是基本的算法思路,实际 GC 实现过程要复杂的多,目前还在发展中的前沿 GC 都是复合算法,并且并行和并发兼备
常见的垃圾回收器:
Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。
从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。
ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作
CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。
有什么办法主动通知虚拟机进行垃圾回收?
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
参考:《java核心技术36讲 第27讲 | Java常见的垃圾收集器有哪些?》
根据java对象的生命周期推测:
1、 我认为对象的生命周期到达对象空间重分配阶段
,才意味对象真正死亡,因为就算对象不被强引用所持有,它还是占据堆内存空间
2、根据垃圾回收算法,对象生命周期到达不可达状态
,即意味死亡,可被GC收集、回收
3、堆的新生代、老年代,并不能判断对象的死亡
参考:JVM垃圾回收算法之新生代和老年代
参考上面:简述垃圾回收器的工作原理?
对象的垃圾收集,主要有两种算法:可达性分析和引用计数法,当对象不可达或引用计数为0时,对象会被处理掉。
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。是JVM主动的一个行为。
System.gc(),是我们主动通知GC运行垃圾回收,但是Java语言规范并不保证GC一定会执行。
不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。
在Java语言中,除了基本数据类型外,其他的都是指向各类对象的对象引用;Java中根据其生命周期的长短,将引用分为4类。
1 强引用
特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足**,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象**来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
2 软引用
特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
3 弱引用
弱引用通过WeakReference类实现。弱引用的生命周期比软引用短。**在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。**由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
应用场景:弱应用同样可用于内存敏感的缓存。
4 虚引用
特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。
强引用就像大老婆,关系很稳固
软引用就像二老婆,随时有失宠的可能
弱引用就像情人,关系不稳定,别人一勾搭就跑了
幻像引用就是梦中情人,只在梦里出现过。
垃圾回收器会在下一个周期对这个对象进行回收
一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化,具体行为在Java 虚拟机规范里有非常详细的定义。
首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java 虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。
最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
如果之前创建过"abc"这个字符串,则String s=“a”+“b”+"c"这个语句实际上是没有创建对象的,而是直接返回常量池中"abc"的引用,否则,就在常量池中创建一个"abc"的字符串对象,因此是创建了0个或1个对象
“a” + “b” + “c” , 会在编译期间就折叠为"abc",并没有创建对象
Dalvik
是 Google 公司自己设计用于 Android 平台的 Java 虚拟机,每一个 Dalvik 应用作为一个独立的Linux进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
Dalvik 主要是完成对象生命周期管理,堆栈管理,线程管理,安全和异常管理,以及垃圾回收等等重要功能。
Dalvik 和 Java 虚拟机的区别
dvm 执行的是.dex 文件,而 jvm 执行的是.class。Android 工程编译后的所有.class 字节码会被 dex 工具抽取到一个.dex 文件中。
dvm 是基于寄存器的虚拟机 而 jvm 执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm 可以根据硬件实现最大的优化,比较适合移动设备。
.class 文件存在很多的冗余信息,dex 工具会去除冗余信息,并把所有的.class 文件整合到.dex 文件中。减少了 I/O 操作,提高了类的查找速度。
Android Runtime (ART)
是 Android 上的应用和部分系统服务使用的托管式运行时。ART 及其前身 Dalvik 最初是专为 Android 项目打造的。作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。
ART主要功能:
ART优点:
ART缺点:
参考:
编码是从一种形式或格式转换为另一种形式的过程也称为计算机编程语言的代码简称编码。计算机中存储信息的最小单元是一个字节,即8个bit。
常见的编码方式:
ASCII码:共有128个,用一个字节的低7位表示
ISO8859-1:在ASCII码的基础上涵盖了大多数西欧语言字符,仍然是单字节编码,它总共能表示256个字符
GB2312:全称为《信息交换用汉字编码字符集基本集》,它是双字节编码,总的编码范围是A1~F7 、A1~A9 符号区、B0~F7 汉字区
GBK:数字交换用汉字编码字符集,它可能是单字节、双字节或者四字节编码,与GB2312编码兼容
UTF-16:具体定义了Unicode字符在计算机中的存取方法。采用2字节来表示Unicode转化格式,它是定长的表示方法,不论什么字符都可以用两个字节表示
UTF-8: UTF-8采用一种变长技术,每个编码区域有不同的字码长度,不同的字符可以由1~6个字节组成。
如果一个字节,最高位为0,表示这是一个ASCII字符(00~7F)
如果一个字节,以11开头,连续的1的个数暗示这个字符的字节数
一个utf8数字占1个字节
一个utf8英文字母占1个字节
少数是汉字每个占用3个字节,多数占用4个字节。
参考:UTF-8编码占几个字节?