一个类的生命周期可以分为什么?
加载-----------连接-----------初始化-----------使用-----------卸载
加载——要完成三件事
1.根据全限定名获取二进制字节流
2.静态存储结构(class文件)转换为方法区的运行时数据数据结构
3.在Java堆里面生成一个类对象,作为方法区的访问入口
连接——包括三个阶段(验证、准备、解析)
验证:
文件格式验证:
1.验证魔数(每个class文件的头4个字节,作用是确认这个文件是否为一个能被虚拟机接受的class文件)
2.验证版本
3.验证常量池
4.验证常量池中的值存不存在
5.是不是符合UTF-8编码
6.各个部分是否完整
元数据(描述数据的数据)验证:
1.类是否有父类
2.父类是否继承不允许修改的类
3.如果本类不是抽象类,是否实现所有的方法
4.本类的字段,方法是否与父类产生矛盾(final)
字节码验证:主要验证字节码虚拟机指令
1.通过数据流和控制流分析,确定程序语义是合法的。
2.对类的方法进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
符号引用验证:
1.符号引用中通过字符串描述的全限定名是否能找到对应的类。
2.在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段
3.符号引用中的类,字段,方法的访问性是否可被当前类访问
准备:
类变量和实例变量
类变量:也称静态变量,在类中以static关键字声明,但必须在构造方法和语句块之外
实例变量:属于该类的对象,必须产生该类的对象,才能调用实例变量
解析:将常量池内的符号引用替换为直接引用(指向目标的指针,或者相对的偏移量)
1.接口或类的解析
2.字段的解析
3.类方法的解析
4.接口方法的解析
初始化:
类的初始化
对象的初始化
类加载器
ClassLoader.loadClass核心代码总结:①同一时间只允许一个线程加载名字为name的类②在加载之前先检查,是否已经加载过该类。只有没有加载过的才允许加载③双亲委派:父加载器能加载的绝不给子类加载④父加载器加载不到类的情况,交给子加载器去加载(即:findClass)
public abstract class ClassLoader {
// 父加载器
private final ClassLoader parent;
// 加载类的核心方法之一。如果没找到类则抛出ClassNotFoundException
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// synchronized意味着:
// 1. 同一时间只允许一个线程加载名字为name的类
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 2. 在加载之前先检查,是否已经加载过该类
Class<?> c = findLoadedClass(name);
// 3. c==null代表只有没有被加载过的类才会进行加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 双亲委派
// 父加载器能加载的绝不给子加载器
if (parent != null) {
// 如果父加载器不为null,则把类加载的工作交给父加载器
c = parent.loadClass(name, false);
} else {
// 如果父加载器为null,则把类加载的工作交给BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父加载器不能加载,子加载器去尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 子加载器去尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
类加载的默认机制:双亲委派(父加载器能加载的绝不给子加载器加载,只有父加载器找不到所需的类才让子加载器尝试加载)
类加载器有
BootStrapClassLoader(启动类加载器): 加载基本类 Obejct Class rt.jar
ExtClassLoader(扩展类加载器): 加载扩展类 ext目录中
AppClassloader(应用程序类加载器): 加载工程路径中的Class
如何打破双亲委派机制?
如果想打破双亲委派模型,只需要重写loadClass方法即可。如
// 自定义类加载器
// 目的:打断默认的双亲委任
ClassLoader loader = new ClassLoader() {
//自定义类加载器需要覆写loadclass函数
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
//根据类名获取文件名
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
//把文件转为流
InputStream is = getClass().getResourceAsStream(fileName);
//如果流为空,则交给父类调用loadClass去加载
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
//把流转换为字节数组
is.read(b);
//把字节码转化为Class并返回
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
为什么要使用双亲委派模型?
保证我们的基类不被替换
保证我们的基类能正常运行
保证我们的基类只有一份
JVM运行时数据区(分为共享类和私有类)
方法区与Java堆一样,是各个线程共享的内存区域。用于存储:
1.已被虚拟机加载的类信息
2.常量
3.静态变量
4.即时编译器编译后的代码
当方法区无法满足内存分配需求时抛出OutOfMemoryError异常
垃圾回收算法
标记清除算法
概念:该算法分为“标记”和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点:①效率问题,标记和清除的效率都不高(标记和清除会花费不少时间)
②空间问题,标记清除之后会产生大量不连续的内存碎片
标记复制算法
概念:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就会将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清除掉
缺点:内存缩小为原来的一半,内存利用率太低
标记整理算法
概念:标记过程和标记清除算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理
缺点:标记整理过程中需要停止用户线程
堆根据年龄(熬过的垃圾回收次数)划分不同的区
新生代:朝生夕死,Eden:s0:s1=8:1:1
老年代(熬过第15次垃圾回收):越难消亡
垃圾回收机制
对象优先分配到Eden区
大对象直接进入到老年代
长期存活的对象会进入到老年代
动态年龄判断
空间分配担保
根据区域回收划分GC分类
Minor GC(YoungGC/YGC):新生代
Major GC(OldGC/OGC):老年代
FullGC/FGC:全部,包含堆和方法区(metaspace)
FullGC触发条件
System.gc
老年代空间不足
方法区空间不足
经过MinorGC后,运行的对象大小大于老年代的可用空间
垃圾回收器
Serial收集器(串行垃圾回收器):进行垃圾收集时,必须暂停其他所有的工作线程,直到他结束。针对于新生代的垃圾回收器,采用复制算法
Serial Old:进行垃圾收集时,必须暂停其他所有的工作线程,直到他结束。针对于老年代的垃圾回收器,采用标记整理算法
Parallel Scavenge:可控制吞吐量,针对于新生代的并行垃圾回收器,采用复制算法
Parallel Old:针对于老年代的并行垃圾回收器,采用标记整理算法。
ParNew:Serial收集器的多线程版本。针对于新生代的垃圾回收器,采用复制算法
CMS:用户线程和垃圾回收线程一块执行解决标记清除效率不高的问题,。针对于老年代的垃圾回收器,采用标记清除算法
CMS回收过程包含四个阶段
- 初始标记
- 并发标记
- 重新标记
- 并发清除
CMS收集器缺点
- ①CMS收集器对CPU资源非常敏感
- ②CMS收集器无法处理浮动垃圾
- ③基于标记清除算法实现,收集结束会有大量空间碎片产生
• 解决方案
• ①设置CMS在完成垃圾收集后是否要进行一次碎片整理
• ②设置CMS在进行若干次垃圾收集后再启动一次内存碎片整理
G1:可控垃圾回收停顿时间
三色标记法:理解为三种状态,标记的时候,可达状态就是1,不可达状态就是0,在0和1之间又多了一种状态
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描过了。黑色的对象已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍
灰色:表示对象已经被垃圾收集器访问过,但是还没有被扫描完
白色:表示对象尚未被垃圾收集器访问过
会出现漏标的现象。
解决方案:增量更新(当一个黑色增加了对白色对象的引用,那么将这个黑色对象置灰)和原始快照(当一个灰色取消对白色对象的引用,那么将这个白色对象置灰)
垃圾回收分为四个步骤
- ①初始标记
- ②并发标记
- ③最终标记
- ④筛选回收
JVM工具
jps:显示正在运行的虚拟机进程
jstat:用于展示运行时的状态信息
用法: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] -gc:统计Java堆,包括Eden、Survivor、老年代、永久代的容量,已用空间、GC时间等信息。例如:jstat -gc 21336 500 1000 每500毫秒打印一次打印1000次
jstack:用于生成虚拟机当前时刻的线程快照,可以定位线程间死锁、死循环、请求外部资源导致的长时间等待
jmap:用于生成head dump文件,如果不使用这个命令,还可以使用-XX:+HeadDumpOnOutOfMemoryError参数来让虚拟机出现OOM自动生成dump文件
用法:# 生成Java堆快照。格式:-dump:[live, ]format=b, file=<filename>
# live为是否只生成存活的对象 jmap -dump:file=4562.hprof 4562
jinfo:①查看正在运行的Java应用进程的参数信息②可以实时调整正在运行的Java应用进程的部分参数
jdk1.9默认的垃圾回收器 G1
jdk1.7和1.8默认使用的垃圾回收器 Parallel Scavenge和Parallel Old