首页 > 编程语言 >还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听

时间:2022-10-21 17:31:07浏览次数:72  
标签:字节 JDK 代理 class 源码 proxy new com public


一、前言

 

Spring中的AOP思想就是对代理模式的经典运用,下面先讲讲代理模式的核心思想,以静态代理为例。


二、静态代理示例

下面有这样一个例子,委托人在遭遇利益受损的时候,可以委托律师帮忙打官司。

先定义一个描述行为的接口:

package com.design.proxy.statics;

public interface Action {
void handle();
}

委托人,实现这个接口,主要的行为是寻找律师。

package com.design.proxy.statics;

/**
* 委托人
*/
public class Client implements Action {
@Override
public void handle() {
System.out.println("委托人:我要找律师,帮我打官司");
}
}

律师类,同样实现这个接口,并且持有一个委托人的引用,可以帮助委托人打官司。

package com.design.proxy.statics;

/**
* 律师
*/
public class Lawyer implements Action {

private Action action;

public Lawyer(Action action) {
this.action = action;
}

@Override
public void handle() {
action.handle();
System.out.println("律师:帮助我的委托人打官司");
}
}

Main类,用来描述打官司的过程

package com.design.proxy.statics;

public class Main {
public static void main(String[] args) {
Action client = new Client();
Action lawyer = new Lawyer(client);
lawyer.handle();
}
}

输出如下:

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_java

这就是代理模式的一个运用,这里的委托人显然是被代理对象,那么律师就是代理对象,律师代理委托人进行打官司。


三、代理模式的类图

代理模式的类图如下:

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_代理类_02

  • Subject              抽象主题对象,是代理对象以及被代理对象都需要实现的接口
  • Proxy                 被代理对象,持有被代理对象的一个引用,控制被代理对象的行为以及访问
  • RealSubject      被代理对象,定义具体的逻辑实现。

代理又分为静态代理与动态代理,在上个例子中,就是一个典型的静态代理。

静态代理与动态代理的区别就在于,静态代理的.class文件在程序运行前就已经存在了,而动态代理的.class文件则是动态生成的。

在静态代理中,静态代理对象=实现一个与被代理对象相同的接口+增强方法。


四、动态代理

在上一节已经说过,动态代理是在程序运行时,动态生成代理类。动态代理又分为JDK动态代理与CGLIB动态代理。

JDK动态代理

我们现在在Action类中增加更多的行为,代表委托人的需求。

package com.design.proxy.statics;

public interface Action {

/**
* 诉讼
*/
void litigation();

/**
* 咨询
*/
void consult();

//...
}

那么委托人:

package com.design.proxy.statics;

/**
* 委托人
*/
public class Client implements Action {

@Override
public void litigation() {
System.out.println("委托人:我有诉讼的需求");
}

@Override
public void consult() {
System.out.println("委托人:我有咨询的需求");
}
}

在动态代理中,就不需要一个真实的代理对象了,代理对象是在运行时动态生成的。

JDK提供了一种思路,实现InvocationHandler接口,并通过Proxy.newProxyInstance获取动态生成的代理对象。

package com.design.proxy.dynamic.jdk;

