☁️ 对于一个类来说,它的生命周期如下图所示:
☁️ 从上图可以看出,类加载可以分为以下几个步骤:
⚡️ 加载:“加载” 是 “类加载” 过程中的一个阶段;加载主要工作是 通过一个类的全限定名来获取定义此类的二进制字节流,接着将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,最后在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
⚡️ 连接:验证 ——> 准备 ——> 解析
🌴 验证: 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全;
🌴 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段;
🌴 解析: 解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程;
⚡️ 初始化:初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶 段就是执行类构造器方法的过程;
☁️ 什么是双亲委派模型?
⚡️ 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载;
☁️ 双亲委派模型的优点
⚡️ 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了;
⚡️ 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自 己提供的因此安全性就不能得到保证了;
☁️ 破坏双亲委派模型
⚡️ 双亲委派模型虽然有其优点,但在某些情况下也存在一定的问题,比如 Java 中 SPI(Service Provider Interface,服务提供接口)机制中的 JDBC 实现;
⚡️ JDBC 的 Driver 接口定义在 JDK 中,其实现由各个数据库的服务商来提供,比如 MySQL 驱动包,我们先来看下 JDBC 的核心使用代码:
public class JdbcTest {
public static void main(String[] args){
Connection connection = null;
try {
connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root",
"awakeyo");
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println(connection.getClass().getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Connection.class.getClassLoader());
}
}
☁️ 然后我们进入 DriverManager 的源码类就会发现它是存在系统的 rt.jar 中的;
☁️ 由双亲委派模型的加载流程可知 rt.jar 是有顶级父类 Bootstrap ClassLoader 加载的;
☁️ 而当我们进入它的 getConnection 源码是却发现,它在调用具体的类实现时,使用的是子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader )来加载具体的数据库数据库包(如mysql 的 jar 包),源码如下:
@CallerSensitive
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws
SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
//获取线程上下为类加载器
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// isDriverAllowed 对于 mysql 连接 jar 进行加载
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " +
aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " +
aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
☁️ 这样就破坏了双亲委派模型了(双亲委 派模型讲的是所有类都应该交给父类来加载,但 JDBC 显然并不能这样实现);
☁️ Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还 存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法:
☁️ 在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收;
☁️ 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就 -1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死";
☁️ 但是,在主流的 JVM 中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题;
☁️ 观察循环引用问题:
/**
* JVM参数 :-XX:+PrintGC
* @author 38134
*
*/
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
☁️ 从下面日志结果可以看出,GC日志包含“ 6092K->856K(125952K) ”,意味着虚拟机并没有因为这两个对象互相引用就不回收他们,即JVM并不使用引用计数法来判断对象是否存活;
[GC (System.gc()) 6092K->856K(125952K), 0.0007504 secs]
☁️ 在上面我们讲了,Java 并不采用引用计数法来判断对象是否已 “死” ,而采用 " 可达性分析 " 来判断对象是否存活 ( 同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言 ) ;
☁️ 此算法的核心思想为 : 通过一系列称为 " GC Roots " 的对象作为起始点,从这些节点开始向下搜索,搜索 走过的路径称之为 " 引用链 " ,当一个对象到 GC Roots 没有任何的引用链相连时 ( 从 GC Roots 到这个对象不可达 ) 时,证明此对象是不可用的;
☁️ 在 Java 语言中,可作为GC Roots的对象包含下面几种:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象;
2、方法区中类静态属性引用的对象;
3、方法区中常量引用的对象;
4、本地方法栈中JNI引用的对象;
☁️ 引用可以分为四类:1、强引用;2、软引用;3、弱引用;4、虚引用;
⚡️ 强引用: 强引用指的是在程序代码之中普遍存在的,类似于 " Object obj = new Object() " 这类 的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例;
⚡️ 软引用: 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用;
⚡️ 弱引用: 弱引用也是用来描述非必需对象的,但是它的强度要弱于软引用,被弱引用关联的对象只能生存到下一次垃圾回收发生之前;当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象;在JDK1.2之后提供了 WeakReference 类来实现弱引用;
⚡️ 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系;一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用;
☁️ "标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象;后续的收集算法都是基于这种思路并对其不足加以改进而已;
☁️ “ 标记-清除 ”算法的不足主要有两个:
1、效率问题:标记和清除这两个过程效率都不高;
2、空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集;
☁️ " 复制 " 算法是为了解决 " 标记-清理 " 的效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;
☁️ 当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉;
☁️ 这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效;
☁️ 新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间;
☁️ HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden : Survivor From : Survivor To = 8:1:1;
☁️ HotSpot实现的复制算法流程如下:
⚡️ 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到 Survivor From 区;当 Eden 区再次触发 Minor gc 的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将 Eden 和 From 区域清空;
⚡️ 当后续 Eden 又发生 Minor gc 的时候,会对 Eden 和 To 区域进行垃圾回收,存活的对象复制到 From区域,并将 Eden 和 To 区域清空;
⚡️ 部分对象会在 From 和 To 区域中复制来复制去,如此交换15次(由 JVM 参数MaxTenuringThreshold 决定,这个参数默认是15),最终如果还是存活,就存入到老年代;
☁️ 复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低,因此在老年代一般不能使用复制算法;
☁️ 针对老年代的特点,提出了一种称之为 " 标记-整理算法 " ,标记过程仍与 " 标记-清除 " 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存;
☁️ 分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收;
☁️ 在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用 " 标记-清理 " 或者 " 标记-整理 " 算法;
☁️ 请问了解 Minor GC 和 Full GC 么,这两种GC有什么不一样吗?
⚡️ Minor GC 又称为新生代GC:指的是发生在新生代的垃圾收集,因为Java对象大多都具备朝生 夕灭的特性,因此 Minor GC (采用复制算法) 非常频繁,一般回收速度也比较快;
⚡️ Full GC 又称为老年代 GC 或者 Major GC:指发生在老年代的垃圾收集,出现了 Major GC,经 常会伴随至少一次的 Minor GC (并非绝对,在 Parallel Scavenge 收集器中就有直接进行 Full GC 的策略选择过程),Major GC 的速度一般会比Minor GC慢10倍以上;
☁️ 垃圾收集器就是内存回收的具体实现;
吞吐量=运行用户代码时间 /(运行用户代码时间 / 垃圾收集时间)
☁️ 特性: CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器;
☁️ CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整 个过程分为4个步骤:
⚡️ 初始标记(CMS initial mark)初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World” ;
⚡️ 并发标记(CMS concurrent mark)并发标记阶段就是进行GC Roots Tracing的过程;
⚡️ 重新标记(CMS remark) 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分 对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的 时间短,仍然需要 “ Stop The World ”;
⚡️ 并发清除(CMS concurrent sweep)并发清除阶段会清除对象;
☁️ 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的;
☁️ CMS收集器的优缺点:
⚡️ 优点:
1、并发收集、低停顿;
⚡️ 缺点:
1、CMS收集器对CPU资源非常敏感
2、CMS收集器无法处理浮动垃圾
3、CMS收集器会产生大量空间碎片
☁️ G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region 块,然后并行的对其进行垃圾回收;
☁️ G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩;
☁️ G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收;