Java基础
文章目录
- 一. 内存泄漏与内存溢出
- 1. 内存泄露(memory leak)
- 2. 内存溢出(out of memory)
- 3. 解决方案
- 3.1获取内存的dump文件
- 3.2 使用MAT工具来分析Dump文件
- 二. count(1)、count(字段)、count(*)的区别?
- 1. count函数的概念
- 2. 区别
- 三. 反射
- 1. 概念
- 2. 特点
- 3. 应用场景
- 4. 代码示例
- 四. JDK动态代理和CGLib动态代理
- 1. JDK动态代理
- 1.概念
- 2. 实现机制
- 3.代码示例
- 4.应用场景
- 4.1 日志记录
- 4.2 事务管理
- 六. 类加载过程、类加载时机、对象初始化过程
- 一、类加载过程
- (一)加载(Loading)
- (二)连接(Linking)
- (三)初始化(Initialization)
- 二、类加载时机
- (一)创建类的实例
- (二)访问类的静态变量或静态方法
- (三)子类初始化时
- (四)使用反射调用
- 三、对象初始化的过程
- (一)分配内存
- (二)初始化成员变量
- (三)执行构造方法
- 四、代码示例
- 分析:
- 六. Java对象的初始化
- 1. Java对象初始化的顺序
- 1. 加载类
- 2. 分配内存
- 3. 初始化成员变量
- 4. 执行构造方法
- 5. 总结
- 七. Spring的循环依赖
- 1.Spring循环依赖的背景
- Spring解决循环依赖的三级缓存机制
- 解决循环依赖的具体流程
- 1. 创建Bean A
- 2. 创建Bean B
- 3. 完成Bean A的初始化
- AOP代理与循环依赖
- 限制条件
一. 内存泄漏与内存溢出
1. 内存泄露(memory leak)
程序申请的内存在使用完毕后,没有释放,导致JVM无法及时回收内存并分配给其他程序使用
2. 内存溢出(out of memory)
程序申请的内存超过了JVM可以提供的内存大小,导致程序崩溃
3. 解决方案
3.1获取内存的dump文件
- 配置JVM启动参数,当产生内存溢出时自动生成dump文件
- 使用imap工具来生成dump文件
3.2 使用MAT工具来分析Dump文件
- 内存泄漏:通过工具定位泄漏代码的位置
- 内存溢出:提升堆内存空间.
二. count(1)、count(字段)、count(*)的区别?
1. count函数的概念
统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少个
2. 区别
- count(*)与count(1):没有区别,*是通配符,表示所有列;1表示常量;-统计总行数-不关心具体的列值
- count(字段):统计该列中非NULL值的数量-统计有效行数
三. 反射
1. 概念
允许程序在运行时访问、检查和修改它自己的结构,包括类、接口、字段和方法。反射机制的存在能够使得Java程序可以在运行时动态地创建对象、调用方法和修改字段值等
2. 特点
- 动态性:动态地创建对象
- 是实现代理模式和AOP的基础
3. 应用场景
- 框架开发:许多框架(Spring)使用反射来实现依赖注入、AOP等特性
- 配置文件和代码分离:通过反射,可以根据配置文件动态创建对象和调用方法.实现配置和代码分离.
4. 代码示例
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;public class ReflectionExample {public static void main(String[] args) {try {// 获取Class对象Class<?> clazz = Class.forName("java.lang.String");// 创建对象Constructor<?> constructor = clazz.getConstructor(String.class);Object obj = constructor.newInstance("Hello, Reflection!");// 调用方法Method method = clazz.getMethod("length");int length = (Integer) method.invoke(obj);// 访问字段Field field = clazz.getDeclaredField("value");field.setAccessible(true); // 私有字段需要设置访问权限char[] value = (char[]) field.get(obj);System.out.println("String length: " + length);System.out.println("String value: " + new String(value));} catch (Exception e) {e.printStackTrace();}}
}
四. JDK动态代理和CGLib动态代理
1. JDK动态代理
1.概念
- 在运行时动态创建调用处理器对象和代理对象。
- 基于接口实现,JDK动态代理要求被代理的类必须实现一个或多个接口.代理类会实现这些接口,当代理对象中的方法被调用时,会触发调用处理器对象(实现了InvocationHandler接口的对象)的invoke方法
2. 实现机制
使用反射机制,通过java.lang.reflect.Proxy类和InvocationHandler接口来实现代理。代理对象仅代理接口中的方法。当调用代理对象的方法时,调用处理器对象会拦截方法调用,并通过InvacationHandler.invoke()方法执行额外的逻辑.
3.代码示例
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 定义一个接口
public interface Service {void execute();
}// 实现接口的业务类-目标类
public class RealService implements Service {@Overridepublic void execute() {System.out.println("Executing service operation");}
}// 动态代理类,实现InvocationHandler接口
public class DynamicProxy implements InvocationHandler {private Object target;public DynamicProxy(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 自定义处理逻辑System.out.println("Before method execution");// 调用目标对象的原始方法,并将原始方法的返回值,返回给代理对象方法的调用者.Object result = method.invoke(target, args);System.out.println("After method execution");return result;}// 获取代理对象:代理对象创建后,它将实现目标对象所实现的所有接口.public Object getProxy() {return Proxy.newProxyInstance(// 类加载器-目标类target.getClass().getClassLoader(),// 目标对象实现的接口列表,代理对象将实现这些接口target.getClass().getInterfaces(),// 一个实现了InvocationHandler接口的对象,负责处理对代理对象的所有方法调用this);}
}// 测试类
public class Test {public static void main(String[] args) {// 目标对象RealService realService = new RealService();// 创建一个DynamicProxyHandler对象,并将目标对象传递给它DynamicProxy dynamicProxy = new DynamicProxy(realService);// 获取代理对象-使用Proxy.newProxyInstance()方法创建一个实现了指定接口的代理对象.Service proxyInstance = (Service) dynamicProxy.getProxy();// 代理对象调用接口中的方法时,会被DynamicProxyHandler的Invoke()方法拦截,也就是说invoke方法被触发.proxyInstance.execute();}
}
4.应用场景
4.1 日志记录
通过AOP,可以在方法执行前后添加日志记录,而不需要在每个业务方法中手动添加代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.springframework.stereotype.Component;@Aspect
@Component
public class LoggingAspect {@Before("execution(* com.example.service.*.*(..))")public void logBefore() {System.out.println("Method execution started.");}@After("execution(* com.example.service.*.*(..))")public void logAfter() {System.out.println("Method execution finished.");}
}
4.2 事务管理
Spring的声明式事务管理使用动态代理来确保方法执行时自动开启和关闭事务
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class UserServiceImpl implements UserService {@Transactional@Overridepublic void createUser(String username) {// 业务逻辑代码}
}
@Transactional注解告诉Spring在createUser方法执行时自动管理事务。Spring会为UserService创建一个代理对象,并在方法执行前后分别开启和提交或回滚事务。
六. 类加载过程、类加载时机、对象初始化过程
一、类加载过程
在Java中,类加载过程主要分为三个阶段:加载(Loading)、连接(Linking)和初始化(Initialization)。
(一)加载(Loading)
- 读取字节码文件
- 加载阶段是类加载过程的第一个阶段。在这个阶段,Java虚拟机(JVM)通过类加载器(ClassLoader)从文件系统、网络等位置读取类的字节码文件(.class文件)。
- 例如,当你通过
new
关键字创建一个对象时,JVM会先通过类加载器找到对应的字节码文件。 - 假设有一个类
Person
,其字节码文件名为Person.class
。类加载器会将这个文件的内容加载到内存中。
- 转换为Class对象
- 加载到内存后,JVM会将字节码数据转换成一个
java.lang.Class
对象。这个对象是类在JVM中的唯一代表,它包含了类的结构信息,如字段、方法、接口,存储到运行时数据区的方法区中。
- 加载到内存后,JVM会将字节码数据转换成一个
(二)连接(Linking)
- 验证(Verification)
- 验证阶段主要是确保加载的字节码文件符合Java虚拟机规范。JVM会检查字节码文件的格式、语义等是否正确。
- 例如,它会检查类的继承关系是否合法(如不能继承
final
类)、方法签名是否正确等。
- 准备(Preparation)
- 在准备阶段,JVM会为类的静态变量分配内存,并设置默认初始值。这些默认值是根据数据类型决定的,如整型变量默认为0,布尔型变量默认为
false
。 - 假设
Person
类中有静态变量static int count;
,在准备阶段,count
会被初始化为0。
- 在准备阶段,JVM会为类的静态变量分配内存,并设置默认初始值。这些默认值是根据数据类型决定的,如整型变量默认为0,布尔型变量默认为
- 解析(Resolution)
- 解析阶段是将类、接口、字段和方法的符号引用转换为直接引用。符号引用是以字符串形式存在的,而直接引用是直接指向内存中的地址。
- 例如,当
Person
类中有一个方法void showName()
,在解析阶段,JVM会将对这个方法的符号引用解析为实际的内存地址。
(三)初始化(Initialization)
- 执行类构造器
<clinit>
方法- 初始化阶段是类加载过程的最后一步。在这个阶段,JVM会执行类构造器
<clinit>
方法。这个方法是由编译器自动收集类中的所有静态变量的赋值操作和静态代码块中的语句组成的。 - 如果
Person
类中有静态代码块static { count = 10; }
和静态变量赋值static int count = 5;
,那么在初始化阶段,<clinit>
方法会被执行,count
的值最终会被设置为10(因为静态代码块的执行顺序在静态变量赋值之后)。
- 初始化阶段是类加载过程的最后一步。在这个阶段,JVM会执行类构造器
二、类加载时机
类加载的时机是指在什么情况下JVM会加载一个类。主要有以下几种情况:
(一)创建类的实例
- 当通过
new
关键字创建一个类的实例时,JVM会加载该类。例如:
Person person = new Person();
在执行这行代码时,Person
类会被加载。
(二)访问类的静态变量或静态方法
- 当访问一个类的静态变量或静态方法时,JVM会加载该类。例如:
int value = Person.count;
或者
Person.showName();
只要Person
类中有static int count;
静态变量或static void showName()
静态方法,JVM就会加载Person
类。
(三)子类初始化时
- 当子类被初始化时,如果父类还没有被加载,那么父类也会被加载。例如:
class Parent {static {System.out.println("Parent class loaded");}
}class Child extends Parent {static {System.out.println("Child class loaded");}
}public class Test {public static void main(String[] args) {Child child = new Child();}
}
运行Test
类时,输出为:
Parent class loaded
Child class loaded
因为子类Child
在初始化时,父类Parent
也被加载。
(四)使用反射调用
- 当通过反射调用一个类时,JVM会加载该类。例如:
Class.forName("Person");
这会加载Person
类。
三、对象初始化的过程
对象初始化的过程主要包括以下步骤:
(一)分配内存
- 当通过
new
关键字创建对象时,JVM会为对象分配内存。分配的内存大小取决于对象的大小,对象的大小由类的字段决定。 - 例如,
Person
类有三个字段:
class Person {int age;String name;double height;
}
创建Person
对象时,JVM会为age
(4字节)、name
(引用类型,通常为4字节或8字节,取决于JVM的架构)和height
(8字节)分配内存。
(二)初始化成员变量
- 在分配内存后,JVM会初始化对象的字段。如果字段有显式的初始值,就使用显式的初始值;如果没有,就使用默认初始值。
- 假设
Person
类的字段有初始值:
class Person {int age = 18;String name = "Kimi";double height = 1.75;
}
创建对象时,age
会被初始化为18,name
会被初始化为"Kimi",height
会被初始化为1.75。
(三)执行构造方法
- 最后,JVM会执行对象的构造方法。构造方法可以对对象进行进一步的初始化。
- 例如:
class Person {int age = 18;String name = "Kimi";double height = 1.75;public Person() {System.out.println("Constructor called");}
}public class Test {public static void main(String[] args) {Person person = new Person();}
}
运行Test
类时,输出为:
Constructor called
这说明构造方法被正确执行了。
四、代码示例
以下是一个完整的代码示例,展示了类加载过程、类加载时机和对象初始化的过程:
class Parent {static {System.out.println("Parent static block");}
}class Child extends Parent {static {System.out.println("Child static block");}{System.out.println("Child instance block");}public Child() {System.out.println("Child constructor");}
}public class Test {public static void main(String[] args) {System.out.println("Main method starts");Child child = new Child();System.out.println("Main method ends");}
}
运行结果:
Parent static block
Child static block
Child instance block
Child constructor
Main method ends
分析:
- 类加载时机:当
Child
类被实例化时,Parent
类和Child
类都被加载。 - 类加载过程:
- 加载阶段:
Parent
和Child
类的字节码文件被加载到内存。 - 连接阶段:验证、准备和解析。
- 初始化阶段:执行
Parent
和Child
的静态代码块。
- 加载阶段:
- 对象初始化过程:
- 分配内存。
- 初始化字段。
- 执行实例代码块。
- 执行构造方法。
六. Java对象的初始化
对象的初始化是一个重要的概念,涉及到类的加载、对象的创建以及成员变量和构造方法的执行。
1. Java对象初始化的顺序
1. 加载类
- 当程序中首次使用某个类时(例如通过new关键字创建对象、调用类的静态方法或访问类的静态变量等),JVM会加载该类。
- 类加载过程中,会执行类的静态初始化代码块(static {})和静态变量的初始化。
2. 分配内存
当使用new关键字创建对象时,JVM会为对象分配内存空间。
3. 初始化成员变量
在对象的内存空间分配完成后,会按照字段在类中声明的顺序初始化成员变量。
如果成员变量有显式的初始化值,则使用该值进行初始化;如果没有显式的初始化值,则使用默认值(例如,int的默认值是0,String的默认值是null)。
class Person {String name = "Unknown"; // 显式初始化int age;{age = 0; // 实例代码块初始化}public Person(String name, int age) {this.name = name; // 构造方法中覆盖name的初始值this.age = age; // 构造方法中覆盖age的初始值}
}
4. 执行构造方法
在成员变量初始化完成后,会调用构造方法(Constructor)来完成对象的最终初始化。
如果类中没有显式定义构造方法,Java会提供一个默认的无参构造方法。
5. 总结
- 初始化成员变量:在构造方法执行之前,通过显式初始化和实例代码块完成。
- 执行构造方法:在成员变量初始化完成后,完成对象的最终初始化逻辑。
- 初始化顺序:类加载 → 分配内存 → 初始化成员变量 → 调用父类构造方法 → 执行当前类的构造方法。
七. Spring的循环依赖
Spring框架通过三级缓存机制解决了单例Bean的循环依赖问题。以下是基于类加载和对象初始化的角度对Spring解决循环依赖的详细解释:
1.Spring循环依赖的背景
循环依赖是指在Spring容器中,两个或多个Bean之间存在相互依赖的关系。例如,Bean A依赖Bean B,而Bean B又依赖Bean A。如果不加以处理,这种关系会导致Bean无法正常初始化,因为每个Bean都在等待另一个Bean完成初始化。
Spring解决循环依赖的三级缓存机制
Spring通过三级缓存来解决循环依赖问题,这三级缓存分别是:
- 一级缓存(singletonObjects):存储完全初始化后的单例Bean。
- 二级缓存(earlySingletonObjects):存储已实例化但未完成属性注入的早期Bean引用。
- 三级缓存(singletonFactories):存储Bean的
ObjectFactory
,用于生成早期对象(可能包含代理对象)。
解决循环依赖的具体流程
以Bean A依赖Bean B,Bean B依赖Bean A为例,Spring解决循环依赖的流程如下:
1. 创建Bean A
- 实例化阶段:Spring实例化Bean A,并将A的
ObjectFactory
放入三级缓存(singletonFactories)。 - 属性注入阶段:在为Bean A注入属性时,发现A依赖Bean B,于是触发Bean B的创建。
2. 创建Bean B
- 实例化阶段:Spring实例化Bean B,并将B的
ObjectFactory
放入三级缓存。 - 属性注入阶段:在为Bean B注入属性时,发现B依赖Bean A。此时,Spring从三级缓存中获取A的
ObjectFactory
,通过ObjectFactory
生成一个未完全初始化的A对象,并将其放入二级缓存(earlySingletonObjects)。 - 完成B的初始化:将未完全初始化的A对象注入到B中,完成B的初始化,并将B放入一级缓存(singletonObjects)。
3. 完成Bean A的初始化
- 继续A的属性注入:Spring回到Bean A的创建过程,此时A需要注入的B对象已经存在于一级缓存中,因此将B注入到A中。
- 完成A的初始化:完成A的初始化,并将A放入一级缓存。
AOP代理与循环依赖
如果Bean A或Bean B被AOP代理(例如使用@Transactional
注解),Spring会通过getEarlyBeanReference
方法提前暴露代理对象。这样,即使Bean A或Bean B尚未完全初始化,依赖它们的Bean也能获取到代理对象,从而避免循环依赖问题。
限制条件
- 仅适用于单例Bean:Spring的循环依赖解决方案仅适用于单例作用域的Bean。对于原型作用域的Bean,Spring会直接抛出异常。
- 依赖注入方式的限制:Spring无法解决通过构造器注入的循环依赖问题,因为构造器注入要求Bean在注入时必须是完全初始化的状态。
通过三级缓存机制,Spring巧妙地解决了单例Bean的循环依赖问题,同时通过提前暴露代理对象,确保了AOP功能的正常运行。