import com.design.proxy.statics.Action;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LawyerHandler implements InvocationHandler {
private Action action;

//绑定被代理对象,最后返回代理对象的实例
public Action bind(Action action) {
this.action = action;
return (Action) Proxy.newProxyInstance(
action.getClass().getClassLoader(),
action.getClass().getInterfaces(),
this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//调用被代理对象的方法
Object o = method.invoke(action, args);
//增强方法
System.out.println("律师处理委托人的需求");
return o;
}
}

Main类:

package com.design.proxy.dynamic.jdk;

import com.design.proxy.statics.Action;
import com.design.proxy.statics.Client;

public class Main {
public static void main(String[] args) {
Action action = new LawyerHandler().bind(new Client());
action.litigation();
action.consult();
}
}

输出如下:

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_代理类_03

动态代理的核心,在于怎么获取动态代理对象,也就是怎么去获取动态代理对象的class对象,而要获取一个类的class对象,需要类加载器与一个.class文件,类加载器由action.getClass().getClassLoader()提供。

对类加载器不熟悉的同学,可以参考我的另外一篇文章​​深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?​

.class文件由java类编译形成,也就是说现在需要一个动态代理类的java文件。由静态代理的类结构可以得出,这个动态代理类同样需要实现与被代理类相同的接口,这个接口由action.getClass().getInterfaces()提供。

动态代理类同样需要持有被代理对象的引用,方便在增强方法中调用被代理对象的方法,这个(增强方法+被代理对象的引用+调用被代理对象方法)的组合,由InvocationHandler中的invoke方法提供。内部的method.invoke(action, args)方法,就是去调用被代理对象(action)的方法(method),方法参数为args。

如果我们使用静态代理来处理上面这个例子,则需要新建律师类,实现Action接口,实现接口中所有的方法,并在这些方法中去做同样的增强逻辑,代码冗余度高。如果有很多类需要代理,也或造成代理类的膨胀。


CGLIB动态代理

JDK动态代理,需要被代理类实现某个接口,如果该类没有实现接口,则无法使用JDK动态代理,此时可以使用CGLIB动态代理。

CGLIB是一个第三方实现的动态代理,需要引入cglib与asm的jar包,这个去maven仓库一搜就能搜到。

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_spring_04

我们重新改造委托人这个类,添加一个final方法

package com.design.proxy.dynamic.cglib;

/**
* 委托人
*/
public class Client {

public void litigation() {
System.out.println("委托人:我有诉讼的需求");
}

public void consult() {
System.out.println("委托人:我有咨询的需求");
}

public final void renting() {
System.out.println("委托人:我有租房的需求");
}
}

CGLIB同样需要我们实现MethodInterceptor类

package com.design.proxy.dynamic.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class LawyerInterceptor implements MethodInterceptor {

private Object object;

public Object getInstance(Object object) {
this.object = object;
//Cglib中的加强器,用来创建动态代理
Enhancer enhancer = new Enhancer();
//设置代理类的父类
enhancer.setSuperclass(this.object.getClass());
//设置回调,这里相当于是对于代理类上所有方法的调用,都会调用Callback,而Callback则需要实现intercept()方法进行拦截
enhancer.setCallback(this);
return enhancer.create();
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object object = proxy.invokeSuper(obj, args);
System.out.println("律师处理委托人的需求");
return object;
}
}

Main类:

package com.design.proxy.dynamic.cglib;

public class Main {
public static void main(String[] args) {
Client client = (Client) new LawyerInterceptor().getInstance(new Client());
client.litigation();
client.consult();
client.renting();
}
}

输出得到:

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_jdk动态代理_05

CGLIB动态代理,依据被代理类生成其对应的子类,子类就是代理类,即在子类中覆盖所有可被继承的方法,并在重写方法中对被代理类进行增强。

委托人这个类中,由于renting方法是个final方法,无法被子类继承,因此也无法被增强。

CGLIB动态代理最为核心的地方在于Enhancer,使用setSuperclass指明被代理类继承什么类,当调用父类中的方法时,将会触发回调,回调将会触发MethodInterceptor 中的intercept方法,在intercept方法中保存了被增强方法调用的时机。


五、JDK动态代理源码分析

一切还要从Proxy.newProxyInstance说起

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
//invocationHandler起码不能为空吧,为空还怎么玩
Objects.requireNonNull(h);
//克隆出被代理类实现的接口,接下来的权限验证与生成代理类的class对象都需要用到它
final Class<?>[] intfs = interfaces.clone();
//权限验证,不是重点
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

//生成代理类的class对象
Class<?> cl = getProxyClass0(loader, intfs);

try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

//获取代理类中参数类型为InvocationHandler的构造器对象
//constructorParams = { InvocationHandler.class };
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
//如果代理类的构造器不是public,就手动设置为可访问
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//反射获取代理类的实例对象,入参为invocationHandler
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

整体逻辑很清楚:

  1. getProxyClass0,获取代理类的class对象
  2. getConstructor,获取上述class对象的构造器
  3. newInstance,获取代理类的实例对象

核心方法是getProxyClass0

private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
//被代理类实现的接口数量不得超过65535
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}

这里有个疑问,被代理类在由.java转化为.class时,就会经过对接口数量的检验(class文件中只使用2个字节来表示实现的接口数量,多了也放不下)

既然在转化阶段,就经过一次检验,那么为什么这里还要做一次检验呢?实在是想不通,难道会有人去改class文件吗?姑且认为这里是double check吧。

通过上面的注释,可以了解到:如果要求的代理类存在,则直接从缓存中获取,否则通过ProxyClassFactory来创建。

WeakCache类中使用双层map作为代理类的缓存,代码也很意思,但限于篇幅,这次先略过,有兴趣的可以去研究。

缓存中最终会调用ProxyClassFactory的apply方法:

//所有生成的代理类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";

//使用自增原子类的值来作为代理类名的后缀,以确保唯一
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {

//1.检查使用指定的类加载器是否可以将此接口解析成同样的class对象
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}

//2.判断上述解析成的class对象是否是接口
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}

//3.验证接口是否重复
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}

//生成的代理类所在的包
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

