阿里-测试开发面经(五)
共 4925字,需浏览 10分钟
·
2021-04-27 20:53
点击蓝字关注我们,获取更多面经
一、定义
内存溢出: 即为out of memory, 当你要求分配的内存超过了系统给你的内存时, 系统就会抛出out of memory的异常(每个Android能用的内存是有限的)
比如: 当前应用只剩下4M的空间可用, 但你却加载得到一个需要占用5M空间的图片Bitmap对象, 就会抛出溢出的异常
内存泄露: 即为memory leak, 一个对象被创建后, 你不再使用它了, 但因为某种原因它又没有成为垃圾对象, 这块内存不能再被分配置使用.
比如: 查询数据库得到的cursor对象在使用完后没有关闭, Activity中使用Handler发延迟消息, 但退出前不移除未处理的消息
内存泄露不多时没有太大影响, 但积累得多了就会导致应用运动缓慢, 到最后就会内存溢出.
二、内存泄漏的分类
常发性内存泄漏: 发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
偶发性内存泄漏: 发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的
一次性内存泄漏: 发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏
说明: 危害性大小顺序为: 1)>2)>3)
三、造成内存泄露的几种场景
长生命周期的对象持有短生命周期对象的引用: Activity中使用Handler
资源数据连接相关对象不关闭: cusor, stream, connection
HashSet中的对象或HashMap中的Key对象, 内部与hash值相关的属性被修改
一些对象产生后不会自动释放或需要完全执行完了才释放. 比如: Bitmap, Thread, AsyncTask
四、避免内存泄露
尽早释放无用对象的引用
使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
避免在循环中创建对象
五、造成内存溢出的的场景
申请了太多的对象. 比如: 使用ListView时, 不复用convertView, 当数据项多时就会出现内存溢出
创建的对象内存太大. 比如: 不经过压缩直接加载大图片文件
内存泄露积累一定的时间后就可能出现
六、避免内存溢出
通过复用对象的方式, 减少产生的对象
大对象需要先压缩后创建
避免或减少内存泄露的情况
Java中实现线程有三种方式:
1、1.继承Thread类
public class Thread extends Object implements Runnable
定义Thread类的子类,并重写Thread类的run()方法,创建子类对象(即线程对象),调用线程对象的start()方法来启动该线程。
2.实现Runnable接口
public interface Runnable
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是该线程的执行体。创建该Runnable实现类的实例,并将此实例作为Thread的target(即构造函数中的参数)来创建Thread对象(该Thread对象才是真正的线程对象,只是该Threa
3.使用Callable和Future
创建Callable接口的实现类,并实现call()方法,该方法有返回值;创建Callable实现类的实例,使用FutureTask来包装Callable对象,并且也封装了call()方法的返回值;使用FutureTask作为Thread类的target创建并启动线程;调用FutureTask对象的get()方法返回子线程执行结束后的返回值。
如何实现多线程:
首先是继承Thread类并重写run()方法
package com.csu.multiThread;
public class MultiThreadExtendsThread extends Thread{
String name;
public MultiThreadExtendsThread(String name) {
this.name = name;
}
public void run() {
for(int i=0;i<5;i++) {
System.out.println(name+"运行:"+i);
}
}
public static void main(String[] args) {
MultiThreadExtendsThread thread1 = new MultiThreadExtendsThread("A");
MultiThreadExtendsThread thread2 = new MultiThreadExtendsThread("B");
thread1.start();
thread2.start();
}
}
二是实现Runnable接口,然后重写run()方法,
package com.csu.multiThread;
public class MultiThreadImplRunnable implements Runnable{
String name;
public MultiThreadImplRunnable(String name) {
this.name = name;
}
public void run() {
for(int i=0;i<5;i++) {
System.out.println(name+"运行:"+i);
}
}
public static void main(String[] args) {
MultiThreadExtendsThread thread1 = new MultiThreadExtendsThread("A");
MultiThreadExtendsThread thread2 = new MultiThreadExtendsThread("B");
new Thread(thread1).start();
new Thread(thread2).start();
}
}
线程安全的实现:
最基本的:synchronized关键字。这个方法是最常用的,它通过互斥的方式保证同步。我们知道java中有几个操作是可以保证原子性的,其中lock/unlock就是一对。虽然java没有提供这两个字节码的接口,但是我们可以通过monitorenter/monitorexit,而synchronized会在块的前后调用两个字节码指令。同时synchronize对于同一条线程来说是可重入的;其次它也是阻塞的。我们知道java线程是映射到操作系统上的,而且是混用的内核态线程和用户态线程(N:M),而将线程从阻塞/唤醒,需要将线程从用户态转换到内核态,这样会消耗太亮的资源,所以synchronize是一个重量级锁
另外一种和synchronize类似的方法:ReentrantLock。它们两个的区别:(1)synchronize是隐式的,只要块内的代码执行完,就会释放当前的锁;而后者需要显式的调用unlock()方法手动释放,所以经常搭配try/finally方法(忘记在finally中unlock是非常危险的) (2)后者可以选择等待中断——即在当前持有锁线程长期不释放锁的情况下,正在等待的线程可以选择放弃等待选择处理其他的事情。(3) 后者可以选择公平锁(虽然默认是非公平的,因为公平锁的吞吐量很受影响)即先来后到,按申请的顺序获得锁。(4)可以绑定多个条件
前面提到的两种方式都是通过互斥来达到同步的目的,这其实是悲观锁的一种。下面介绍的是乐观锁,基于冲突检测的并发策略,不需要将线程挂起,因此又被成为非阻塞同步。
典型:CAS(Compare And Swap),通过Unsafe类提供。有三个操作数,内存位置、旧的预期值、和新的值;当且仅当内存地址V符合预期值A时,执行将值更新为新的预期值B。 存在的问题:“ABA”情况,即原值为A,但在检测之前发生了改变,变成了B,同时也在检测时变回了A;即不能保证这个值没有被其他线程更改过。
接下来是无同步方案:
可重入代码(纯代码):是一种无同步方案,在执行的任何时候去中断,转而执行其他的代码;在重新返回代码后,不会出现任何的错误。可重入性->线程安全,充分不必要条件。即可重入性的代码都是线程安全的,但反之不一定。简单判断原则:一个方法的返回结果是可预测的,只要输入了相同的数据,就都能返回相同的结果。
线程本地存储:即利用ThreadLocal类;每个Thread类中都有一个变量ThreadLocalMap,默认是为null的。它将为每一个线程创立一个该变量的副本。这样线程之间就不存在数据征用的问题了。适用情况:(1)数据库的Connection连接 (2)WEB中的“一个请求对应一个服务器线程”,在知乎上看到一个回答,解释的蛮清晰的。(3)Spring中创建的默认模式是Singleton单例 (4)“生产者-消费者问题”
ThreadLocal就是变量在不同线程上的副本,不同线程不共享,所以对变量改动时就不需要考虑线程间同步的问题了
ThreadLocal在web应用开发中是一种很常见的技巧,当web端采用无状态写法时(比如stateless session bean和spring默认的singleton),就可以考虑把一些变量放在ThreadLocal中
举个简单例子,以理解意思为主:你有两个方法A和B都要用到变量userId,又不想传来传去,一个很自然的想法就是把userId设为成员变量,但是在无状态时,这样做就很可能有问题,因为多个request在同时使用同一个instance,userId在不同request下值是不一样的,就会出现逻辑错误
但由于同一个request下一般都是处于同一个线程,如果放在ThreadLocal的话,这个变量就被各个方法共享了,而又不影响其他request,这种情况下,你可以简单把它理解为是一种没有副作用的成员变量(作者:卡斯帕尔)
线程栈线程的每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
更多面经
扫描二维码
获取更多面经
扶摇就业