24-06-16-17-18
线程的创建方式、生命周期、线程池的理解及参数的作用
- 多线程的创建方式
(1) 继承Thread类:但Thread本质上也是首先Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一的方法就是通过Thread类的start()实例方法。start()方法是一个navite方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定位的run()方法。
例如:继承Thread类实现多线程,并在合适的地方启动线程
public class MyThread extends Thread{
public void run(){
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
(2) 实现Runnable接口的方式实现多线程,并且实例化Thread,传入自己的Thread实例,调用run()方法
public class MyThread implementss Runnable{
public void run(){
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
(3)使用ExecutorService,Callable,Future实现有返回结果的多线程:ExecutorService,Callable,Future实际上都是属于Executor框架中的功能类。返回结果的线程是JDK5引进的,很实用,不需要再为得到返回值而担心,可返回值的任务必须实现Callable接口,执行,执行Callable任务后,可以获取一个Future的对象,在对象上调用get就可以获取到Callable任务返回的Object,再结合线程池接口,ExecutorService就可以实现有返回结构的多线程。
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test{
public static void main(String [] args) throws ExecutionException,InterruptedException{
System.out.println("----程序开始运行----");
Date data1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for(int i = 0;i < taskSize; i++){
Callable c = new MyCallable(i + " ");
// 执行任务并获取future对象
Future f = pool.submit(c);
list.add(f);
}
pool.shutdown();
// 获取所有并发任务的运行结果
for(Future f : list){
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序运行结束----,程序运行是按【"+(date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object>{
private String taskNum;
MyCallable(String taskNum){
this.taskNum = taskNum;
}
public object call() throws Exception{
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date DateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
- 在Java中wait和sleep方法有声明不同?
- 最大是不同在于等待wait会释放锁,而sleep会一直持有锁。wait一般被用于线程间交互,sleep通常用于暂停执行
- synchronized和volatile关键字的作用
一旦一个共享变量(类的变量,类的静态成员变量)被volatile修饰后,那就具有两层语义:
(1)保证不同线程对这个变量进行操作的可见性,即一个线程修改某个变量的值,这新值对其他线程来说的立即可见的
(2)禁止指令重排序
- volatile本质是告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程访问访问该变量,其他线程被阻塞
- volatile仅能使用在变量级别;synchronized则可以使用在变量,方法和类级别
- volatile仅能实现变量的修改可见性,并不能保证原子性。synchronized则保证需改可见性和原子性
- volatile不会造成线程的阻塞,synchronized可能造成线程的阻塞
- volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
- 分析线程并发访问代码解释原因
public class Counteer{
private volatile int count = 0;
public void run(){
try{
Thread.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
count++;
}
@Override
public String toString(){
return "[count=" + count + "]";
}
}
// -----------------分割线-------------------
public class VolatileTest{
public static void main(String[] args){
final Counter counter = new Counter();
for(int i = 0;i < 1000;i++ ){
new Thread(new Runnable(){
@Override
public void run(){
counter.inc();
}
}).start();
}
System.out.println(counter);
}
}
counter小于1000
- 在Java的内存模型中每个线程运行时都有一个线程栈,线程栈保存线程运行时变量值消息。当线程访问某个对象是值时候,首先通过对象的引用找到对应堆内存的变量的值,然后把堆内存变量的值load(加载)到线程本地内存中,建立一个副本,之后线程就不再和对象在堆内存中变量有任何关系,而是直接修改副本的变量,在修改完后的某个时刻,自动把线程变量的副本的值写回对象在堆中的变量。这样对象的值就发生了变化
- 什么是线程池,任何使用?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候不用new线程而是直接去池中拿线程即可,节省开辟线程的时间,提高代码执行效率
在JDK的java.util.concurrent.Executor中提供生成多种线程池的静态方法
1. ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
2. ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
3. ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
4. ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
调用他们的execute方法即可
-
常用的线程池有哪些?
- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务提交顺序执行
- newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
- newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池的大小完全依赖于操作系统(或者jvm)能够创建的最大线程大小
- newScheduleThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求
-
线程池的理解
- 合理利用线程池能带来的三个好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务不需要等到线程创建就立即执行
- 提高线程的可管理性。线程的稀缺资源,如果无限制的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
- 线程池的启动策略
- 线程池刚创建,里面没有一个线程。任务队列是作为参数传进来的。不过就算队列里面有任务,线程池也不会马上执行它们。
- 当调用execute()方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,会马上创建线程运行这个任务
- 如果正在运行的线程数大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是创建线程运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务”
- 当一个线程完成任务时,它会从队列中取出下一个如来执行
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于core Pool Size,这个线程就被停掉。所以线程池的所有任务完成后,它会收缩到core Pool Size大小
- 控制某个方法允许并发访问线程的个数
public class SemaphoreTest{
static Semaphore semaphore = new Semaphore(5,true);
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
test();
}
}).start();
}
}
public static void test(){
try {
//申请一个请求
semaphore.acquire();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"进来了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"走了");
//释放一个请求
semaphore.release();
}
}
}
- 线程的生命周期
- 新建状态:当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化器成员变量的值
- 就绪状态(runnable):当线程对象调用start()方法之后,该线程就处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
- 运行状态(running):如果就绪的线程获得CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
- 阻塞状态(Blocked):阻塞状态是指线程因为某种原因放弃CPU使用权,暂时停止运行,知道线程进入可运行状态,才有机会再次获取CPU转到运行状态
- 等待阻塞:运行的线程执行object.wait()方法,JVM会把该线程放入等待队列中
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM会将该线程放入锁池(lock pool)
- 其他阻塞:运行的线程执行Thread.sleep(long ms)或t.join()方法,或者发出I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕时,现成转入可运行状态
- 线程死亡:
- 正常死亡:run()或Call()方法执行完毕,线程正常结束
- 异常结束:线程抛出一个未捕获的Exception或error
- 调用stop:直接调用该线程的stop()方法来结束该线程--该方法容易导致死锁
- 线程池组成部分
- 线程池管理器(ThreadPoolExecutor):负责创建,管理和控制线程池。它负责线程的创建,销毁和管理,以及线程池的状态监控和调度任务
- 工作队列(BlockingQueue):用于存储待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入工作队列中等待执行
- 线程池线程(Worker Thread):实例执行任务的线程。线程池中会维护一组线程,这些线程可以被重复利用,从而避免频繁创建和销毁线程的开销
- 线程池的参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:线程在线程池中不被销毁的空闲时间,如果线程池的线程太多,任务比较少,到了这个时间就会销毁线程
- workQueue:任务队列,存放等待执行的任务
- threadFactory:创建线程的任务的工厂,比如给线程命名加上前缀
- handler:拒绝任务处理器,当任务处理不过来时就拒绝处理器
- allowCoreThreadTimeOut:是否允许核心线程超时销毁,这个参数不在构造函数中
说说你对Java 中反射的理解
Java中反射首先是能够获取到Java中反射类的字节码,获取字节码有三种方法。1,Class.forName(className)。2,类名.class。3,this.getClass。然后将字节码中的方法,变量,构造函数等映射成相应的Method,Filed,Constructor等类,这些类提供了丰富的方法可以被我们所使用
单例设计模式
饿汉式:
public class Singleton{
// 直接创建对象
public static Singleton instance = new Singleton();
// 私有化构造函数
private Singleton(){}
// 返回对象实例
public static Singleton getInstance(){
return instance();
}
}
懒汉式
public class Singleton{
// 声明变量
private static volatile Singleton singleton = null;
// 私有化构造函数
private Singleton(){}
// 提供对外方法
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return instance();
}
}
工厂设计模式
工厂模式分成工厂方法模式和抽象工厂模式
-
工厂方法模式:分三种,普通工厂模式,多个工厂方法模式,静态工厂方法模式
- 普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建
public interface Sender{
public void Send();
}
public class MailSender implements Sender{
@Override
public void Send(){
System.out.println("this is mail sender!");
}
}
public class SmsSender implements Sender{
@Override
public void Send(){
System.out.println("this is sms sender!");
}
}
public class SendFactory{
public Sender produces(String type){
if("mail".equals(type)){
return new MailSender();
}else if("sms".equals(type)){
return new SmsSender();
}else{
System.out.println("请输入正确的类型!");
return null;
}
}
}
- 多个工厂方法模式
- 该模式是对普通工厂方法模式的改进,在普通方法模式中,如果传递的字符串出错,则不能正确的创建对象,而多个工厂方法模式提供多个工厂方法,分别创建对象。
public class SendFactory{
public Sender produceMail(){
return new MailSender();
}
public Sender produceSms(){
return new SmsSender();
}
}
public class FactoryTest{
publlic static void main(String[] args){
SendFactory factory = new SendFactory();
Sender sender = factory.produceMail();
sender.send();
}
}
- 抽象工厂模式
工厂模式有一个问题,类的创建依赖工厂类,也就是说,想要扩展程序,必须对工厂进行修改,违反闭包原则。抽象工厂模式,创建多个工厂类,一旦需要增加新的功能,直接添加新的工厂类就可以,不需要修改
public interface Provider{
public Sender produce();
}
public interface Sender{
private void send();
}
public class MailSender implements Sender{
@Override
public void send(){
System.out.println("this is mail sender");
}
}
public class SmsSender implements Sender{
@Override
public void send(){
System.out.println("this is sms sender");
}
}
public class SendSmsFactory implements Provider{
@Override
public Sender produce(){
return new SmsSender();
}
}
public class SendMailFactory implements Provider{
@Override
public Sender produce(){
return new MailSender();
}
}
public class Test{
public static void main(String[] args){
Provider provider = new SendMailFactory();
Sender sender = provider.produce();
sender.send();
}
}
JVM 垃圾回收机制和常见算法
-
垃圾回收机制:sun公司只定义了垃圾回收机制规则而不局限于其实现算法,因此不同的产商生成的虚拟机采用的算法不经相同
-
GC(Garbage Collection)在回收对象前首先必须发现那些无用的对象,常用的搜索算法
- 引用计数器算法(废弃)
引用计数器算法是给每个对象设置一个计数器,,当有地方引用这个对象时,计数器+1,当引用生效时,计算器-1,当计数器为0时,JVM就认为对象不再被使用,是垃圾了
引用计数器实现简单,效率高。但是解决不了循环引用问题(A对象引用B对象,B对象又引用A对象,但A,B对象不被任何其他对象引用),同时每次计数器的增加和减少会带来额外的开销,jdk1.1废弃
- 根搜索算法(使用)
根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链,当一个对象没有被GC Roots的引用链连接的时候,说明对象是不可用的。
GC Roots对象包括:- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区域中的类静态属性引用的对象
- 方法区域中常量引用的对象
- 本地方法栈中的JNI(Navite方法)的引用的对象
-
回收算法
- 标记-清除算法(Mark-Sweep)(DVM使用的算法)
标记-清除算法分两个阶段:“标记”和“清除”。在标记阶段,确定要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记-清除算法是基础的收集算法,标记-清除阶段的效率不高,而且清除后会产生大量不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间
- 复制算法(Copying)
复制算法是把内存分成相等的两块,每次使用其中一块,当垃圾回收时,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高。但由于每次只能使用其中的一半,造成内存的利用率不高。现在JVM使用复制方法收集新生代,由于新生代中大部分对象都是朝生夕死,所以两块内存的比例不是1:1,大约是8:1
- 标记-整理法(Mark-Compact)
标记-整理算法和标记-清除算法一样,但标记-整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记-整理算法提高内存的利用率,并且它适合在收集对象存活时间较长的老年代
- 分代收集(Generational Collection)
分代收集是根据对象的存活时间把内存分成新生代和老年代,根据各个代对象的存活特点,每个代采用不同的垃圾回收机制。新生代采用复制算法,老年代使用标记-整理算法。垃圾算法涉及大量的程序细节,而且不同给虚拟机平台实现的方法也各不相同
谈谈 JVM 的内存结构和内存分配
Java虚拟机将其管辖的内存大致分成三个逻辑部分:方法区(Method Area),java栈和Java堆
内存模型
- 1.方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会再运行时改变。常数池,源代码中的命名常量,String常量和static变量保存再方法区
- 2.Java Stack是一个逻辑概念,特点是后进先出。一个栈的空间可能的连续的,也可能是不连续的。最经典的是方法的调用。Java虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法则对应的方法帧被弹出(pop)。栈中存储的数据也是运行时确定的
- java堆分配(heap alloction)意味着以随意的顺序,再运行时进行存储空间的分配和回收的内存管理模型。堆中存储的数据常常是大小,数量和生命周期在编译时无法确定。Java对象的内存总是在heap中分配
内存分配
- 基础数据类型直接在栈空间分配
- 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收
- 引用数据类型,需要使用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量
- 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完后从栈空间回收
- 局部变量new出来时,在栈空间和堆空间中分配空间,当区部变量生命周期结束,栈空间立即回收,堆空间等待GC回收
- 方法调用时传入的实际参数,先在栈空间分配,在方法调用完后从栈空间释放
- 字符串常量在DATA区域分配,this在堆空间分配
- 数组既可栈空间分配数组名称,又在堆空间分配数据实际大小
Java 的类加载器的种类都有哪些?
- 根类加载器(Bootstrap) C++写的,看不到源码
- 扩展类加载器(Extension):加载位置:jre\lib\ext
- 系统(应用)类加载器(System\App):加载位置classPath
- 自定义加载器(必须继承ClassLoader)
Java 类加载体系之ClassLoader 双亲委托机制
java是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性。
- 类加载体系
- class文件检验器
- 内置于Java虚拟机(及语言)的安全特性
- 安全管理器及JavaAPI
Java程序中的.Java文件编译完成会生成.class文件,而.class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoader在加载过程中会使用“双亲委托机制”来加载.class文件。
BootStrapClassLoader:启动类加载器,该ClassLoader是JVM在启动时创建的,用于加载¥JAVA_HOME&/jre/lib下面的类库。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法获取到启动类加载器的引用,所以不能直接通过引用进行操作
ExtClassLoader:扩展类加载器。该class Loader是sun.misc.Launcher里作为一个内部类ExtClassLoader定义的,ExtClassLoader会加载$JAVA_HOME/jre/lib/ext下的类库
AppClassLoader:应用程序类加载器,该ClassLoader同样是在sun.misc.Lauuncher里作为一个内部类。AppClassLoader会加载Java环境变量CLASSPATH所指定的路径下的类库,而CLASSPATH所指定的路径可以通过System.getProperty("java.class.path")获取,该变量也可以覆盖,使用参数 - cp
CustomClassLoader:自定义类加载器,该ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类,大部分情况使用AppClassLoader就足够了
ClassLoader使用双亲委派机制来加载class文件的,ClassLoader的双亲委派句子是这样的(忽略自定义类加载器CustomClassLoader):
- 当AppClassLoader加载一个class时,首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载委派给BootStrapClassLoader去完成
- 如果BootStrapClassLoader加载失败(在$JAVA_HOME$/jre/lib里未找到该class),会使用ExtClassLoader来尝试加载
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也失败,则会报出ClassNotFoundException
ClassLoader的loadClass(String name,boolean resolve)的源码
protected synchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
// 首先找缓存是否有class
Class c = findLoadedClass(name);
if(c == null){
// 判断有没有父类
try{
if(parent != null){
// 有,用父类递归获取class
c = parent.loadClass(name,false);
}else{
// 没有父类,则通过这个方法来加载
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
}
if( c == null){
// 如果还是没有找到,调用findClass(name)去找这个类
c = findClass(name);
}
}
if(resolve){
resolveClss(c);
}
return c;
}
代码很明朗:首先找缓存(findLoadedClass),没有就判断有没有parent,有就用parent来递归的loadClass,然而ExtClassLoader并没有设置parent,则会通过findBootstrapClassOrNull来加载class,而findBootstrapClassOrNull会通过JNI方法“private native Class findBoootstrapClass(String name)”来使用BootStrapClassLoader来加载class
然后如果parent未找到class,则会调用findClass来加载class,findClass是一个protected的空方法,可以覆盖它以便自定义class加载过程
虽然ClassLoader加载类是使用loadClass方法,但是鼓励用ClassLoader的子类重写findClass(String),而不是重写loadClass,这样就不会覆盖类加载默认的双亲委派机制
双亲委派机制的安全性
例如:ClassLoader加载的class文件来源很多,比如编译器生成的class,或者网络下载的字节码,而一些来源的class文件是不可靠的。比如我们自定义java.util.Integer类来覆盖jdk中默认的Integer
public class Integer{
public Integer(int value){
Sstem.exit(0);
}
}
初始化这个Integer构造器会退出JVM,破坏程序的正常运行,如果双亲委派机制的话该Integer类永远不会被调用,以委托BootStrapClassLoader加载后会加载JDK 中Integer而不会加载自定义的Integer
public static void main(Stirng[] args){
Integer i = new Integer(1);
System.err.println(i);
}
执行时JVM未在new Integer(1)时退出,寿命未定义的Integer,于是保证了安全性
Java 中为什么会有 GC 机制呢
- 安全性考虑
- 减少内存泄漏
- 减少程序员的工作量
对于Java 的 GC 哪些内存需要回收
内存运行时JVM会有一个运行时数据区来管理内存。它主要包括5个部分:程序计数器(Program Counter Register),虚拟机栈(VM Stack),本地方法栈(Navite Method Stack),方法区(Method Area),堆(Heap)
其中程序计数器,虚拟机栈,本地方法栈是每个线程私有的内存空间,随线程生死,因此这3个区域的内存分配和回收是确定的,无需考虑回收问题
方法区和堆不同,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注这部分内存
Java 的 GC 什么时候回收垃圾
主流判定一个对象已死的方法是:可达性分析(Reachability Analysis)。所有生成的对象都是一个称为“GC rOOts”的根的子树。从GC Roots开始向下搜索,搜索经过的路径称为引用链(Reference Chain)当一个对象到GC Roots没有任何引用链可以到达,就称这个对象是不可达的(不可用的),也就是可以被GC回收了
根据不同的需求,给出以下4种引用,根据引用类型的不同,GC回收时也有不同的操作
- 强引用(Strong Reference):Object object = new Object();只要强引用还存在,GC永远不会回收掉被引用的对象。
- 软引用(Soft Reference):描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象列入回收范围进行二次回收(即系统将发生内存溢出,才会对它们进行回收)
- 弱引用(Weak Reference):程序比软引用还要弱,这些对象只能生存到下次GC之前。当GC工作时,无论内存是否足够都会将其回收(即只要进行GC,就会对他们进行回收)
- 虚引用(Phoantom Reference):一个对象是否存在虚引用,完全不会对其生存时间构成影响
关于方法区种需要回收的是一些废弃的常量和无用的类
- 废弃的常量回收。这里看引用计数就可以。
- 无用的类回收,什么是无用的类
- 该类的所有实例被回收,也就是Java堆种不存在该类的任何实例
- 加载类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射类访问该类的方法
对于堆种的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。而根据我们实际引用的不同需求,又分成4种引用,每种引用的回收机制也是不同的
对于方法区种的常量和类,当一个常量没有任何对象引用它,它就可以被回收。而对于类,如果可以判定它为无用类,就可以被回收
在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?
引起内存溢出的原因有很多种,常见的有以下几种:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
- 代码中存在死循环或者循环产生过多重复的对象实体
- 使用的第三方软件中的BUG
- 启动参数内存值设定过小
内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其他异常或错误
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置