1、讲讲什么情况下会出现内存溢出,内存泄漏?
内存泄漏的原因很简单:
1、 对象是可达的(一直被引用)
2、 但是对象不会被使用
常见的内存泄漏例子:
public static void main(String[] args) {
Set<Object> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
Object object = new Object();
set.add(object);
// 设置为空,该对象不再使用
object = null;
}
// 但是set集合中还维护object的引用,gc不会回收object对象
System.out.println(set);
System.out.println(set.size());
}
}
输出结果
[java.lang.Object@74a14482,
java.lang.Object@677327b6,
java.lang.Object@6d6f6e28,
java.lang.Object@4554617c,
java.lang.Object@45ee12a7,
java.lang.Object@1b6d3586,
java.lang.Object@7f31245a,
java.lang.Object@135fbaa4,
java.lang.Object@1540e19d,
java.lang.Object@14ae5a5]
10
Process finished with exit code 0
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了。其他内存泄漏得一步一步分析了。
内存溢出的原因:
1、 内存泄露导致堆栈内存不断增大,从而引发内存溢出。
2、 大量的jar,class文件加载,装载类的空间不够,溢出
3、 操作大量的对象导致堆内存空间已经用满了,溢出
4、 nio直接操作内存,内存过大导致溢出
解决:
1、 查看程序是否存在内存泄漏的问题
2、 设置参数加大空间
3、 代码中是否存在死循环或循环产生过多重复的对象实体、
4、 查看是否使用了nio直接操作内存。
2、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
3、线程与进程的区别?
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
4、Session的save()、update()、merge()、lock()、saveOrUpdate()和persist()方法分别是做什么的?有什么区别?
瞬时态的实例可以通过调用save()、persist()或者saveOrUpdate()方法变成持久态;游离态的实例可以通过调用 update()、saveOrUpdate()、lock()或者replicate()变成持久态。save()和persist()将会引发SQL的INSERT语句,而update()或merge()会引发UPDATE语句。save()和update()的区别在于一个是将瞬时态对象变成持久态,一个是将游离态对象变为持久态。merge()方法可以完成save()和update()方法的功能,它的意图是将新的状态合并到已有的持久化对象上或创建新的持久化对象。
对于persist()方法,
persist()方法把一个瞬时态的实例持久化,但是并不保证标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时间;
persist()方法保证当它在一个事务外部被调用的时候并不触发一个INSERT语句,当需要封装一个长会话流程的时候,persist()方法是很有必要的;
save()方法不保证第②条,它要返回标识符,所以它会立即执行INSERT语句,不管是在事务内部还是外部。至于lock()方法和update()方法的区别,update()方法是把一个已经更改过的脱管状态的对象变成持久状态;lock()方法是把一个没有更改过的脱管状态的对象变成持久状态。
5、用代码演示三种代理
- 静态代理
什么是静态代理
由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
- 代码演示
我有一段这样的代码:(如何能在不修改UserDao接口类的情况下开事务和关闭事务呢)
package com.lijie;
//接口类
public class UserDao{
public void save() {
System.out.println("保存数据方法");
}
}
package com.lijie;
//运行测试类
public class Test{
public static void main(String[] args) {
UserDao userDao = new UserDao();
userDao.save();
}
}
修改代码,添加代理类
package com.lijie;
//代理类
public class UserDaoProxy extends UserDao {
private UserDao userDao;
public UserDaoProxy(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
System.out.println("开启事物...");
userDao.save();
System.out.println("关闭事物...");
}
}
//添加完静态代理的测试类
public class Test{
public static void main(String[] args) {
UserDao userDao = new UserDao();
UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);
userDaoProxy.save();
}
}
缺点:
每个需要代理的对象都需要自己重复编写代理,很不舒服,
优点:
但是可以面相实际对象或者是接口的方式实现代理
- 动态代理
什么是动态代理
动态代理也叫做,JDK代理、接口代理。
动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就叫动态代理
- 代码演示
package com.lijie;
//接口
public interface UserDao {
void save();
}
package com.lijie;
//接口实现类
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("保存数据方法");
}
}
下面是代理类,可重复使用,不像静态代理那样要自己重复编写代理
package com.lijie;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
// 每次生成动态代理类对象时,实现了InvocationHandler接口的调用处理器对象
public class InvocationHandlerImpl implements InvocationHandler {
// 这其实业务实现类对象,用来调用具体的业务方法
private Object target;
// 通过构造函数传入目标对象
public InvocationHandlerImpl(Object target) {
this.target = target;
}
//动态代理实际运行的代理方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用开始处理");
//下面invoke()方法是以反射的方式来创建对象,第一个参数是要创建的对象,第二个是构成方法的参数,由第二个参数来决定创建对象使用哪个构造方法
Object result = method.invoke(target, args);
System.out.println("调用结束处理");
return result;
}
}
利用动态代理使用代理方法
package com.lijie;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
// 被代理对象
UserDao userDaoImpl = new UserDaoImpl();
InvocationHandlerImpl invocationHandlerImpl = new InvocationHandlerImpl(userDaoImpl);
//类加载器
ClassLoader loader = userDaoImpl.getClass().getClassLoader();
Class<?>[] interfaces = userDaoImpl.getClass().getInterfaces();
// 主要装载器、一组接口及调用处理动态代理实例
UserDao newProxyInstance = (UserDao) Proxy.newProxyInstance(loader, interfaces, invocationHandlerImpl);
newProxyInstance.save();
}
}
缺点:
必须是面向接口,目标业务类必须实现接口
优点:
不用关心代理类,只需要在运行阶段才指定代理哪一个对象
- CGLIB动态代理
CGLIB动态代理原理:
利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
什么是CGLIB动态代理
CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。(CGLIB动态代理需要导入jar包)
- 代码演示
package com.lijie;
//接口
public interface UserDao {
void save();
}
package com.lijie;
//接口实现类
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("保存数据方法");
}
}
package com.lijie;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
//代理主要类
public class CglibProxy implements MethodInterceptor {
private Object targetObject;
// 这里的目标类型为Object,则可以接受任意一种参数作为被代理类,实现了动态代理
public Object getInstance(Object target) {
// 设置需要创建子类的类
this.targetObject = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
//代理实际方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开启事物");
Object result = proxy.invoke(targetObject, args);
System.out.println("关闭事物");
// 返回代理对象
return result;
}
}
package com.lijie;
//测试CGLIB动态代理
public class Test {
public static void main(String[] args) {
CglibProxy cglibProxy = new CglibProxy();
UserDao userDao = (UserDao) cglibProxy.getInstance(new UserDaoImpl());
userDao.save();
}
}
6、stackoverflow错误,permgen space错误
stackoverflow错误主要出现:
在虚拟机栈中(线程请求的栈深度大于虚拟机栈锁允许的最大深度)
permgen space错误(针对jdk之前1.7版本):
1、 大量加载class文件
2、 常量池内存溢出
7、分代收集算法
当前主流 VM 垃圾收集都采用”分代收集” (Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中的新生代、老年代、永久代, 这样就可以根据各年代特点分别采用最适当的 GC 算法
8、同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
请知道一条原则:同步的范围越小越好。
9、Java 中的编译期常量是什么?使用它又什么风险?
公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。
10、Java死锁以及如何避免?
Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。
Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。