//非public修饰的接口,这些接口需要在同一个包下,并取该包为代理类所在的包
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

//说明都是public接口,直接取默认包,即com.sun.proxy.
if (proxyPkg == null) {
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

//代理类的序号
long num = nextUniqueNumber.getAndIncrement();
//代理类的全限定名,例如com.sun.proxy.$Proxy0
String proxyName = proxyPkg + proxyClassNamePrefix + num;

//生成代理类的字节码文件,并转化为字节数组,和ClassLoader的findClass方法有点像
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
//将字节数组转化为class对象
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
}

apply方法主要做了一件事,就是确定生成的代理类的全限定名

首先确定包名:

  • 如果接口数组中有非public接口存在,那么这些接口都应该在同一个包下,并取该包作为代理类的包。
  • 如果接口都是public类型的,直接取默认的包名,即com.sun.proxy

接着确定类名,前缀是$Proxy,后缀使用自增的数字,从0开始。

现在我们拿着全限定名为com.sun.proxy.$Proxy0进入到ProxyGenerator.generateProxyClass方法中:

(直接点进去的话,是idea反编译的文件,可读性不是很好。建议去​​openjdk8: OpenJDK官方源码。​​上下载openjdk8的源码,该类的路径为openjdk\jdk\src\share\classes\sun\misc\ProxyGenerator.java)

public static byte[] generateProxyClass(final String name,
Class<?>[] interfaces,
int accessFlags)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
//动态生成代理类,并转化成字节数组
final byte[] classFile = gen.generateClassFile();
//saveGeneratedFiles默认为false,之后我们会手动开启
if (saveGeneratedFiles) {
//将代理类写入到文件中
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
try {
//name为com.sun.proxy.$Proxy0
int i = name.lastIndexOf('.');
Path path;
if (i > 0) {
//com\sun\proxy
Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar)); //创建该文件夹
Files.createDirectories(dir);
//com\sun\proxy\$Proxy0.class
path = dir.resolve(name.substring(i+1, name.length()) + ".class");
} else {
path = Paths.get(name + ".class");
}
//将代理类的字节数组写入到文件中
Files.write(path, classFile);
return null;
} catch (IOException e) {
throw new InternalError(
"I/O exception saving generated file: " + e);
}
}
});
}

return classFile;
}

通过generateClassFile生成代理类的class对象并转化为字节数组,如果此时saveGeneratedFiles为true,会将代理类的字节码保存在com\sun\proxy\$Proxy0.class文件中,我们可以使用以下代码,使得saveGeneratedFiles为true。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

接着进入到generateClassFile中:

private byte[] generateClassFile() {

//为hashCode、equals、toString生成代理方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

//为接口中的方法生成代理方法
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}

//具有相同签名的方法,验证他们的返回类型是否兼容
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}

try {
//生成代理类的构造函数
methods.add(generateConstructor());

for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
//有多少个代理方法,就生成多少个被private static Method修饰的字段
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
//生成代理方法
methods.add(pm.generateMethod());
}
}
//生成静态代码块,为Method类型的字段执行初始化
methods.add(generateStaticInitializer());

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
//对方法以及字段数量的检查
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}

//为类索引、超类索引与接口表索引预留位置
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}

//常量池仅可读
cp.setReadOnly();

ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

try {
//写入魔数,占用4个字节
dout.writeInt(0xCAFEBABE);

//写入副版本号,占用2个字节
dout.writeShort(CLASSFILE_MINOR_VERSION);

//写入主版本号,占用2个字节
dout.writeShort(CLASSFILE_MAJOR_VERSION);

//写入常量池
cp.write(dout);

//写入类访问标记,占用2个字节
dout.writeShort(accessFlags);

//写入类索引,占用2个字节
dout.writeShort(cp.getClass(dotToSlash(className)));

//写入超类索引,占用2个字节
dout.writeShort(cp.getClass(superclassName));

//写入接口计数,占用2个字节
dout.writeShort(interfaces.length);

//写入接口表索引,一个接口占用2个字节
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}

//写入字段计数,占用2个字节
dout.writeShort(fields.size());

//写入每一个字段,这里包含字段的访问标记、名称索引、描述符索引、属性计数与属性表
for (FieldInfo f : fields) {
f.write(dout);
}

//写入方法计数,占用2个字节
dout.writeShort(methods.size());

//写入方法信息,包括方法计数、访问标记、名称索引、描述符索引、属性表,其中属性表也是包含属性计数与属性集合
for (MethodInfo m : methods) {
m.write(dout);
}

//写入类文件属性计数,占用2个字节,不过代理类的文件属性个数为0
dout.writeShort(0);

} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}

