几分钟了解下java虚拟机--02
<p><s>几分钟应该看不完,私密马赛, 俺是标题党</s><br>既然来了, 看看吧, 球球你了</p>
<h2 id="java类加载器">Java类加载器</h2>
<h3 id="类的生命周期和加载过程">类的生命周期和加载过程</h3>
<p><img src="https://img2024.cnblogs.com/blog/3642195/202506/3642195-20250617225104525-1376124242.png"></p>
<ul>
<li>加载 加载所有的.class文件/jar文件/网络流 →字节流 (JVM 与java.lang.classLoader协作)</li>
</ul>
<p>存储于Metaspace/Method Area</p>
<ul>
<li>校验 确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全</li>
<li>准备 设置变量默认值分配内存</li>
<li>解析 解析常量池符号引用转换为直接引用,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析 链接相关参数, 方法索引(为后续初始化和运行提供直接引用)</li>
<li>初始化 执行类构造器 <code><clinit></code> 方法 初始化</li>
</ul>
<hr>
<h3 id="类加载时机">类加载时机</h3>
📖
<ul>
<li>启动main方法所在类</li>
<li>new 类时</li>
<li>调用静态方法</li>
<li>访问静态对象</li>
<li>子类初始化 →父类初始化</li>
<li>类的初始化 →接口初始化</li>
<li>初次调用Methon Handle 方法</li>
</ul>
</aside>
📖
<ul>
<li>引用父类静态字段, 不触发子类初始化</li>
<li>定义对象数组,不触发对象初始化</li>
<li>常量的调用 ,不初始化</li>
<li>ClassLoader.loadClass 只加载不初始化</li>
</ul>
</aside>
<hr>
<h3 id="类加载机制">类加载机制</h3>
<p><img src="https://img2024.cnblogs.com/blog/3642195/202506/3642195-20250617225128997-1777830723.png"></p>
⚙
<ul>
<li>启动类加载器(bootstrap class loader): 加载java核心类(c++)JVM自带</li>
</ul>
<p>举例来说,java.lang.String 是由启动类加载器加载的,所以 String.class.getClassLoader() 就会返回 null</p>
<ul>
<li>扩展类加载器(extensions class loader): 加载JRE的扩展目录, 由启动类加载</li>
<li>应用类加载器(app class loader): 通过ClassLoader.getSystemClassLoader() 来获取应用类加载器。</li>
</ul>
<p>如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载</p>
</aside>
📖
<ul>
<li>双亲委托: 加载所需类时 懒加载, 委托父类加载相关类负责依赖</li>
<li>负责依赖: 加载所需类时, 加载依赖类与接口</li>
<li>缓存加载:类被加载后缓存</li>
</ul>
</aside>
<hr>
<h2 id="jvm方法调用">JVM方法调用</h2>
<p>方法于JVM构成 : 类名, 方法名, 方法描述符{参数类型, 返回参数类型}</p>
📖
<p>静态方法</p>
<ul>
<li><code>invokestatic</code>,顾名思义,这个指令用于调用某个类的静态方法,这也是方法调用指令中最快的一个。</li>
<li><code>invokespecial</code>, 我们已经学过了, <code>invokespecial</code> 指令用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。</li>
</ul>
<p>动态调用</p>
<ul>
<li><code>invokevirtual</code>,如果是具体类型的目标对象,<code>invokevirtual</code>用于调用公共,受保护和打包私有方法。</li>
<li><code>invokeinterface</code>,当要调用的方法属于某个接口时,将使用 <code>invokeinterface</code> 指令。</li>
</ul>
</aside>
<h3 id="jvm方法查询">JVM方法查询</h3>
<p><em>子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法<strong>Interface 同理</strong></em></p>
📖
<ol>
<li>在 C 中查找符合名字及描述符的方法。</li>
<li>如果没有找到,在 C 的父类中继续搜索,直至 Object 类。</li>
<li>如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。</li>
</ol>
</aside>
<h3 id="虚方法调用">虚方法调用</h3>
<ul>
<li>虚拟方法表 <em>链接时建立class的虚方法(非static, final)表</em></li>
</ul>
<p>分离<code>invokinterface</code> 与<code>invokevirtual</code>原因</p>
<pre><code class="language-java">class A
1: method1
2: method2
class B extends A
1: method1
2: method2
3: method3
</code></pre>
<pre><code class="language-java">class B extends A implements X
1: method1
2: method2
3: method3
4: methodX
class C implements X
1: methodC
2: methodX
</code></pre>
<hr>
<ul>
<li>
<p>内联缓存</p>
<pre><code> *-只是缓存并非内联(嵌入内部)*
</code></pre>
</li>
</ul>
<p><em>执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。</em></p>
💡
<ul>
<li>单态内联 <em>缓存了一种动态类型以及它所对应的目标方法</em></li>
<li>多态内联 <em>缓存多种….</em> 热门方法前调</li>
</ul>
</aside>
<hr>
<ul>
<li>劣化为超多态状态</li>
</ul>
💡
<p>当类型切换太频繁(超多态),缓存的维护成本(写开销)超过收益时,JVM 选择放弃缓存</p>
</aside>
<hr>
<h2 id="jvm处理异常">JVM处理异常</h2>
<h3 id="异常基本概念">异常基本概念</h3>
💡
<p>抛出异常</p>
<ul>
<li>显示抛出 : application层面</li>
<li>隐式抛出 : JVM层面</li>
</ul>
</aside>
💡
<p>异常捕获</p>
<ul>
<li>try 代码块 <em>用来标记需要进行异常监控的代码</em></li>
<li>catch 代码块</li>
</ul>
<p><em>try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器</em></p>
<ul>
<li>finally 代码块 <em>用来声明一段必定运行的代码</em></li>
</ul>
</aside>
<p>因为异常总是动态的, 实时的, 所以我们总是<code>new exception()</code></p>
<p><img src="https://img2024.cnblogs.com/blog/3642195/202506/3642195-20250617225211662-1547573488.png"></p>
💡
<ul>
<li>Error <em>它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机</em></li>
</ul>
<p>错误的运行影响到了全局代码</p>
<ul>
<li>Exception <em>涵盖程序可能需要捕获{try - catch}并且处理的异常</em></li>
<li>RuntimeException <em>局部的、可恢复的。通过适当的错误处理,程序可以继续运行</em></li>
</ul>
</aside>
<hr>
<p><img src="https://img2024.cnblogs.com/blog/3642195/202506/3642195-20250617225237890-1510932598.png"></p>
📌
<p>当前版本 Java 编译器的做法(<strong>变种1</strong>),<em>复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中</em></p>
</aside>
<hr>
<h3 id="如何捕获异常">如何捕获异常</h3>
<p>每个method都会维护一张 Exception table</p>
<pre><code class="language-java">Exception table:
fromto target type
0 3 6 Class java/lang/Exception
</code></pre>
⚙
<p>当程序触发异常时, 自上到下遍历异常表中的条目,若命中, 将PC指向target ,否则照常抛出异常</p>
</aside>
<h3 id="java-7-的-suppressed-异常以及语法糖"><strong>Java 7 的 Suppressed 异常以及语法糖</strong></h3>
📌
<p>语法糖</p>
<ul>
<li><strong>可读性强</strong>:让代码更接近人类语言,容易理解。</li>
<li><strong>非必需</strong>:去掉语法糖后,语言仍然能实现相同的功能,只是写法更复杂。</li>
<li><strong>编译器/解释器处理</strong>:语法糖通常在编译或解释时被转换为更基础的代码</li>
</ul>
</aside>
📖
<p>Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Suppressed 异常 以解决代码繁琐 👉</p>
</aside>
<pre><code class="language-java">try {
in0 = new FileInputStream(new File("in0.txt"));
...
try {
in1 = new FileInputStream(new File("in1.txt"));
...
try {
in2 = new FileInputStream(new File("in2.txt"));
...
} finally {
if (in2 != null) in2.close();
}
} finally {
if (in1 != null) in1.close();
}
} finally {
if (in0 != null) in0.close();
</code></pre>
<pre><code class="language-java">try (Foo foo0 = new Foo("Foo0"); // try-with-resources语法糖优化后
Foo foo1 = new Foo("Foo1"); // 该语法糖下自动使用suppressed异常
Foo foo2 = new Foo("Foo2")) {
throw new RuntimeException("Initial");
}
**suppressed异常**允许将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息
</code></pre>
<hr>
<h2 id="jvm实现反射机制">JVM实现反射机制</h2>
<p><em>依赖于 JVM 的类加载器和运行时数据结构(如方法表、字段表)</em></p>
<h3 id="怎么用">怎么用</h3>
📖
<p>通常来说,使用反射 API 的第一步便是获取 Class 对象。在 Java 中常见的有这么三种。</p>
<ol>
<li>使用静态方法 <code>Class.forName</code> 来获取。</li>
<li>调用对象的 <code>getClass()</code> 方法。</li>
<li>直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。int[].class</li>
</ol>
</aside>
📌
<ol>
<li>使用 <code>newInstance()</code> 来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。</li>
<li>使用 <code>isInstance(Object)</code> 来判断一个对象是否该类的实例,语法上等同于 <code>instanceof</code> 关键字(JIT 优化时会有差别,我会在本专栏的第二部分详细介绍)。</li>
<li>使用 <code>Array.newInstance(Class,int)</code> 来构造该类型的数组。</li>
<li>使用 <code>getFields()/getConstructors()/getMethods()</code> 来访问该类的成员。除了这三个之外,Class 类还提供了许多其他方法,详见 。需要注意的是,方法名中带 Declared 的不会返回父类的成员,但是会返回私有成员;而不带 Declared 的则相反。</li>
</ol>
</aside>
📌
<p>当获得了类成员之后,我们可以进一步做如下操作。</p>
<ul>
<li>使用 <code>Constructor/Field/Method.setAccessible(true)</code> 来绕开 Java 语言的访问限制。</li>
<li>使用 <code>Constructor.newInstance(Object[])</code> 来生成该类的实例。</li>
<li>使用 <code>Field.get/set(Object)</code> 来访问字段的值。</li>
<li>使用 <code>Method.invoke(Object, Object[])</code> 来调用方法。</li>
</ul>
</aside>
<h3 id="应用">应用</h3>
<ul>
<li>IDE <em>每当我们敲入点号时,IDE 便会根据点号前的内容,动态展示可以访问的字段或者方法</em></li>
<li>Java调试器 <em>在调试过程中枚举某一对象所有字段的值</em></li>
<li>Spring framework <em>IOC</em></li>
</ul>
<h3 id="methodinvoke">Method.invoke</h3>
<p><em>本地实现, 委派实现, 动态实现</em>(均由<code>MethodAccessor</code>抽象)</p>
<p><em>getMethod会形成一份class内方法的的拷贝</em> -避免在热点代码中使用getMethod</p>
<p>取消委派实现, 关闭检查时目标方法的权限可以小幅度提升性能</p>
<pre><code class="language-java">public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) throws ... {
... // 权限检查
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
}
</code></pre>
<ul>
<li>本地实现</li>
</ul>
<pre><code class="language-java">// v0 版本
import java.lang.reflect.Method;
public class Test {
public static void target(int i) {
new Exception("#" + i).printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> klass = Class.forName("Test");
Method method = klass.getMethod("target", int.class);
method.invoke(null, 0);
}
}
# 不同版本的输出略有不同,这里我使用了 Java 10。
$ java Test
java.lang.Exception: #0
at Test.target(Test.java:5)
**本地实现** at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
**↑** at java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
**委派实现** at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
**↑
invoke** at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at Test.main(Test.java:131
</code></pre>
<ul>
<li>委派实现</li>
</ul>
<p>作为invoke实现的中间件</p>
<p>选择method invoke本地实现还是动态实现</p>
<ul>
<li>动态实现(纯java字节码, 无需重新从java→c++→java)</li>
</ul>
<p>优势在于避免了 JNI <em>( java native interface )</em>的切换开销,但它的缺点是生成字节码耗时</p>
<pre><code class="language-java">// 动态实现的伪代码,这里只列举了关键的调用逻辑,其实它还包括调用者检测、参数检测的字节码。
package jdk.internal.reflect;
public class GeneratedMethodAccessor1 extends ... {
@Overrides
public Object invoke(Object obj, Object[] args) throws ... {
Test.target((int) args);
return null;
}
}
</code></pre>
💡
<p>当本地实现某一方法的次数大于<code>Dsun.reflect.inflationThreshold</code> 时</p>
<p>JVM虚拟机开始对反射method以java字节码形成动态实现</p>
</aside>
<pre><code class="language-java">Method method1 = Test.class.getMethod("target", int.class);
Method method2 = Test.class.getMethod("target", int.class);
</code></pre>
📖
<p>每次get都会创建一个新的method实例</p>
<p>即使访问的方法相同</p>
<p>method1≠method2</p>
</aside>
<h2 id="jvm实现invokedynamic">JVM实现invokedynamic</h2>
<h3 id="方法句柄method-handle">方法句柄(Method Handle)</h3>
💡
<ul>
<li>通过<code>MethodHandles.Lookup</code> 类完成</li>
</ul>
<p><em>它提供了多个 API,既可以使用反射 API 中的 Method 来查找,也可以根据类、方法名以及方法调用类型来查找</em></p>
<ul>
<li><code>Lookup.findStatic</code> , <code>Lookup.findVirtual</code> , <code>Lookup.findSpecial</code></li>
<li>方法句柄的类型(MethodType)仅由Method的参数类型和返回类型决定</li>
</ul>
</aside>
<pre><code class="language-java">class Foo {
private static void bar(Object o) {
..
}
public static Lookup lookup() {
return MethodHandles.lookup();
}
}
// 获取方法句柄的不同方式
MethodHandles.Lookup l = Foo.lookup(); // 具备 Foo 类的访问权限
Method m = Foo.class.getDeclaredMethod("bar", Object.class);
MethodHandle mh0 = l.unreflect(m);
MethodType t = MethodType.methodType(void.class, Object.class);
MethodHandle mh1 = l.findStatic(Foo.class, "bar", t);
</code></pre>
⚙
<ul>
<li>方法句柄同样会检查权限, 但相比于反射, 其检查权限是在创建阶段完成(不需要每次使用时重复检查)</li>
<li>方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置</li>
</ul>
</aside>
<h3 id="方法句柄的操作">方法句柄的操作</h3>
📌
<p>严格匹配传入参数类型<code>invokeExact</code></p>
<p>只接受相同类型 (Object)String →Object</p>
<p>❌String →Object</p>
<p><code>@PolymorphicSignatur</code></p>
<p>实现签名多态性</p>
<p>可根据传入参数不同自动创建不同的方法句柄</p>
</aside>
📌
<p>如果你需要自动适配参数类型,那么你可以选取方法句柄的第二种调用方式 invoke。它同样是一个签名多态性的方法。invoke 会调用 <code>MethodHandle.asType</code> 方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值同样也会先进行适配,然后再返回给调用者。</p>
</aside>
📖
<p>方法句柄还支持增删改参数的操作,这些操作都是通过生成另一个方法句柄来实现的。这其中,改操作就是刚刚介绍的 <code>MethodHandle.asType</code> 方法。删操作指的是将传入的部分参数就地抛弃,再调用另一个方法句柄。它对应的 API 是 <code>MethodHandles.dropArguments</code> 方法。</p>
<p>增操作则非常有意思。它会往传入的参数中插入额外的参数,再调用另一个方法句柄,它对应的 API 是 <code>MethodHandle.bindTo</code> 方法。Java 8 中捕获类型的 Lambda 表达式便是用这种操作来实现的,下一篇我会详细进行解释。</p>
<p>增操作还可以用来实现方法的柯里化 。举个例子,有一个指向 f(x, y) 的方法句柄,我们可以通过将 x 绑定为 4,生成另一个方法句柄 g(y) = f(4, y)。在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。</p>
</aside>
<hr>
<p>如果你看完了, 非常感谢你对我付出的认可🥰🥰🥰😘😘😘</p><br><br>
来源:https://www.cnblogs.com/many-bucket/p/18933753
頁:
[1]