Java深入学习
这篇文章用于记录Java基础知识相关的深入学习。包括了 动态代理、反射、JVM
一、动态代理篇
首先先理清代理的概念,代理就是用来增强原对象功能的。通过代理,可以无侵入式的给对象增强其他功能。
代理长什么样?
代理类里面就是对象要被代理的方法。
Java通过什么来保证代理的样子?
通过接口保证的,被代理对象和代理都需要实现同一个接口,而这个接口里的方法就是要被代理的所有方法。
例子
JDK动态代理
被代理的原对象
1 | public class Star implements Star_Interface { |
代理接口(中介)
1 | public interface Star_Interface { |
代理创建类,并不是代理
通过Proxy的newProxyInstance方法来创建代理对象
1 | public class StarProxy { |
测试
1 | public class Test { |
代理对象调用dance()方法时,实际上会通过反射将method对象和参数传递给invoke
Cglib动态代理
二、反射篇
是什么?
反射是指通过获取类的Class对象来获取类的方法、构造函数和成员变量信息。
怎么用?
创建Class对象(字节码文件对象)一共有三种方法
- Class.forName(“全类名”)
- 类名.class
- 实例对象.getClass()
以下是三个方法的详细应用场景
1.Class.forName(“全类名”)
Class.forName用于动态加载类,适用于不知道类名的情况。因为有些时候,我们可能不确定要使用哪些类,某些类是需要我们在运行时期才可以知道的。
实现原理:它通过类加载器(ClassLoader)来加载指定的类的。类加载器会根据提供的全限定名,从类路径中查找对应的字节码文件(.class
文件)。如果找到了字节码文件,那么类加载器会将字节码文件加载到JVM中,并将其转换为Class
对象。
使用场景:就比如Spring中,我们建了多个模块,其中一个模块需要另一个模块的配置类自动装配,但是Spring默认自动装配的是当前路径下的组件,所以我们就需要给另一个模块加上spring.factory来写明要自动装配的全类名。这些类就是通过Class.forName()来加载的。
2.类名.class
实现原理:直接通过类的静态属性 .class
获取 Class
对象。
使用场景:通常作用于各种需要Class对象的方法的参数。
3.实例对象.getClass()
实现原理:通过对象实例的 getClass()
方法获取其运行时类的 Class
对象。
使用场景:
1.通过 getClass()
获取被代理对象的真实类型,实现拦截逻辑。
2.多态环境下的类型判断
当父类引用指向子类对象时,
getClass()
可以获取实际的运行时类。示例:
1
2Object obj = new ArrayList<>();
Class<?> actualClass = obj.getClass(); // 返回 ArrayList.class,而非 Object.class
至于获取Class对象后,有什么用,看如下这张图
用在哪里?
反射的使用场景有很多,以下罗列了我已知的
1.框架开发
(1)依赖注入
Spring 通过反射扫描类路径,找到带有 @Component
、@Service
等注解的类(反射是可以获取类、成员变量或方法上面的注解的),并动态创建 Bean 实例。
底层实现:Spring 使用 Class.forName()
加载类,再通过反射调用构造方法创建对象。
(2)AOP
代理类在运行时生成,通过反射调用目标方法,并插入额外逻辑(如日志、事务)。
(3)ORM框架
ORM 框架通过反射获取类的字段名,并动态生成 SQL。
2. 序列化与反序列化(JSON/XML)
通过反射获取对象的字段,并转换为 JSON/XML。
3. 单元测试(JUnit、Mockito)
**JUnit 的
@Test
**:
测试框架通过反射查找并执行带有@Test
的方法。
示例:1
2
3
4
5@Test
public void testAdd() {
Calculator calc = new Calculator();
assertEquals(3, calc.add(1, 2));
}- 底层实现:JUnit 通过
getClass().getMethods()
找到测试方法并执行。
- 底层实现:JUnit 通过
4. 绕过访问限制(慎用!)
访问私有字段/方法:
反射可以突破private
限制,但破坏封装性,需谨慎使用。
示例:1
2
3Field field = target.getClass().getDeclaredField("secret");
field.setAccessible(true); // 强制访问私有字段
String value = (String) field.get(target);
5.动态生成代码(如 Lombok)
**Lombok 的
@Getter
/@Setter
**:
编译时注解处理器(APT)通过反射生成 getter/setter 方法。
示例:1
2
3
4@Getter @Setter
public class User {
private String name;
}- 底层实现:Lombok 在编译时修改字节码,添加相应方法。
原理是怎么样的?
Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。
JVM
1.什么是JVM
java之所以一次编译到处运行,就是因为有JVM的存在,使得同一份字节码文件能被不同操作系统的JVM解释执行。编译成机器码是非常耗时的,因为涉及全局的代码分析和进行复杂的优化,而解释只需要按照已有的内容按需翻译成机器码就行,不需要考虑这些,所以移植性好。
因为jvm是逐行解释字节码成机器码,并逐行执行机器码,所以执行速度相比于通过编译一次性翻译成全部机器码后执行要更慢,所以jvm在解释字节码那块有通过JIT编译进行优化。
JVM 的工作方式实际上是解释执行和 JIT(Just-In-Time)编译的结合:
- 解释执行:JVM 可以逐行解释执行字节码,这种方式启动速度快,但运行速度较慢。
- JIT 编译:为了提高性能,JVM 可以在运行时将热点代码(频繁执行的代码)编译成本地机器码。这种方式可以在不牺牲移植性的前提下提高程序的运行速度。
2.字节码文件的组成
3.运行时数据区
就是指由JVM管理的内存区域,分为两大类
一类是线程共享的,分为方法区和堆
一类是线程不共享的,分为本地方法栈、虚拟机栈和程序计数器
4.内存溢出
5.JDK6-8内存区域上有哪些不同
1.JDK6,方法区存放在堆中,运行时常量池和字符串常量池存放在方法区
2.JDK7,方法区存放、字符串常量池在堆中,运行时常量池存放在方法区
3.JDK8,方法区以及包含的运行时常量池存在元空间,而字符串常量池存放在堆中
什么是元空间?
指的是由操作系统管理的内存空间,内存空间大。
堆是由JVM管理的内存,受制于JVM所被分配的内存大小。
字符串常量池为什么要移动到堆中?
6.类的生命周期
7.类加载器
什么是类加载器
类加载器负责在类的加载过程中,将字节码信息以流的方式获取并加载到内存中。
种类
- 启动类加载器:JDK9之前是c++编写,之后是java编写
- 扩展/平台类加载器: JDK9及以后称为平台类加载器
- 应用程序类加载器:加载当前程序的classpath中的类
- 自定义类加载器
每种类加载器都有自己的加载目录。
8.双亲委派机制
首先,类加载器之间是有层级关系的,上一级称为下一级的父类加载器
层级关系如下
BootStrap(启动类加载器) -> Extension(扩展类加载器) -> Application(应用程序类加载器) -> 自定义加载器
什么是双亲委派机制?
双亲委派机制指的是,当一个类加载器接收到加载类的任务时,会先检查自己是否加载过这个类,如果没有,则会向上交给父类加载器查看是否加载过,父类没有则上交给父类的父类,直至启动类加载器。如果都没有加载过这个类,则会从顶部开始,往下依次试着加载这个类,直至有类加载器可以加载成功才会停下。
总之,就是从下往上检查,如果都没有加载过,则从上往下依次试着加载。
双亲委派机制的作用?
1.保证类加载的安全性
是因为可以避免恶意代码替换JDK的核心类库,比如java.lang.String,是由启动类加载器加载的,当我们自己写一个同样全类名的String类时,由于我们自定义的类是交给应用程序类加载器加载的,而启动类加载器会提前加载,所以应用程序类加载器会向上递交给启动类加载器,启动类加载器发现这个类自己已经加载过了,就不加载我们重定义的这个类了,我们自己写的这个就无法使用。确保了核心类库的完整性和安全性。
2.避免重复加载
由双亲委派机制可知,如果当前类加载器没有加载过这个类,则会向上传递,而不是直接加载,从而避免了重复加载。
9.如何打破双亲委派机制
双亲委派机制主要是因为类加载器ClassLoader的loadClass方法,它是通过递归的方式实现了类的向上检查到向下加载。
所以想要打破双亲委派机制,就只需要重写ClassLoader的loadClass方法,自定义加载逻辑即可。
10.如何判断堆上对象也没有被引用?
11.JVM中的引用类型
- 强引用
- 软引用
- 弱引用
- 虚引用
- 终结器引用
12.ThreadLocal中为什么要使用弱引用
1.使用弱引用可以让对象被回收。因为仅有弱引用没有强引用的情况下,对象是可以被回收的。
ThreadLocal使用的示例代码如下
1 | public class MyThreadLocal { |
如果threadLocal使用了set方法的话,当前线程会弱引用这个变量指向的对象,此时如果把threadLocal变量=null,那么强引用没了,这个对象就可以随时被回收。之所以要这样做,是因为没有变量指向这个ThreadLocal对象了,那我们也就无法通过变量使用set和get方法了,那么这个对象也就没有存在的必要了。总之就是,外部都不知道有这个人了,还留着干嘛呢
但弱引用并没有完全解决对象回收问题。因为弱引用的Entry对象和其value值被当前线程的ThreadLocalMap强引用。
所以一般需要先手动调用remove()方法进行回收,之后再将ThreadLocal对象的强引用解除(即threadLocal=null)。
试想下,如果没有弱引用,那么即使没有外部变量指向它,只要有一个线程的ThreadLocalMap强引用了它且没有remove,它就无法被回收,一直在map中,直至所有引用它的线程销毁。
而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,即如果threadlocal引用被回收时,即便我们没有手动先进行remove(),也会自动清理关联的Entry,这样就不会导致内存泄漏了。
追问
Entry对象的key既然是弱引用,那value为什么不是弱引用。
答:防止value被提前回收。value要是设置成弱引用的话,要是这个value没有被外界其他变量强引用,仅仅是存在Entry对象里,但发生GC时,它就被回收了,变成null。之后要是来获取这个value值,不就为null了吗。总之就是你不知道value有没有被强引用,要是先于ThreadLocal对象被回收了,之后想获取时该怎么办?