1,一个类被加载进jvm中要经历哪几个过程
加载:通过io的方式将class文件读入到我们的jvm中。
校验:通过校验字节码文件头4位16进是否是以cafe babe开头。 目的是为了检查此文件是否java编写。
准备:将类中的静态属性赋初始值。
解析:将符号引用转换成直接引用。(在刚开始时,假如一个类引用了另一个类,这时jvm并不不知道这人引用的地址,会先用一个唯一的符号代替,这时就叫符号引用,通过解析,通过这个符号找到另一个类的地址,把这个符号替换成这个引用。
初始化:将类中的静态部分赋指定的值,并执行静态代码块。
2,类加载器
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,全盘委托机制
/**
重写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(线程共享):存放元信息,类的模板,常量池,静态部分
线程栈(线程独享):本地方法产生的数据
本地方法区(线程独享):本地方法产生的数据
程序计数器(线程独享):配合执行引擎执行指令
- 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 位)
- 对象头中的类型指针
- 指针压缩
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,标记清除算法。
首先标记出所有需要被回收的对象,在标记完成之后再统一回收。
标记和清除的效率都不高。
空间问题,清除后产生大量不连续的内存随便。如果有大对象会出现空间不够的现象从而不得不提
前触发另一次垃圾收集动作。
解决了内存碎片问题。
缺点:
将原来的内存缩小为原来的一半,存活对象越多效率越低。
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的含义就是老年代的空间不足时做一次整体的清理,从新生代到老年代,整个堆进行清理。
总结:
- 大对象直接进入到老年代中:大对象可以通过参数设置大小,多大的对象被认为是大对象,-xx:PretenureSizeThreshold
- 当对象的年龄到达15岁时将进入到老年代,这个年龄也可以通过参数设置:XX-MaxTenuringThreshold.
- 根据对象动态的年龄判断,如果s区中的对象总和超过了s区中的50%,那么下一次复制的时候,把年龄大于等于这次年龄的对象都一次性全部放入到老年代中。
-
⽼年代空间分配担保机制 :在 minor gc 时,检查⽼年代剩余可⽤空间是否⼤于年轻代⾥现有的所有对象(包含垃圾)。如果⼤于等于,则做 minor gc 。如果⼩于,看下是否配置了担保参数的配置: -XX: -HandlePromotionFailure ,如果配置了,那么判断⽼年代剩余的空间是否⼩于历史每次 minor gc 后进⼊⽼年代的对象的平均⼤⼩。如果是,则直接full gc ,减少⼀次 minor gc 。如果不是,执⾏ minor gc 。如果没有担保机制,直接 fullgc 。
----------------------------------------------------垃圾回收器-------------------------------------------------------------
18,Serial收集器(-XX:+UseSerialGC - XX:+UseSerialOldGC)
19,Parallel收集器(-XX:+UseParallelGC,- XX:+UseParallelOldGC)
20,ParNew收集器(-XX:+UseParNewGC)
21,CMS收集器(-XX:+UseConcMarkSweepGC)
- 初始标记:暂停所有的其他线程(STW),并记录gc roots直接能引用的对象。
- 并发标记:从gc roots的直接关联开始向下查找,这个过程比较长,但是不需要stw,可以与其他线程一起运行,可能会导致已经标志的对象发生改变。
- 重新标记:重新标记并发标记阶段新产生的节点,这个阶段会stw,远远比并发标记时间短,主要采用三色标记法。
- 并发清理:开启用户线程,不会stw,同时gc线程开始对未标记的区域做清理,这个阶段如果有新增对象,会被标为黑色不做任何处理。
- 并发重置:重置本次gc过程中做的标记。
22,三色标记算法
在并发标记阶段,对象的状态可能随时会发生改变。gc在进行可达性分析时,用三色来标识对象的状态。
- 黑色:这个对象及其所有引用已被gc root遍历,黑色的对象不可被回收。
- 灰色:这个对象被gc root遍历过,但是中间可能又产生了新的节点,会在重新标记阶段遍历灰色节点。
- 白色:这个对象没有被gc root遍历过,在重新标记阶段如果这个节点还是白色的话会被回收。
23,垃圾收集器组合方案