return bout.toByteArray();
}

generateClassFile用于生成代理类的字节码,直接一个一个bit写入到class文件中。

这里涉及到class文件的结构,有兴趣的同学可以参考我的另外三篇文章

​class文件结构1——魔数、版本号、常量池与类访问标记​

​class文件结构2——类索引、超类索引与接口表索引​

​class文件结构3——字段表与方法表​

那么,生成的代理类到底是什么样的呢?

在上文也提到过,我们只要加一行方法就行了:

public static void main(String[] args) {

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

Action action = new LawyerHandler().bind(new Client());
action.litigation();
action.consult();
}

会在该区域生成$Proxy0.class

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听_java_06

 代理类的具体内容为:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import com.yang.ym.proxy.Action;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Action {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m4;
private static Method m0;

//这里会拿到LawyerHandler实例(LawyerHandler实现了InvocationHandler接口)
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void consult() throws {
try {
//最终还是调用LawyerHandler的invoke方法
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void litigation() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.yang.ym.proxy.Action").getMethod("consult");
m4 = Class.forName("com.yang.ym.proxy.Action").getMethod("litigation");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

其实,到这里已经很清楚了。

总结下关键点:

  • Proxy.newProxyInstance能够生成代理类(com.sun.proxy.$Proxy0)的实例,本质上是通过拼凑字节码来实现的。
  • 代理类会继承Proxy,并且和被代理类实现同样的接口(这就造成了代理类可以安全的转化为代理接口类型)
  • 每一个代理类实例都会关联一个InvocationHandler实例,该实例本质上是代理对象实例。
  • 对代理类实例的方法调用,会进入到InvocationHandler的invoke方法中,传入的Method将确定对代理对象实例的哪个方法进行反射调用

留下一个思考题吧

为什么JDK动态代理摆脱不了只能代理接口的枷锁呢?

答案会在以后补上来,今天就太晚了。

标签:字节,JDK,代理,class,源码,proxy,new,com,public
From: https://blog.51cto.com/u_15840568/5784357

相关文章

  • GATK源码解析(一)
    程序入口 org.broadinstitute.hellbender.Main类下的main函数publicstaticvoidmain(finalString[]args){newMain().mainEntry(args);}......
  • android Activity的启动流程源码分析
    ActivityThread在handlebindapplication中执行完Application的初始化之后会继续进入到消息循环中接收AMS(activitymanagerservice)启动activity的消息。AMS首先会发送启动......
  • 直播网站源码,React中的三大实例之ref的三种形式
    直播网站源码,React中的三大实例之ref的三种形式ref有三种形式:字符串形式回调函数形式CreateRef形式如下示例代码展示了三种形式ref的创建于使用 <!DOCTYPEhtml><......
  • Java一个还不错的日期格式转换工具类(附源码)
    Java工具类pom依赖<commons-lang3.version>3.3.2</commons-lang3.version><dependency><groupId>org.apache.commons</groupId......
  • JDK、JRE、JVE
    JDK、JRE、JVEJDK:JavaDevelopmentKItjava开发者工具JRE:JavaRuntimeEnvironmentjava运行时环境JVM:JAVAVirtualMachinejava虚拟机三者关系JDK包含......
  • JVM、JDK、JRE你分的清吗
    JVM、JDK、JRE你分的清吗前言在我们学习Java的时候,就经常听到"需要安装JDK"、"运行需要JRE"、"JVM调优"等等,这里面的JVM、JDK、JRE你真的分得清吗,今天我们就来讲讲它们......
  • 软件绘制源码流程分析
    引言:之前的文章中提到过软件绘制是会调用drawSoftware方法进行绘制的。在这个方法里面调用了Surface.lockCanvas和unlockAndPost方法。这篇文章就分析这两个方法Surface......
  • 字节跳动后端面经(12)
    孤儿进程和僵尸进程了解多少虚拟内存说一下页面置换算法说一下问TCP和UDP的区别视频、直播、游戏等采用TCP还是UDPUDP为什么实时性好https与http的区别堆中的GC说......
  • 基于springboot高考填报志愿综合参考系统设计与实现-计算机毕业设计源码+LW文档
    摘要:高考填报志愿综合参考系统是针对目前高考填报志愿管理的实际需求,从实际工作出发,对过去的高考填报志愿综合参考系统存在的问题进行分析,完善用户的使用体会。采用计算机系......
  • 从JDK8到JDK17
    从JDK8到JDK17JDK9从Java8到Java17(一)JDK9新特性Java9新特性模块系统JDK9引入了一个新的特性叫做JPMS(JavaPlatformModuleSystem),也可以叫做ProjectJigsaw。模......