1,一个类被加载进jvm中要经历哪几个过程
加载:通过io的方式将class文件读入到我们的jvm中。
校验:通过校验字节码文件头4位16进是否是以cafe babe开头。 目的是为了检查此文件是否java编写。
准备:将类中的静态属性赋初始值。
解析:将符号引用转换成直接引用。(在刚开始时,假如一个类引用了另一个类,这时jvm并不不知道这人引用的地址,会先用一个唯一的符号代替,这时就叫符号引用,通过解析,通过这个符号找到另一个类的地址,把这个符号替换成这个引用。
初始化:将类中的静态部分赋指定的值,并执行静态代码块。
2,类加载器
Bootstrap ClassLoader 启动类加载器:负载加载 jre/lib 下的核⼼类库中的类,⽐如rt.jar, charsets.jar
ExtClassLoader 扩展类加载器:负载加载 jre/lib 下的 ext ⽬录内的类
AppClassLoader 应⽤类加载器:负载加载⽤户⾃⼰写的类
⾃定义类加载器:⾃⼰定义的类加载器,可以打破双亲委派机制。
3,双亲委派机制
当类加载进⾏加载类的时候,类的加载需要向上委托给上⼀级的类加载器,上⼀级继续向上
委托,直到启动类加载器。启动类加载器去核⼼类库中找,如果没有该类则向下委派,由下
⼀级扩展类加载器去扩展类库中,如果也没有继续向下委派,直到找不到为⽌,则报类找不
到的异常。
4,为什么要使用双亲委派机制
防⽌核⼼类库中的类被随意篡改
防⽌类的重复加载
5,双亲委派机制的核心源码
ClassLoader.class
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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;
}
}
6,全盘委托机制
当⼀个类被当前的 ClassLoader 加载时,该类中的其他类也会被当前该 ClassLoader 加载。除
⾮指明其他由其他类加载器加载。 -- 意思就是假如我们有一个自定义的一个类Student, 里面引用了String,按照常理String应该由BootStrap ClassLoader加载,但是这里Student由我们的AppClassLoader加载,所以这个String也会有AppClassLoader加载。
7,自定义类加载器打破双亲委派
自定义一个类继承ClassLoader并重写findClass和LoadClass
/**
重写loadClass⽅法
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//对于Object类,使⽤⽗加载器
if(!name.startsWith("com.qf.jvm")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
7,运行时数据区的介绍
运行时数据区也就是jvm在运行时产生的数据存放的区域,这块区域就是jvm的内存区域,也称为jvm的内存模型--jmm。
运行时数据区主要包括以下几个部分:
堆空间(线程共享):存放new出来的对象
b(线程共享):存放元信息,类的模板,常量池,静态部分
线程栈(线程独享):本地方法产生的数据
本地方法区(线程独享):本地方法产生的数据
程序计数器(线程独享):配合执行引擎执行指令
8,程序在执行时运行时数据区中的内存变化
当我们执行一个方法时,就会在线程栈中创建一个栈帧,栈帧包括以下四个区域:
局部变量表:存放方法的局部变量
操作数栈:用来存放方法中要操作的数据
动态链接:存放方法名和方法内容的映射关系,通过方法名找到方法的内容,即符合引用转 换为直接引用。
方法出口:记录方法执行后调用此方法的位置。
9.对象的创建流程。
(1)类加载校验:校验此类是否已经被加载,主要检查常量池中是否存在该类的类元信息,如果没有,则需要进行加载。
(2) 分配内存:为对象分配内存,具体有以下策略:
- Bump the Pointer(指针碰撞):如果内存空间中分配是绝对工整的,则jvm记录当前剩余内存的指针,在已用内存进行分配。
- Free List(空闲列表):如果内存分配不规则,则jvm会维护一个可用内存空间的列表用于分配。
对象并发分配中存在的问题:
- Compare And Swap :自旋分配,如果并发分配失败则重试分配之后的地址。即下一个。
Thread Local Allocation Buffer (TLAPB): 本地线程分配缓冲,jvm会为每一个线程分配一块空间,每个线程在自己的空间中创建对象。
(3)设置初始值:根据数据类型,赋初始值,如String先赋值null,int赋值0。
(4)设置对象头:为对象设置对象头信息,对象头信息包括以下内容:类元信息,对象hash码,对象年龄(后面垃圾回收机制用),锁状态标志。
- 对象头中的 Mark Work 字段( 32 位)
- 对象头中的类型指针
类型指针⽤于指向元空间当前类的类元信息。⽐如调⽤类中的⽅法,通过类型指针找到元空
间中的该类,再找到相应的⽅法。
开启指针压缩后,类型指针只⽤ 4 个字节存储,否则需要 8 个字节存储。
- 指针压缩
过⼤的对象地址,会占⽤更⼤的带宽和增加 GC 的压⼒。
对象中指向其他对象所使⽤的指针: 8 字节被压缩成 4 字节。 最早的机器是 32 位,最⼤⽀持内
存 2 的 32 次⽅ =4G 。现在是 64 位, 2 的 64 次⽅可以表示 N 个 T 的内存。内存 32G 即等于 2 的 35 次
⽅。如果内存是 32G 的话,⽤ 35 位表示内存地址,这样过于浪费。如果把 35 位的数据,根据
算法,压缩成 32 位的数据(也就是 4 个字节)。在保存时⽤ 4 个字节,再使⽤时使⽤ 8 个字节。
之前⽤ 35 位保存内存地址,就可以⽤ 32 位保存。这样 8 个字节的对象,实际上使⽤ 32位来保
存,这样 64 位就能表示 2 个对象。
如果内存⼤于 32G ,指针压缩会失效,会强制使⽤ 64 位来表示对象地址。因此 jvm 堆内存最好
不要⼤于 32G 。
(5)执行init方法
为对象中的属性赋值和执行构造方法。
10,对象成为垃圾的判断依据
在堆空间和元空间中,GC这条守护线程会对这些空间展开垃圾回收工作,有两种算法:
- 引用计数法:
对象被引用,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收。但是这种算法没有办法解决循环依赖的问题,因此jvm目前的主流厂商HotSpot没有使用这种算法。
- 可达性分析算法:GC Roots根
gc roots根节点:在对象引用中,会有这么几种对象的变量:来自于线程栈中的局部变量表中的变量,静态变量,本地方法中的变量,这些变量都被称为gc roots根节点。
判断依据:gc在扫描堆空间中的某个节点时,向上遍历,看看能不能遍历到gc roots根节点,如果不能,那么意味这个对象是垃圾。
11,对象中的finalize方法。
Object类中有一个finalize方法,也就是说任何一个对象都要finalize方法,这个方法是对象被回收之前的最后一根救命稻草。
- GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize方法将被调用。
- 调用finalize方法如果对象被引用,那么第二次标记该对象,被标记对象将移除即将被回收 的集合,继续存活。
- 调用finalize方法如果对象没有被引用,那么将会被回收。
- finalize只会被调用一次。
12,对象的逃逸分析。
在jdk1.7之前,对象的创建都是在堆中,但是会有一个问题,方法中创建的对象,没有被外界访问。类似于这种对象,在堆中频繁的创建,当方法结束时,又要被gc,很浪费资源。解决办法就是这种对象直接在栈中创建,会随着方法的出栈而随之销毁,不再需要gc。
-------------------------------------------------------垃圾回收算法-------------------------------------------------------
13,标记清除算法。
首先标记出所有需要被回收的对象,在标记完成之后再统一回收。
缺点:
标记和清除的效率都不高。
空间问题,清除后产生大量不连续的内存随便。如果有大对象会出现空间不够的现象从而不得不提
前触发另一次垃圾收集动作。
14,复制算法
他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
解决了内存碎片问题。
缺点:
将原来的内存缩小为原来的一半,存活对象越多效率越低。
15,标记整理算法。
先标记出要被回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。解决了复制算法和标记清理算法的问题。
16,分代收集算法。
它把我们的堆内存空间划分为了两块,一个叫新生代,一个叫老年代,占比为1:2。新生代又进一步划分为了eden(伊甸园区),survivor1,survivor2。 占比为 8:1:1。
- 为什么要做一个这样子的划分?
主要是因为在Java中有的对象是需要长时间使用,长时间使用的对象放在老年代中,而那些用完了就可以丢弃的对象就可以放在新生代中,这样的话就可以针对于对象生命周期的不同特点进行不同的垃圾回收策略。老年代的垃圾回收就很久才发生一次,新生代的垃圾回收就发生的比较频繁,新生代处理的都是朝生夕死的对象,老年代则是处理更有价值而且长时间存在的对象。这样针对于不同的区域采用不同的算法可以更有效的对垃圾回收进行管理。
- 一个对象的生命
当我们创建一个新的对象时,这个新对象默认就会采用伊甸园的一个空间,接下来可能会创建很多的对象并放入伊甸园中,伊甸园的空闲空间也就逐渐减少,当创建一个对象发现伊甸园的空间不够的时候,就会触发一次垃圾回收,这是在新生代的垃圾回收(Minor GC)。Minor GC触发以后,就会采用之前说的可达性分析算法,沿着GC Root引用链去找,看伊甸园的对象是有用的还是垃圾并进行标记。标记成功后就会采用复制算法,把存活的对象复制到幸存区To中,复制到幸存区TO之后,会让幸存对象的寿命加1,剩余在伊甸园的对象就可以进行全部的回收,然后再让幸存区From和幸存区To进行交换,至此第一次垃圾回收就完成了,此时伊甸园的空间又充足了,就可以继续向伊甸园分配对象。
当经过一段时间,伊甸园的空间又满了,这时触发第二次垃圾回收,这次的垃圾回收除了把伊甸园中幸存的对象找到之外,还需要在幸存区中找是否还有需要继续存活的对象。然后把伊甸园中存活的对象放到幸存区TO中并将寿命加1,除此之外还会把幸存区From中仍要存活的对象放在幸存区To中并在之前的寿命基础上加1。然后再来释放伊甸园和幸存区From的垃圾清理掉。然后交换From和To.这样伊甸园的空间又充足,第二次的垃圾回收就完成了。
幸存区中的对象不会永远在幸存区待着,当它的寿命超过了一个阈值,比如默认的阈值是15,即只要经历15次垃圾回收,对象还在存活,则说明这个对象价值比较高,经常在使用,这就没必要一直在幸存区中留着,因为以后垃圾回收还是不能回收这个对象,此时这个这个对象就会晋升到老年代中,因为老年代的垃圾回收频率比较低,不会轻易将它回收掉。这就是Minor GC垃圾回收的流程。
在不断的回收过程中,可能会出现老年代中的内存空间已经满了,新生代中的空间也满了,此时进行Minor GC就不能解决问题,此时就需要进行Full GC,一般这些垃圾回收都是在空间内存不足的时候才会触发。Full GC的含义就是老年代的空间不足时做一次整体的清理,从新生代到老年代,整个堆进行清理。
总结:
堆空间被分成了新⽣代( 1/3 )和⽼年代( 2/3 ),新⽣代中被分成了 eden ( 8/10 )、
survivor1(1/10) 、 survivor2(1/10)
对象的创建在 eden ,如果放不下则触发 minor gc
对象经过⼀次 minorgc 后存活的对象会被放⼊到 survivor 区,并且年龄 +1
survivor 区执⾏的复制算法,当对象年龄到达 15. 进⼊到⽼年代。
如果⽼年代放满。就会触发 Full GC
17,对象进入到老年代的条件。
- 大对象直接进入到老年代中:大对象可以通过参数设置大小,多大的对象被认为是大对象,-xx:PretenureSizeThreshold
- 当对象的年龄到达15岁时将进入到老年代,这个年龄也可以通过参数设置:XX-MaxTenuringThreshold.
- 根据对象动态的年龄判断,如果s区中的对象总和超过了s区中的50%,那么下一次复制的时候,把年龄大于等于这次年龄的对象都一次性全部放入到老年代中。
- ⽼年代空间分配担保机制 :在 minor gc 时,检查⽼年代剩余可⽤空间是否⼤于年轻代⾥ 现有的所有对象(包含垃圾)。如果⼤于等于,则做 minor gc 。如果⼩于,看下是否配 置了担保参数的配置: -XX: -HandlePromotionFailure ,如果配置了,那么判断⽼年代剩 余的空间是否⼩于历史每次 minor gc 后进⼊⽼年代的对象的平均⼤⼩。如果是,则直接 full gc ,减少⼀次 minor gc 。如果不是,执⾏ minor gc 。如果没有担保机制,直接 full gc 。
----------------------------------------------------垃圾回收器-------------------------------------------------------------
18,Serial收集器(-XX:+UseSerialGC - XX:+UseSerialOldGC)
单线程执⾏垃圾收集,收集过程中会有较⻓的 STW ( stop the world ),在 GC 时⼯作线程不
能⼯作。虽然 STW 较⻓,但简单、直接。
新⽣代采⽤复制算法,⽼年代采⽤标记 - 整理算法。
何为stw? 意思就是在gc时会停止其他的线程。
19,Parallel收集器(-XX:+UseParallelGC,- XX:+UseParallelOldGC)
使⽤多线程进⾏ GC ,会充分利⽤ cpu ,但是依然会有 stw ,这是 jdk8 默认使⽤的新⽣代和⽼年
代的垃圾收集器。充分利⽤ CPU 资源,吞吐量⾼。
新⽣代采⽤复制算法,⽼年代采⽤标记 - 整理算法。
20,ParNew收集器(-XX:+UseParNewGC)
⼯作原理和 Parallel 收集器⼀样,都是使⽤多线程进⾏ GC ,但是区别在于 ParNew 收集器可以
和 CMS收集器配合⼯作。主流的⽅案: ParNew 收集器负责收集新⽣代。 CMS 负责收集⽼年代。
21,CMS收集器(-XX:+UseConcMarkSweepGC)
⽬标:尽量减少 stw 的时间,提升⽤户的体验。真正做到 gc 线程和⽤户线程⼏乎同时⼯作。
CMS 采⽤标记 - 清除算法。
概述:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。由于大部分 Java 应用主要集中在互联网网站以及基于浏览器的 B/S 系统的服务端,这类应用通常会较为关注服务的响应速度,希望系统的停顿时间尽可能少,CMS 收集器就非常符合这类应用的需求。
步骤:
- 初始标记:暂停所有的其他线程(STW),并记录gc roots直接能引用的对象。
- 并发标记:从gc roots的直接关联开始向下查找,这个过程比较长,但是不需要stw,可以与其他线程一起运行,可能会导致已经标志的对象发生改变。
- 重新标记:重新标记并发标记阶段新产生的节点,这个阶段会stw,远远比并发标记时间短,主要采用三色标记法。
- 并发清理:开启用户线程,不会stw,同时gc线程开始对未标记的区域做清理,这个阶段如果有新增对象,会被标为黑色不做任何处理。
- 并发重置:重置本次gc过程中做的标记。
22,三色标记算法
在并发标记阶段,对象的状态可能随时会发生改变。gc在进行可达性分析时,用三色来标识对象的状态。
- 黑色:这个对象及其所有引用已被gc root遍历,黑色的对象不可被回收。
- 灰色:这个对象被gc root遍历过,但是中间可能又产生了新的节点,会在重新标记阶段遍历灰色节点。
- 白色:这个对象没有被gc root遍历过,在重新标记阶段如果这个节点还是白色的话会被回收。
23,垃圾收集器组合方案
不同的垃圾收集器可以组合使⽤,在使⽤时选择适合当前业务场景的组合。