异常
Error
- 是程序无法处理的错误,表示运行应用程序中较严重问题。
- 内存溢出可能是因为代码有死循环;静态变量过多;递归
Exception
- 是程序本身可以处理的异常。
- Exception又包含了运行时异常(RuntimeException, 又叫非检查异常)和非运行时异常(又叫检查异常)
- 运行时异常都是RuntimeException类及其子类,这些异常是不检查的异常, 是在程序运行的时候可能会发生的, 所以程序可以捕捉, 也可以不捕捉。
- 检查异常是运行时异常以外的异常, 也是Exception及其子类, 这些异常从程序的角度来说是必须经过捕捉检查处理的, 否则不能通过编译。
java中的异常处理机制
捕获异常
1
2
3
4
5
6
7
8try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
}catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}finally {
}抛出异常
1
throw new MyException("这是一个异常");//异常信息
示例
1 | try{ |
1 | public void test() throws IOException{ |
- 自定义的异常
1 | public class MyException extends Exception{ |
注意点
- 不要在finally块中处理返回值
1
2
3
4
5
6
7try{
//因为finally中包含了return语句
//所以return true失去作用
return true;
}finally{
return false;
}- 不要在构造函数中抛出异常。
- 尽可能的减小try块
try -with-resources
try(){}
这个括号在JDK1.7之前是没有的,是1.7的新特性。
括号里的内容支持包括流以及任何可关闭的资源,数据流会在 try 执行完毕后自动被关闭,而不用我们手动关闭了
1 | try (FileReader reader = new FileReader("data.txt")) { |
泛型
自定义泛型方法
先写public void test1(T t),由于java中要先声明后使用,所以要在方法前面加上
1 | public class Geric { |
自定义泛型类
在类定义了泛型后就不用在方法中再定义了,但静态方法还是要先定义
1 | public class Geric<T> { |
- 泛型类
1 | //原来的样子 |
1 | //使用泛型来代替自己的变量类型 |
1 | //使用例子 |
- 泛型接口
1 | public interface Generator<T>{ |
- 泛型方法
1 | public class Main{ |
1 | public class Main { |
- 泛型接口使用方法
1 | interface Info<T>{ |
- 类型判断
1 | Class c1=new ArrayList<Integer>().getClass(); |
通配符
<? extends XXX>
能获取,但是向上转型成XXX;不能插入,因为XXX的子类很多,不能确定你插入哪一种
<? super XXX>
能插入XXX或XXX的子类,但是存入时会向上转型成XXX;不能获取,因为XXX的父类很多,不知道获取的类型,但是如果用基类Object或者强制类型转换(XX)是可以获取的
<?>
一个无法确定的类型,不能获取,不能插入
上边界限定通配符
1 | List<? extends Father> list1 = new ArrayList<>(); |
下边界限定通配符
1 | List<? super Child> list1 = new ArrayList<>(); |
边界通配符
List<?> list
表示 list
是持有某种特定类型的 List,但是不知道具体是哪种类型。
因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。
和List list
相比也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。
参考:
https://segmentfault.com/a/1190000005337789
http://www.jianshu.com/p/2005318f171f
多线程
- 通过实现Runable接口
1 | public class DisplayMessage implements Runnable |
- 通过继承Thread类本身
1 | public class GuessANumber extends Thread |
- 测试
1 | public class ThreadClassDemo |
IO处理
- 字节和字符的关系
- 1个字节=8位
- 1个字符=2字节=16位
- 一个汉字是2字符,全角符号也2字符,半角就一个字符
- byte 1字节
- short 2字节
- int 4字节
- long 8字节
- char 2字节(字符)
- float 4字节
- double 8字节
File类操作API
File实例可以是目录也可以是文件
路径分隔符
- windows下的路径分隔符是
\
,又因为\
表示转义字符,所以需要两个\
,如”C:\\a.txt” - Linux下的路径符是
/
,如/User/frank/document/b.txt
- windows下的路径分隔符是
构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13public class FileDemo {
public static void main(String[] args){
//File(String pathName)
File f1 =new File("c:\\abc\\1.txt");
//File(String parent,String child)
File f2 =new File("c:\\abc","2.txt");
//File(File parent,String child)
File f3 =new File("c:"+File.separator+"abc");//separator 跨平台分隔符,创建文件夹
File f4 =new File(f3,"3.txt");
System.out.println(f1);
}
}
//输出:c:\abc\1.txt创建方法
1
2
3boolean createNewFile() //不存在返回true 存在返回false
boolean mkdir() //创建目录
boolean mkdirs() //创建多级目录删除方法
1
2boolean delete() //删除File对象对应的文件或路径
boolean deleteOnExit() //当java虚拟机退出时,删除File对象对应的文件或路径判断方法
1
2
3
4
5
6
7
8boolean canExecute() //判断File对象对应的文件是否可执行
boolean canRead() //判断File对象对应的目录或文件是否可读
boolean canWrite() //判断File对象对应的目录或文件是否可写
boolean exists() //判断File对象对应的目录或文件是否存在
boolean isDirectory() //判断File对象对应的是目录,不是文件
boolean isFile() //判断File对象对应的是文件,不是目录
boolean isHidden() //判断File对象对应的文件是否隐藏
boolean isAbsolute() //判断File对象对应的目录或文件是否是绝对路径获取方法
1
2
3
4
5
6
7
8
9
10String getName() //获取File对象对应的目录名或文件名
String getPath() //获取File对象对应的路径名
String getAbsolutePath() //获取File对象绝对路径
String getParent() //获取File对象对应的父目录名,如果没有父目录返回null
long lastModified() //获取文件最后一次修改的时间
long length() //获取文件内容的长度
boolean renameTo(File f) //重命名File对象对应的目录或文件,成功返回true
File[] liseRoots() //获取系统的所有根路径
String[] list() //获取File对象的所有子路径名或子文件名
File[] listFiles() //获取File对象的所有子路径或子文件
字符流和字节流的主要区别
- 字节流读取的时候,读到一个字节就返回一个字节; 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回。
- 字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流
输入和输出
- 输入(InputStream)是指从文件输入到内存(程序),输出(OutputStream)是指从内存(程序)输出到文件
字节流
方法
1
2
3
4
5
6
7
8
9<InputStream>
int read() //从输入流中读取单个字节
int read(byte[] b) //从输入流中最多读取b.length个字节的数据,并将其储存在字节数组b中,返回实际读取的字节数
int read(byte[] b, int off, int len) //从输入流中最多读取len个字节,并将其储存在字节数组b中,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数
<OutputStream>
void write(int c) //将指定的字节输出到输出流
void write(byte[] buf) //将字节数组中的数据输出到指定的输出流中
void write(byte[] buf,int off,int len) //将字节数组中从off位置开始,长度为len的数据输出到指定的输出流中使用字节流读数据
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
28import java.io.FileInputStream;
import java.io.IOException;
public class ReadByteStream {
public static void main(String[] args) {
FileInputStream fis = null; // 声明文件输入流对象
try {
fis = new FileInputStream("test.txt"); // test.txt文件在当前工程目录下事先创建好
byte input[] = new byte[30];
fis.read(input); // 读入到一个字节数组
//转化到字符串str中
String str = new String(input, "UTF-8"); // 字符编码要与读入的文件对应
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close(); // 关闭输入流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}使用字节流写数据
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
27import java.io.FileOutputStream;
import java.io.IOException;
public class WriteByteStream {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
//里面true参数表示不覆盖原文件,直接在文件后面追加添加内容
fos = new FileOutputStream("test2.txt",true);
String str = "1234567";
byte[] outStr = str.getBytes("UTF-8"); // 读入字节数组,并指定编码方式
fos.write(outStr); // 使用文件输出流写出到文件
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}使用字节流拷贝文件
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
34import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyFile {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("test.png");
fos = new FileOutputStream("test_new.png");
byte input[] = new byte[50]; // 每次读取50 bytes
while (fis.read(input) != -1) { // read返回读入的数据大小,如果没有数据返回-1
fos.write(input); // 每次写入50 bytes
}
System.out.println("done");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}使用带缓冲的字节流读写数据
- 不带缓冲的操作, 每读一个字节就要写入一个字节, 由于涉及磁盘的IO操作相比内存的操作要慢很多, 所以不带缓冲的流效率很低
- 带缓冲的流, 可以一次读很多字节, 但不向磁盘中写入, 只是先放到内存里. 等凑够了缓冲区大小的时候一次性写入磁盘, 这种方式可以减少磁盘操作次数, 速度就会提高很多!
- 带缓冲的流适合读写比较大的文件
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
41import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ReadByBufferedByteStream {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("movie.mp4");
// 缓冲区大小1000000字节
BufferedInputStream bis = new BufferedInputStream(fis,1000000);
FileOutputStream fos = new FileOutputStream("moive_new.mp4");
BufferedOutputStream bos = new BufferedOutputStream(fos,1000000);
//大型文件对应的数组可以大一些,小文件对应的数组小一些
byte input[] = new byte[100000];
int count = 0;
long before = System.currentTimeMillis(); // 开始计时
while (bis.read(input) != -1) {
bos.write(input);
count++;
}
bos.flush();
bis.close();
fis.close();
bos.close();
fos.close();
System.out.println(System.currentTimeMillis()-before+"ms"); // 总时长
System.out.println("读取了:"+count+"次");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14<Reader>
int read() //从输入流中读取单个字符
int read(char[] cbuf) //从输入流中最多读取cbuf.length个字符的数据,并将其储存在字符数组cbuf中,返回实际读取的字符数
int read(byte[] cbuf, int off, int len) //从输入流中最多读取len个字符,并将其储存在字符数组cbuf中,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数
<Writer>
void write(int c) //将指定的字符输出到输出流
void write(char[] buf) //将字符数组中的数据输出到指定的输出流中
void write(char[] buf,int off,int len) //将字符数组中从off位置开始,长度为len的数据输出到指定的输出流
//因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组
void write(String str) //将str字符串中包含的字符输出到输出流
void write(String str,int off,int len) //将str字符串中从off位置开始,长度为len的数据输出到指定的输出流
//因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组使用字符流读写数据
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
38import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class RWByCharStream {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("java.txt");
FileOutputStream fos = new FileOutputStream("java_new.txt");
// 字符流的使用要传入字节流作为参数
InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8");
char input[] = new char[100]; // 每次读取的数据大小
int l = 0;
while ((l = reader.read(input)) != -1) {
// void write(char cbuf[], int off, int len)
// 文件末尾的长度不一定是100,所以需要设置写入数据长度
writer.write(input, 0, l);
}
reader.close(); // 先关闭字符流
fis.close(); // 再关闭字节流
writer.close();
fos.close();
System.out.println("done");
} catch (IOException e) {
e.printStackTrace();
}
}
}使用带缓冲的字符流读写数据
- 使用字符流读取数据时不能按行读取,这时候就需要使用带有缓冲区的字符流。
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
53import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
public class RWByBufferedCharStream {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("java.txt");
FileOutputStream fos = new FileOutputStream("java_new_buff.txt");
InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8");
BufferedReader br = new BufferedReader(reader);
// BufferedWriter bw = new BufferedWriter(writer);
// PrintWriter和BufferedWriter用法类似
// PrintWriter可以输出换行符
// 构造方法PrintWriter(Writer out,boolean autoFlush) 里可以设置缓冲区自动输出,这样就不需要手动调用flush方法了。
PrintWriter pw = new PrintWriter(writer, true);
String input;
while ((input = br.readLine()) != null) { // BufferedReader可以按行读取
// bw.write(input); //
// BufferedWriter的writer方法是带有缓冲区的,此时打印的文本是不带换行符的
pw.println(input);// PrintWriter的println方法支持不同平台的换行符输出
}
// bw.flush(); // 强制输出缓冲区内容。如果不加上flush,最后的缓冲区未读满将不输出内容
// bw.close();
pw.close();// 按顺序关闭流
writer.close();
fos.close();
br.close();
reader.close();
fis.close();
System.out.println("done");
} catch (IOException e) {
e.printStackTrace();
}
}
}FileReader 和FileWriter
- FileReader 和FileWriter 专门用于操作文本文件. 用法与FileInputStream 类似
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
42import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileRW {
public static void main(String[] args) {
FileReader fr = null;
BufferedReader br = null;
FileWriter fw = null;
BufferedWriter bw = null;
try {
fr = new FileReader("java.txt");
br = new BufferedReader(fr);
fw = new FileWriter("java_new.txt");
bw = new BufferedWriter(fw);
String line;
while ((line = br.readLine()) != null) {
bw.write(line + "\n");
}
bw.flush();
System.out.println("done");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bw.close();
fw.close();
br.close();
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
相互转化
1 | //用字节流读取文件,再转成字符流 |
1 | //没有字符流转字节流,因为实际开发用不到,所有相关API也没有了 |
Java Nio
阻塞和非阻塞(都是同步)
阻塞是询问后阻塞到有结果再返回
非阻塞询问后没结果则返回不等待
同步和异步
同步是从上到下地顺序运行
异步是分一个任务,在有空的时候执行,返回结果的时候通知主任务
区别:
同步是主动地执行,异步是被动地执行
Channel
Channel国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream。而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作,NIO中的Channel的主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel;通过看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。
Buffer
NIO中的关键Buffer实现有:ByteBuffer、CharBuffer、DoubleBuffer、 FloatBuffer、IntBuffer、 LongBuffer,、ShortBuffer,分别对应基本数据类型: byte、char、double、 float、int、 long、 short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不具体陈述其用法细节。
Selector
Selector 是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用 Selector , 得向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
邮箱
- 准备
- 要使用Java的邮箱功能需要javax.mail这个jar包
- SMTP
- 简单邮件传输协议
- POP3
- 邮局协议
- 最简单的邮件传输
- 不包附件,只有正文部分,并且只是负责邮件的发送,因此只需要SMTP,当要读取邮件的时候就必须要使用POP3
1 | public boolean sendEmail(){ |
验证码
二维码
Excel导入导出
类加载
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。
加载
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象
连接
验证
为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全准备
负责为类的静态成员分配内存,并设置默认初始化值解析
虚拟机将常量池中的符号引用替换为直接引用
初始化
- 创建类的实例
- 访问类的静态变量,或者为静态变量赋值
- 调用类的静态方法
- 使用反射方式来强制创建某个类或接口对应的
java.lang.Class
对象 - 初始化某个类的子类
- 直接使用java.exe命令来运行某个主类
- 初始化注意点
- 如果类存在直接父类的话,且直接父类还没有被初始化,则先初始化其直接父类
- 如果类存在一个初始化方法,就执行此方法
- 初始化接口并不需要初始化它的父接口。
类什么时候初始化
加载完类后,类的初始化就会发生,意味着它会初始化所有类静态成员,以下情况一个类被初始化:
- 实例通过使用new()关键字创建或者使用class.forName()反射,但它有可能导致ClassNotFoundException。
- 类的静态方法被调用
- 类的静态域被赋值
- 静态域被访问,而且它不是常量
- 在顶层类中执行assert语句
类加载器
负责将
.class
文件加载到内存中,并为之生成对应的Class对象。虽然我们不需要关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行。
- Bootstrap ClassLoader 根类加载器
- 负责加载存放在JDK\jre\lib(JAVA_HOME/lib)的类库
- Extension ClassLoader 扩展类加载器
- 负责加载存放在JAVA_HOME/lib/ext下的类库
- Application ClassLoader 系统类加载器
- 负责加载用户类路径(ClassPath)所指定的类
- Bootstrap ClassLoader 根类加载器
- 示例
1 | package kevin.demo; |
1 | public class Demo { |
1 | 结果: |
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
JMM内存模型
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
JVM的内存结构
JVM的内存一共分为5个部分:
程序计数器
: 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,因为cpu调度线程是轮流切换的,所以每个线程需要程序计数器来记录下一个字节码指令的地址,来恢复到正确的执行位置方法区
: 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也存放着常量池。虚拟机栈
: Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。本地方法栈
: 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务堆
: Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
JVM堆内存被分为两部分年轻代(Young Generation)和老年代(Old Generation)
新生代(Young)
新生代是所有新对象产生的地方。当年轻代的内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。新生代分为3个部分Eden区和Survivor区(FromSpace和ToSpace,两个区域大小相同,大小8:1:1),新生代的大小可以通过-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
特点
大多数新建的对象都在Eden区
当Eden区域被填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区
Minor GC同样会检查存活下来的对象,并把他们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区
经过多次GC周期后,仍然存活下来的对象会被转移到老年代内存空间。通常这是在新生代有资格提升到老年代前通过设定年龄阈值来完成的。
老年代(Tenured)
老年代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。通常会在老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC会花费更多的时间。旧生带占用大小为-Xmx值减去-Xmn对应的值
永久代(Perm)
永久代包含了JVM需要的应用元数据,这些元数据描述了在应用里使用的类和方法。注意,永久代不是Java堆内存的一部分,有一些JVM没有这一代,主要存放敞亮及类的一些信息,默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize
及-XX:MaxPermSize
来设置最小值和最大值。
永久代存放JVM运行时使用的类。永久代同样包含了Java SE库的类和方法。永久代的对象在full GC时进行垃圾收集。
以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的方法区,不属于堆。
元空间(metaspace)
现今JDK8中PermGen(永久代)已经被彻底移除,取而代之的是metaspace数据区,使用native内存,申请和释放由虚拟机负责管理。
在JDK8下,旧的参数-XX:PermSize和-XX:MaxPermSize会被忽略并显示警告。新的Metaspace通过参数-XX:MetaspaceSize 和-XX:MaxMetaspaceSize设定。
内存泄露和内存溢出的区别
泄漏强调无法收回,占用内存;溢出强调不够,无法满足
- 内存泄露:指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
或【内存中存在可达、无用的对象,且不会被GC回收,一直占用着内存。
该对象就是内存泄漏的对象 】 - 内存溢出:指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
jvm 何时抛出OutOfMemoryException:并不是内存被耗空的时候才抛出
1)JVM98%的时间都花费在内存回收
2)每次回收的内存小于2%
垃圾收集器(统称)
Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
Full GC(Major GC)
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
- 年老代(Tenured)被写满
- 持久代(Perm)被写满
- System.gc()被显示调用
垃圾收集器(分类)
Serial GC
Serial是年轻代的单线程GC,GC算法是复制算法
Serial old GC
Serial old是老年代的单线程GC,GC算法是标记-整理算法
Parnew GC
Parnew是年轻代的多线程GC,GC算法是复制算法
Parallel Scavenge GC
Parallel Scavenge是年轻代的多线程GC,GC算法是复制算法
优点:最优化吞吐量
吞吐量 = 用户代码执行时间占cpu所有时间的比率
Parallel old GC
Parallel old是老年代的多线程GC,GC算法是标记-整理算法
CMS(concurrent mark sweep) GC
CMS是老年代的多线程GC,GC算法是标记-清除算法
优点:用户线程和GC线程并发同时运行
缺点:清除之后空间碎片太多
G1(garbage first) GC
G1是管理整个GC堆的多线程GC,它把java堆分成多个大小相等的region独立区域,GC算法是类似标记整理的算法
优点:并行和并发
垃圾回收GC算法
- 引用计数算法:
该算法对每一个对象都有一个引用计数,没增加一次引用就+1,减少一次引用-1,在回收时将引用计数为0的对象清理掉。这种算法简单,但是无法解决循环引用的问题(比如: A引用B, B也引用A,但是A和B都没有被其它任何对象引用)。
- 标记-清除算法:
该算法分为两个阶段, 第一阶段遍历找出所有需要被回收的对象,并做上标记,第二阶段对清理所有被标记的对象,这种算法效率比较低,并且会产生较多的内存碎片。
- 标记-整理(压缩)算法:
该算法的第一阶段和标记-清除算法是一样的,而第二阶段它不是直接清理掉垃圾对象,而且将存活的对象往同一侧移动,移动完成后清理掉另一侧所有的对象。这种算法不会产生内存碎片,但是效率低下。
- 复制算法:
该算法将内存分为两个区域,进行垃圾回收时,就将还活着的对象复制到另一块内存区域中,然后再将整片内存区域清空。这种算法简单快速,而且不会产生内存碎片,但是因为将内存分成两块,所以可用的内存会少很多。
- 分代收集算法:
将内存细分为多个区域,不同区域GC的频率,并对不同的区域采用适当的收集算法。如JVM将内存分为年轻代和老年代,普通对象最开始分配在年轻代(大对象会直接分配到老年代),同一个对象在经过几次GC后还存活着,就认为这个对象的生命周期会比较长,将其移入老年代,GC主要发生在年轻代。
GC工作机制
1.新生代有一个Eden区和两个survivor区,首先将对象放入Eden区,如果空间不足就向其中的一个survivor区上放,如果仍然放不下就会引发一次发生在新生代的minor GC,将存活的对象放入另一个survivor区中,然后清空Eden和之前的那个survivor区的内存。在某次GC过程中,如果发现仍然又放不下的对象,就将这些对象放入老年代内存里去。
2.大对象以及长期存活的对象直接进入老年区。
3.当每次执行minor GC的时候应该对要晋升到老年代的对象进行分析,如果这些马上要到老年区的老年对象的大小超过了老年区的剩余大小,那么执行一次Full GC以尽可能地获得老年区的空间。
GC Roots
针对的对象:从GC Roots搜索不到,而且经过一次标记清理之后仍没有复活的对象。
当一个对象到 GC Roots 没有任何引用链相接的时候,那么这个对象就是不可达,就可以被回收。
不可达不一定会死
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)
各区GC算法策略
新生代:复制清理; 老年代:标记-清除或标记-整理算法; 永久代:存放Java中的类和加载类的类加载器本身。
什么时候发生GC
eden区放不下新生的对象时,发生minor gc,如果在发生minor gc之前,虚拟机检查老年代的最大可用的连续空间小于新生代所有对象的总空间,则需要担保策略。如果担保,则计算新生代对象晋升的平均空间,如果够用,则冒风险试一次,如果不够用就要发生一次fullgc。除了CMS的concurrent collection之外,年轻代和老年代都会被收集。
什么时候转向老年区
- 对象优先在eden区分配,eden区的gc之后对象年龄就加1,到15岁之后就转老年代
- 大对象会直接转向老年代
- 如果eden区的survivor区放不下对象时也会转向老年代
finalize
finalize是在垃圾收集器删除对象之前对这个对象调用的,如果对象没有在这个方法里进行自救,那只能被回收,但是一个对象的finalize()方法只会被系统自动调用一次
减少GC开销
- 不要显式调用System.gc()
- 尽量减少临时对象的使用
- 尽量使用StringBuffer,而不用String来累加字符串
- 尽量少用静态对象变量
速学路径
反射
动态语言
- Python,Ruby是动态语言,C++,Java,C#不是动态语言。但是JAVA有着一个非常突出的动态相关机制:Reflection(反射),用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
概念
- Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
静态编译
- 在编译时确定类型,绑定对象,即通过
动态编译
- 运行时确定类型,绑定对象。动态编译最大限度发挥了java的灵活性,体现了多态的应用,有以降低类之间的藕合性
优点
- 一个大型的软件,不可能一次就把把它设计的很完美,当这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时才动态的创建和编译,就可以实现该功能。
缺点
- 对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。
Object
- 该类是所有类的父类
Class类
- 类是java.lang.Class类的实例对象,而Class是所有类的类
1
2
3
4
5//Class c = new Class();不能这样构建一个Class
//因为构造方法是私有的
private Class(ClassLoader loader) {
classLoader = loader;
}- 得到Class对象的3种方法
1
2
3
4
5
6
7
8
9//这说明任何一个类都有一个隐含的静态成员变量class,这种方式是通过获取类的静态成员变量class得到的
Class c1 = Code.class;
//code1是Code的一个对象,这种方式是通过一个类的对象的getClass()方法获得的
Code code1 = new Code();
Class c2 = code1.getClass();
//这种方法是Class类调用forName方法,通过一个类的全量限定名获得
Class c3 = Class.forName("com.trigl.reflect.Code");- c1、c2、c3都是Class的对象,他们是完全一样的,而且有个学名,叫做Code的类类型(class type)
- 我们可以通过类类型知道一个类的属性和方法,并且可以调用一个类的属性和方法,这就是反射的基础
1
2
3
4
5
6
7System.out.println(c1.getName());
System.out.println(c2.getName());
System.out.println(c3.getName());
//输出
com.trigl.reflect.Code
com.trigl.reflect.Code
com.trigl.reflect.CodeClass作用
获取成员方法Method
1
2
3
4public Method getDeclaredMethod(String name, Class<?> ...parameterTypes) // 得到该类指定参数的自身声明的方法,不包括父类的
public Method[] getDeclaredMethods(); // 得到该类自身声明所有的方法,不包括父类的
public Method getMethod(String name, Class<?> ...parameterTypes) // 得到该类指定参数的自身所有的public方法,包括父类的
public Method[] getMethods();// 得到该类自身所有的public方法,包括父类的获取成员变量Field
1
2
3
4public Field getDeclaredField(String name) // 获得该类指定名称的自身声明的变量,不包括其父类的变量
public Field[] getDeclaredFields() // 获得该类自身声明的所有变量,不包括其父类的变量
public Field getField(String name) // 获得该类指定名称的自身所有的public成员变量,包括其父类变量
public Field[] getFields() // 获得该类自身所有的public成员变量,包括其父类变量获取构造函数Constructor
1
2
3
4public Constructor<T> getDeclaredConstructor(Class<?> ...parameterTypes) // 获得该类指定参数的自身声明的构造器,不包括其父类的构造器
public Constructor<?>[] getDeclaredConstructors() // 获得该类所有的自身声明的构造器,不包括其父类的构造器
public Constructor<T> getConstructor(Class<?> ...parameterTypes) // 获得该类指定参数的自身所有的public构造器,包括父类
public Constructor<?>[] getConstructors() // 获得该类自身所有的public构造器,包括父类获得(Class,Method,Constructor上)的注解Annotation
1
2
3
4
5public <A extends Annotation>A getDeclaredAnnotation(Class<A> annotationClass)//获取直接修饰该程序的指定类型的Annotation
public Annotation[] getDeclaredAnnotations()//获取直接修饰该程序的所有的Annotation
public <A extends Annotation>A getAnnotation(Class<A> annotationClass) //获取该程序元素上存在的指定类型的Annotation
public Annotation[] getAnnotations() //获取该程序元素上存在的所有的Annotation
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) //判断该程序元素上是否存在指定类型的注解实例化对象
1
2Class c = Class.forName("com.tengj.reflect.Person"); //先生成class
Object o = c.newInstance(); //newInstance会使用该类默认的构造器来实例化一个对象
示例
1 | public class Person { |
1 | public class ReflectDemo { |
1 | 结果: |
- 使用反射避免泛型的类型约束
1 | public class GenericEssence { |
1 | 结果: |
注解
作用
- 创建文档。
- 这是最常见的,也是java 最早提供的注解。常用的有
@see
@param
@return
等
- 这是最常见的,也是java 最早提供的注解。常用的有
- 跟踪代码中的依赖性。
- 实现替代配置文件功能。比较常见的是spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量。
- 甚至执行基本编译时检查。
- 如
@override
放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。
- 如
- 创建文档。
标准 Annotation (java內置注解)
- @Override
- 用于修饰此方法覆盖了父类的方法。
- @Deprecated
- 用于修饰已经过时的方法。
- @SuppressWarnings
- 通知java编译器忽略特定的警告信息
@SuppressWarnings(value={“deprecation”, “unchecked”})
@SuppressWarnings({“deprecation”, “unchecked”})
@SuppressWarnings(“unchecked”)
- 通知java编译器忽略特定的警告信息
- @Override
元 Annotation (元注解)
元 Annotation 是指用来定义 Annotation 的 Annotation
@Retention
- 指明了该Annotation被保留的时间长短。
- RetentionPolicy取值为SOURCE,CLASS,RUNTIME。
SOURCE:在源文件中有效(即源文件保留)
CLASS:在class文件中有效(即class保留)
RUNTIME:在运行时有效(即运行时保留)
@Target
- 指明该类型的注解可以注解的程序元素的范围。
- 该元注解的取值可以为
TYPE
,METHOD
,CONSTRUCTOR
,FIELD
等。 - 如果Target元注解没有出现,那么定义的注解可以应用于程序的任何元素。
CONSTRUCTOR:用于描述构造器
FIELD:用于描述域
LOCAL_VARIABLE:用于描述局部变量
METHOD:用于描述方法
PACKAGE:用于描述包
PARAMETER:用于描述参数
TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Inherited
- 标记注解,表示了某个被标注的类型是被继承的
- 如果用户在当前类中查询这个元注解类型并且当前类的声明中不包含这个元注解类型,那么也将自动查询当前类的父类是否存在Inherited元注解,这个动作将被重复执行知道这个标注类型被找到,或者是查询到顶层的父类。
@Documented
- 指明拥有这个注解的元素可以被javadoc此类的工具文档化。
- 如果一种声明使用Documented进行注解,这种类型的注解被作为被标注的程序成员的公共API。
示例
- 注解方法不能带有参数
- 注解方法返回值类型限定为:基本类型、
String
、Enums
、Annotation
或者是这些类型的数组 - 注解方法可以有默认值
- 注解本身能够包含元注解,元注解被用来注解其它注解
1
2
3
4
5
6
7
8
9
10
(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
public MethodInfo{
String author() default 'yangki';
String date();
int revision() default 1;
String comments();
}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
27package net.yangki.annotations;
public class AnnotationExample {
public static void main(String[] args) {
}
'yangki', comments = 'Main method', date = 'Nov 17 2012', revision = 1) (author =
public String toString() {
return 'Overriden toString method';
}
'deprecated method', date = 'Nov 17 2015') (comments =
public static void oldMethod() {
System.out.println('old method, don't use it.');
}
@SuppressWarnings({ 'unchecked', 'deprecation' })
@MethodInfo(author = 'yangki', comments = 'Main method', date = 'Nov 17 2015', revision = 10)
public static void genericsTest() throws FileNotFoundException {
List l = new ArrayList();
l.add('abc');
oldMethod();
}
}- 用反射来解析注解
- 注解的
RetentionPolicy
应该设置为RUNTIME
,否则java类的注解信息在执行过程中将不可用,那么我们也不能从中得到任何和注解有关的数据。
- 注解的
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
31public class AnnotationParsing {
public static void main(String[] args) {
try {
for (Method method : AnnotationParsing.class
//Class.getClassLoader()方法返回的类的类加载器
.getClassLoader()
//加载AnnotationExample类
.loadClass(('net.yangki.annotations.AnnotationExample'))
//获得AnnotationExample类的Class后调用getMethods()获取该类的所有的public方法
.getMethods()) {
//判断该方法上的MethodInfo注解是否存在
if (method.isAnnotationPresent(com.journaldev.annotations.MethodInfo.class)) {
try {
for (Annotation anno : method.getDeclaredAnnotations()) {
System.out.println('Annotation in Method ''+ method + '' : ' + anno);
}
MethodInfo methodAnno = method.getAnnotation(MethodInfo.class);
if (methodAnno.revision() == 1) {
System.out.println('Method with revision no 1 = '+ method);
}
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}