JVM八股文学习
JVM八股文学习
本篇文章转自Gakkiyomi的博客
作者: Gakkiyomi
原文地址: https://fangcong.ink/articles/2021/04/17/1618650579784.html
内存分配和回收策略
Minor GC/Major GC /Full GC
•Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。•Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。•在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。
对象优先在 eden 区分配
Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。大对象直接进入老年代
byte[1024 * 1204 * 1024]。Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。-XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。长期存活的对象进入老年代
survivor 区,并且对象年龄设为 1,之后对象每熬过一次 Minor GC,对象年龄加 1,当年龄增长到一定程度后(默认 15),就会晋升到老年代中。设置阈值:-XXMaxTenuringThreshold=15。-XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。动态对象年龄判定
Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立,Minor GC 可以确保是安全的;如果不成立,则虚拟机会查看 HandlePromotionFailure值是否设置为允许担保失败, 如果是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那此时也要改为进行一次 Full GC。
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
HandlePromotionFailure 这个参数不会在影响空间分配担保策略了。JVM 触发 Full GC 的情况
1.我们手动调用 system.gc() 方法,但这也不一定会执行 Full GC,只会增加 Full GC 的执行频率我们可以通过 -XX:DisableExpliitGC 来禁止调用 Full GC。2.老年代空间不足,当老年代空间不足后会触发 Full GC,若触发后仍然不足,则会抛出 java.lang.OutOfMemoryError:Java heap space。3.方法区(永久代)空间不足,方法区存放一些类信息,常量,和静态变量等数据,若系统加载的类,反射的类和调用的方法较多的时候,永久代可能被占满,则触发 Full GC,若经过 Full GC 仍然回收不了,则会抛出 java.lang.OutOfMemoryError:PemGen space。4.统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的空间,则会触发 Full GC。5.CMS GC 时出现 promotion failed 和 concurrent mode failure ,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
垃圾收集器

(A)图中展示了 7 种不同分代的收集器:
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
(B)而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:
新生代收集器:
Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
(C)两个收集器间有连线,表明它们可以搭配使用:


class 类文件结构

constant_pool 常量池。final 声明的常量值等。而符号引用则属于编译原理方面的概念,包括了下面 3 类常量:•类和接口的全限定名•字段的名称和描述符•方法的名称和描述符
类加载
类加载过程

第一步:Loading 加载
1.通过类的全限定名(包名 + 类名),获取到该类的二进制字节流
2.将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构
3.在 内存 中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
第二步:Linking 链接
链接是指将上面创建好的 class 类合并至 Java 虚拟机中,使之能够执行的过程,可分为 验证、准备、解析 三个阶段。
•① 验证(Verify)
确保 class 文件中的字节流包含的信息符合当前虚拟机的要求,保证这个被加载的 class 类的正确性,不会危害到虚拟机的安全。1.文件格式验证
首先要验证字节流是否符合 class 文件的格式规范,并且能被当前版本的虚拟机处理。这一阶段包括:•是否已魔数 0xCAFEBABE 开头•主,次版本号是否在当前虚拟机的处理范围之内•常量池中的常量是否存在不被支持的常量类型(检查常量 flag 标志)•指向常量池的各种索引值是否有指向不存在的常量或不符合类型的常量•CONSTANT_Utf8_info 型常量是否有不符合 utf8 编码的数据•Class 文件中的各个部分以及文件本身是否有被删除的或者附加的其他信息•……2.元数据验证
对字节码描述的信息做语义分析,对类的元数据信息做语义校验,保证不存在不符合 Java 语言规范的元数据信息。3.字节码验证
通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的,这个阶段会对方法体进行校验分析,保证这个方法在运行时不会做出危害虚拟机的安全事件。4.符号引用验证
最后一个阶段的校验发生在虚拟机将符号引用转换成直接引用的时候,这个转换动作将在连接的第三阶段--解析阶段中发生。此阶段可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配行校验。•② 准备(Prepare)
为类中的 静态字段 分配内存,并设置默认的初始值,比如 int 类型初始值是 0。被 final 修饰的 static 字段不会设置,因为 final 在编译的时候就分配了。•③ 解析(Resolve)
解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程(将常量池内的符号引用解析成为实际引用)。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
事实上,解析器操作往往会伴随着 JVM 在执行完初始化之后再执行。符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java 虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。
第三步:initialization 初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对 static 修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
类加载机制
类加载器
1.启动类加载器(Bootstrap ClassLoader):由 C++ 语言实现。负责加载 JAVA_HOME\lib 目录中并且能被虚拟机识别的类库到 JVM 内存中,如果名称不符合的类库即使放在 lib 目录中也不会被加载。该类加载器无法被 Java 程序直接引用。2.扩展类加载器(Extension ClassLoader):该加载器主要是负责加载 JAVA_HOME\lib\ext,该加载器可以被开发者直接使用。3.应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派机制

破坏双亲委派
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的 API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?
这并非是不可能的事情,一个典型的例子便是 JNDI 服务,JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进去的 rt.jar),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码啊!那该怎么办?
为了解决这个问题,Java 设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
JDBC 中的逆双亲委派机制源码分析
try{//加载MySql的驱动类Class.forName("com.mysql.jdbc.Driver") ;}catch(ClassNotFoundException e){System.out.println("找不到驱动程序类 ,加载驱动失败!");e.printStackTrace() ;}String url = "jdbc:mysql://localhost:3306/test" ;String username = "root" ;String password = "root" ;try{Connection con = DriverManager.getConnection(url , username , password ) ;}catch(SQLException se){System.out.println("数据库连接失败!");se.printStackTrace() ;}
以上就是 JDBC 连接数据并获取连接的代码,那调用这些的方法到底做了些什么呢?
首先,我们用 Class.forName("com.mysql.jdbc.Driver") 加载了驱动,Driver 的源码很简单,如下:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}}
com.mysql.jdbc.Driver,类初始化的时候执行静态代码块,静态代码块中将 new 了一个 Driver 实例并将他注册到 DriverManager 中。注意,这里的 Driver 实例的类加载器是系统类加载器。 接下来,我们调用了 DriverManager.getConnection(String url,String user, String password),其源码如下:@CallerSensitivepublic static Connection getConnection(String url,String user, String password) throws SQLException {java.util.Properties info = new java.util.Properties();if (user != null) {info.put("user", user);}if (password != null) {info.put("password", password);}return (getConnection(url, info, Reflection.getCallerClass()));}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {/** 这里要确保类加载不能是BootstrapClassLoader,* 因为BootstrapClassLoader不能加载到用户类库(JDBC驱动为用户类库)*/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 + "\")");// Walk through the loaded registeredDrivers attempting to make a connection.// Remember the first exception that gets raised so we can reraise it.SQLException reason = null;for(DriverInfo aDriver : registeredDrivers) {// If the caller does not have permission to load the driver then// skip it.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 we got here nobody could connect.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");}
ClassLoaderprivate static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {boolean result = false;if(driver != null) {Class<?> aClass = null;try {aClass = Class.forName(driver.getClass().getName(), true, classLoader);} catch (Exception ex) {result = false;}//类加载器与类相同才能确定==result = ( aClass == driver.getClass() ) ? true : false;}return result;}
排查 OOM

欢迎关注我的公众号“须弥零一”,原创技术文章第一时间推送。
