-
2020-12-29 14:35:13
c#中的对象生命周期
类
是定义在代码文件中,保存在硬盘上 ,是对象的蓝本,它描述了对象在内存中大概是什么样子的。
对象:
我们都知道.net将值类型存储在栈中,引用类型存储在堆中,这样做的原因是栈中的数据是轻量级的,而堆中的数据是重量级,目的是在应用程序在操作它们的时候比较方便存取,从而提高程序的运行速度。创建一个对象实例,用new+类名+(),就创建了一个对象实例,创建的这个对象实例是引用类型,被存储在托管堆中,以后就不用管它了,new关键字返回一个对象实例存在的地址,这个存储地址(引用)变量,被放在栈中,实际上应用程序在运行时都是操作的这个引用。
简单了解一下
无论是指类型的变量或是类类型的变量,其存储单元都是在栈中分配的,唯一不同的是类类型的变量实际上存储的是该类对象的指针,相当于vc6中的CType*,只是在.net平台的语言中将指针的概念屏蔽掉了。我们都知道栈的一大特点就是LIFO(后进先出),这恰好与作用域的特点相对应(在作用域的嵌套层次中,越深层次的作用域,其变量的优先级越高)。因此,再出了“}”后,无论是值类型还是类类型的变量(对象指针)都会被立即释放(值得注意的是:该指针所指向的托管堆中的对象并未被释放,正等待GC的回收)。.NET中的栈空间是不归GC管理的,GC仅管理托管堆。
我想就我的理解简要说明一下:
1、GC只收集托管堆中的对象。
2、所有值类型的变量都在栈中分配,超出作用域后立即释放栈空间,这一点与VC6完全
一样。
3、区别类类型的变量和类的对象,这是两个不同的概念。类类型的变量实际上是该类对
象的指针变量。如C#中的定义CType myType;与VC6中的定义CType* myType;是完全一
样的,只是.net语言将*号隐藏了。与VC6相同,必须用new 关键字来构造一个对象,如(C#):CType myType=new CType();其实这一条语句有两次内存分配,一次是为类类 型变量myType在栈中分配空间(指针类型所占的空间,对32位系统分配32位,64位 系统则分配64位,在同一个系统中,所有指针类型所占的内存空间都是一样的,而 不管该类型的指针所指向的是何种类型的对象),另一次是在托管堆(GC所管理的 堆)中构造一个CType类型的对象并将该对象的起始地址赋给变量myType。正因为如 此才造成了在同一个作用域中声明的类类型的变量和该类型的对象的生存期不一样。
更多相关内容 -
理解C#对象生命周期
2021-03-01 18:12:01一个类只是一个描述这种类型的实例(instance)在内存中布局的蓝图。当然,类是定义在一个代码文件中(在C#中代码文件以.cs作为后缀)。一个简单的Car类,定义在一个叫做SimpleGC的C#ConsoleApplication中:当一个类被... -
Java对象的生命周期
2021-05-25 19:16:53文章目录生命周期概览加载(loading)验证(Verifaction)准备(Preparation)解析(Resolution)解析阶段的静态绑定和动态绑定初始化(Initialization)类的初始化阶段对象的初始化阶段触发对象初始化的场景使用(Using)卸载...文章目录
生命周期概览
类生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个流程,其中验证、准备、解析这三个流程也被称之为连接(除了解析外,其他流程是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型),如下图所示:
加载(loading)
在此阶段,主要做了三件事:
-
类加载器(ClassLoader)通过一个类的全限定名来获取其定义的二进制字节流(class文件),并加载到jvm内存中(如果已经获取过则直接返回其Class对象)。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
-
在Java堆中生成一个代表这个类的
java.lang.Class
对象,作为对方法区中这些数据的访问入口。
关于类加载模型的详细介绍请参考这篇博文:JVM双亲委派模型
验证(Verifaction)
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:
- 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
- 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
- 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
对于该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
- 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
解析(Resolution)
只有解析可以与其它阶段交叉进行
解析阶段就是虚拟机将常量池中的符号引用转化为直接引用的过程。
-
符号引用:
符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。
-
直接引用可以是以下三种情况:
- 1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 3)一个能间接定位到目标的句柄 直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了
解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。
对同一个符号引用可以进行多次解析请求,但是默认情况下虚拟机会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
、CONSTANT_InterfaceMethodref_info
四种常量类型。解析主要分为两种情况:
1. 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。 2. 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系递归搜索其父类,直至查找结束。
解析阶段的静态绑定和动态绑定
- 静态绑定(static binding):也叫前期绑定,在程序执行前,该方法就能够确定所在的类,此时由编译器或其它连接程序实现,比如构造方法或者被static或final修饰的。
- 动态绑定(auto binding):也叫后期绑定,在运行时,虚拟机根据具体对象的类型进行绑定,或者说是只有对象在虚拟机中创建了之后,才能确定方法属于哪一个对象,比如含有泛型的。
初始化(Initialization)
到了此阶段,说明对于该对象对应类(即Class对象)的加载流程已经完成,才真正开始执行对象实例的创建流程。
需要注意的是加载、验证和装备阶段只会进行一次,而初始化是可以重复进行的。
在准备阶段,类变量已经被初始化过一次系统提供的默认值,而在初始化阶段,则是根据java代码中实际指定的值去初始化类变量和其它内容。
类的初始化阶段
类的初始化即是执行类构造器
<clinit>
()方法的过程,规则如下:<clinit>
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,则只可以赋值,而不能访问。<clinit>
()方法与实例构造器<init>
()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>
()方法执行之前,父类的<clinit>
()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>
()方法的类肯定是java.lang.Object
<clinit>
()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>
()方法。- 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成
<clinit>
()方法。但是接口鱼类不同的是:执行接口的<clinit>
()方法不需要先执行父接口的<clinit>
()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>
()方法。 - 虚拟机会保证一个类的
<clinit>
()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>
()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>
()方法完毕。如果在一个类的<clinit>
()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
对象的初始化阶段
<init>
是对象实例构造器所调用的方法,Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,如果存在父类,则先递归处理父类的语句块、变量。在
<init>
()方法执行完毕后则先递归调用父类的构造方法,直到Object类的构造方法被调用后,再依次执行下面的构造方法触发对象初始化的场景
- 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
- 通过反射方式实例化对象,如
Class.forName
()方法。 - 初始化子类的时候,会触发父类的初始化。
- 虚拟机启动时,初始化一个执行主类(也就是直接调用main方法)。
- 使用(反)序列化机制创建对象
- 使用
JDK7
的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、RE_invokeStatic
的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
初始化的原则:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序依次递归运行父类中的变量赋值语句和静态语句。
具体案例请参考:class的内容在jvm中的加载顺序使用(Using)
当一个对象初始化完成后就生成了一个对象的实例。
- 访问类变量和方法不需要实例化
- 静态代码块只会被调用一次,而实例的代码块则是每次初始化调用一次
- 通过final修饰符可以防止类被继承或者变量的值被修改
- 设置访问权限限制其它对象的访问
可以通过对象的
getClass()
方法获取到该对象对应的class对象并使用。java栈上的reference只存储了对象的引用,至于如何通过这个引用去定位、访问堆中的对象的具体位置则取决于虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针。
我们常用的Sun HotSpot 虚拟机则是使用直接指针方式进行对象访问的。
卸载(Unloading)
对象的卸载
当类被加载、连接和初始化后,它就可以被其它对象或类所调用,当一个对象不再被引用,即不可达时,该对象就会被JVM垃圾回收器所回收掉,从而结束对象的生命周期,此时类在方法区内的数据并不一定会被卸载。
类的卸载
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载
java虚拟机自带的类加载器包含启动类加载器、扩展类加载器、应用类加载器,Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载类的Class对象,因此这些Class对象始终是可触及的。
如果出现以下三种情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有被任何类或对象被引用,无任何类或对象通过反射访问该类的方法
如果满足以上三个条件情况下,JVM就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,这样一来java类的整个生命周期就结束了。
注意:本文中所讲到的类实际上指的是类的Class对象
面试十问
读完本文你应该能回答以下问题了:
-
一个类可以生成多少个Class对象?
-
Class对象的生命周期有哪几个阶段?
-
类的连接是哪几个阶段?
-
一个类变量在对象实例化过程中最多可以被赋值几次?
-
静态绑定和动态绑定的区别?
-
什么情况下会触发对象的初始化?
-
对象初始化过程执行了哪些操作?
-
父类静态代码块和子类静态代码块哪个先执行?
-
以下代码,哪个类先被JVM加载?
MyObject myObject = new MyObject(); Object o = new Object();
-
通过克隆方法复制一个对象的过程中会重新初始化吗?
-
-
springboot bean对象生命周期
2020-10-10 22:28:14Bean对象生命周期 Bean对象的生命周期主要由3部分组成:bean对象的初始化过程,bean对象在ioc容器中被使用,bean对象的销毁过程,具体可见下图: 初始化bean对象过程 方法 描述 bean对象实例化 调用构造...Bean对象生命周期
Bean对象的生命周期主要由3部分组成:bean对象的初始化过程,bean对象在ioc容器中被使用,bean对象的销毁过程,具体可见下图:
初始化bean对象过程
方法 描述 bean对象实例化 调用构造函数为对象分配内存空间 setBeanName方法调用 如果对象实现了BeanNameAware接口,则调用该方法 setBeanFactory方法调用 如果对象实现了BeanFactoryAware接口,则调用该方法 setApplicationContext方法调用 如果实现了ApplicationContextAware接口,且spring ioc容器也是ApplicationContext的接口实现类,则调用该方法 postProcessBeforeInitialization方法调用 如果实现了BeanPostProcessor接口,则调用该方法 postConstruct注解方法调用 如果bean对象定义了@PostConstruct注解的方法,则调用该方法 afterPropertiesSet方法调用 如果实现了InitializingBean接口,则调用该方法 自定义init-method方法调用 如果设置了自定义初始化方法,则调用自定义初始化方法 postProcessAfterInitialization方法调用 如果实现了BeanPostProcessor接口,则调用该方法 销毁bean对象过程
方法 描述 PreDestroy注解方法调用 如果bean对象定义了@PreDestroy注解的方法,则调用该方法 destory方法调用 如果实现了DisposableBean接口,则调用该方法 自定义destroyMethod方法调用 如果自定义了销毁方法,则调用该方法 代码实例
下面的代码定义了一个bean类SpringLifeCycleBean用来对bean对象的生命周期进行测试,将上述不同阶段的方法都封装在该类中。
package com.mary; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; /** * Created by gaoling on 2020/10/10. */ public class SpringLifeCycleBean implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, BeanPostProcessor, DisposableBean{ public SpringLifeCycleBean(){ System.out.println("Constructor method invoked"); } @Override public void setBeanName(String s) { System.out.println("BeanNameAware setBeanName method inovked, name: " + s); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("BeanFactoryAware setBeanFactory method inovked, beanFactory: " + beanFactory.getClass().getName()); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("ApplicationContextAware setApplicationContext method inovked, applicationContext: " + applicationContext.getClass().getName()); } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor postProcessBeforeInitialization method inovked, beanName: " + beanName); return null; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor postProcessAfterInitialization method inovked, beanName: " + beanName); return null; } @PostConstruct public void init(){ System.out.println("PostConstruct method invoked"); } @PreDestroy public void preDestroy(){ System.out.println("PreDestroy method invoked"); } @Override public void destroy() throws Exception { System.out.println("DisposableBean destroy method invoked"); } public void customInit(){ System.out.println("customInit method invoked"); } public void customDestroy(){ System.out.println("customDestroy method invoked"); } } package com.mary; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; /** * Created by gaoling on 2020/10/10. */ @SpringBootApplication public class AppStarter { public static void main(String[] args){ SpringApplication.run(AppStarter.class, args); } @Bean(destroyMethod = "customDestroy", initMethod = "customInit") public SpringLifeCycleBean lifeCycleBean(){ SpringLifeCycleBean lifeCycleBean = new SpringLifeCycleBean(); return lifeCycleBean; } }
代码执行结果如下
Constructor method invoked BeanPostProcessors (for example: not eligible for auto-proxying) BeanNameAware setBeanName method inovked, name: lifeCycleBean BeanFactoryAware setBeanFactory method inovked, beanFactory: org.springframework.beans.factory.support.DefaultListableBeanFactory ApplicationContextAware setApplicationContext method inovked, applicationContext: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext PostConstruct method invoked InitializingBean afterPropertiesSet method invoked customInit method invoked BeanPostProcessor postProcessBeforeInitialization method inovked, beanName: org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat BeanPostProcessor postProcessAfterInitialization method inovked, beanName: org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat
-
Java 对象生命周期和类生命周期
2015-04-21 16:57:30Java 对象生命周期 在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected...Java 对象生命周期
在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。
创建阶段
在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:
(1)为对象分配存储空间。
(2)开始构造对象。
(3)递归调用其超类的构造方法。
(4)进行对象实例初始化与变量初始化。
(5)执行构造方法体。
上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在 Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:
public class A { … } 这个声明等同于下面的声明: public class A extends java.lang.Object { … }
上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。
下面是在创建对象时的几个关键应用规则:
(1)避免在循环体中创建对象,即使该对象占用内存空间不大。
(2)尽量及时使对象符合垃圾回收标准。
(3)不要采用过深的继承层次。
(4)访问本地变量优于访问类中的变量。
关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:
… … for (int i = 0; i < 10000; ++i) { Object obj = new Object(); System.out.println("obj= "+ obj); } … …
上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:
… … Object obj = null; for (int i = 0; i < 10000; ++i) { obj = new Object(); System.out.println("obj= "+ obj); } … …
采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。
另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:
public class A { private Hashtable table = new Hashtable (); public A() { // 将Hashtable对象table初始化了两次 table = new Hashtable(); } }
正确的方式为:
public class B { private Hashtable table = new Hashtable (); public B() { } }
不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。
图2-5 初始化对象多次所带来的性能差别
看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。
应用阶段
当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:
◆系统至少维护着对象的一个强引用(Strong Reference);
◆所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。
上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。
Java对象引用的结构层次示意如图2-6所示。
图2-6 对象引用的结构层次示意
由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。
1.强引用
强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。
2.软引用
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出
OutOfMemoryError
之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:… … import java.lang.ref.SoftReference; … A a = new A(); … // 使用 a … // 使用完了a,将它设置为soft 引用类型,并且释放强引用; SoftReference sr = new SoftReference(a); a = null; … // 下次使用时 if (sr!=null) { a = sr.get(); } else{ // GC由于内存资源不足,可能系统已回收了a的软引用, // 因此需要重新装载。 a = new A(); sr=new SoftReference(a); } … …
软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。
3.弱引用
弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:
… … import java.lang.ref.WeakReference; … A a = new A(); … // 使用 a … // 使用完了a,将它设置为weak 引用类型,并且释放强引用; WeakReference wr = new WeakReference (a); a = null; … // 下次使用时 if (wr!=null) { a = wr.get(); } else{ a = new A(); wr = new WeakReference (a); } … …
弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。
… … WeakReference wr = new WeakReference(obj); 或 WeakReference wr = new WeakReference(obj, false); … …
4.虚引用
虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 java 终结机制更灵活的方式调度 pre-mortem 清除操作。
&注意 在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
不可视阶段
在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。
… … public void process () { try { Object obj = new Object(); obj.doSomething(); } catch (Exception e) { e.printStackTrace(); } while (isLoop) { // ... loops forever // 这个区域对于obj对象来说已经是不可视的了 // 因此下面的代码在编译时会引发错误 obj.doSomething(); } } … …
如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。
不可到达阶段
处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
可收集阶段、终结阶段与释放阶段
对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:
(1)垃圾回收器发现该对象已经不可到达。
(2)finalize方法已经被执行。
(3)对象空间已被重用。
当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。
Java 类生命周期
最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前国内java方面的教材大多只是告诉你“怎样做”,但至于“为什么这样做”却不多说,所以造成大家在基础和原理方面的知识比较匮乏,所以笔者今天就斗胆来讲一下这个问题,权当抛砖引玉,希望对在这个问题上有疑惑的朋友有所帮助,文中有说的不对的地方,也希望各路高手前来指正。
首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周期中扮演着比较重要的角色:
- 方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
- 常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
- 堆区:用于存放类的对象实例。
- 栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
除了以上四个内存区域之外,jvm中的运行时内存区域还包括本地方法栈和程序计数器,这两个区域与java类的生命周期关系不是很大,在这里就不说了,感兴趣的朋友可以自己百度一下。
类的生命周期
当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。
一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:
下面我们就依次来说一说这五个阶段。
加载
在java中,我们经常会接触到一个词——类加载,它和这里的加载并不是一回事,通常我们说类加载指的是类的生命周期中加载、连接、初始化三个阶段。在加载阶段,java虚拟机会做什么工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取。另外,还有下面几种方式也比较常用:
- 从网络中获取:比如10年前十分流行的Applet。
- 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。
- 从非class文件中获取,其实这与直接从class文件中获取的方式本质上是一样的,这些非class文件在jvm中运行之前会被转换为可被jvm所识别的字节码文件。
对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。
加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是连接阶段。有一点需要注意,就是有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。
连接
连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
- 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
- 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
- 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
- 引用类型的默认值为null。
- 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
- 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。
初始化
如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:
- 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
- 通过反射方式执行以上三种行为。
- 初始化子类的时候,会触发父类的初始化。
- 作为程序入口直接运行时(也就是直接调用main方法)。
除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。请看主动引用的示例代码:
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- class InitClass{
- static {
- System.out.println("初始化InitClass");
- }
- public static String a = null;
- public static void method(){}
- }
- class SubInitClass extends InitClass{}
- public class Test1 {
- /**
- * 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时
- * 导致Test1初始化,这一点很好理解,就不特别演示了。
- * 本代码演示了前三种情况,以下代码都会引起InitClass的初始化,
- * 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。
- * @param args
- * @throws Exception
- */
- public static void main(String[] args) throws Exception{
- // 主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。
- // new InitClass();
- // InitClass.a = "";
- // String a = InitClass.a;
- // InitClass.method();
- // 主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。
- // Class cls = InitClass.class;
- // cls.newInstance();
- // Field f = cls.getDeclaredField("a");
- // f.get(null);
- // f.set(null, "s");
- // Method md = cls.getDeclaredMethod("method");
- // md.invoke(null, null);
- // 主动引用引起类的初始化三:实例化子类,引起父类初始化。
- // new SubInitClass();
- }
- }
上面的程序演示了主动引用触发类的初始化的四种情况。
类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。先看一个例子,首先建两个类用来显示赋值操作:
- public class Field1{
- public Field1(){
- System.out.println("Field1构造方法");
- }
- }
- public class Field2{
- public Field2(){
- System.out.println("Field2构造方法");
- }
- }
下面是演示初始化顺序的代码:
- class InitClass2{
- static{
- System.out.println("运行父类静态代码");
- }
- public static Field1 f1 = new Field1();
- public static Field1 f2;
- }
- class SubInitClass2 extends InitClass2{
- static{
- System.out.println("运行子类静态代码");
- }
- public static Field2 f2 = new Field2();
- }
- public class Test2 {
- public static void main(String[] args) throws ClassNotFoundException{
- new SubInitClass2();
- }
- }
上面的代码中,初始化的顺序是:第03行,第05行,第11行,第13行。第04行是声明操作,没有赋值,所以不会被运行。而下面的代码:
- class InitClass2{
- public static Field1 f1 = new Field1();
- public static Field1 f2;
- static{
- System.out.println("运行父类静态代码");
- }
- }
- class SubInitClass2 extends InitClass2{
- public static Field2 f2 = new Field2();
- static{
- System.out.println("运行子类静态代码");
- }
- }
- public class Test2 {
- public static void main(String[] args) throws ClassNotFoundException{
- new SubInitClass2();
- }
- }
初始化顺序为:第02行、第05行、第10行、第12行,各位可以运行程序查看结果。
在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。
使用
类的使用包括主动引用和被动引用,主动引用在初始化的章节中已经说过了,下面我们主要来说一下被动引用:
- 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会引起类的初始化。
- 引用类的常量,不会引起类的初始化。
被动引用的示例代码:
- class InitClass{
- static {
- System.out.println("初始化InitClass");
- }
- public static String a = null;
- public final static String b = "b";
- public static void method(){}
- }
- class SubInitClass extends InitClass{
- static {
- System.out.println("初始化SubInitClass");
- }
- }
- public class Test4 {
- public static void main(String[] args) throws Exception{
- // String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化
- // String b = InitClass.b;// 使用类的常量不会引起类的初始化
- SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化
- }
- }
最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段。
卸载
关于类的卸载,笔者在单例模式讨论篇:单例模式与垃圾回收一文中有过描述,在类使用完之后,如果满足下面的情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
总结
做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。
-
大数据全生命周期中关键问题研究及应用
2018-03-15 15:06:57研究对象,对大数据的存储、处理、挖掘三大关键问题进行 研究。根据大数据特点进行分类;建立基于改进稀疏矩阵、 指标维度和指标数据分离方式的大数据压缩存储模型,实现 大数据的压缩存储和动态扩展 -
堆栈 java对象生命周期
2018-08-21 10:36:25java运行时内存区域的各个部分中,程序计数器、虚拟机栈、本地方法栈这三个区域的生命周期和线程相关,栈中的栈帧随着方法的进入和退出执行着进栈和出栈,每一个栈帧分配多少内存基本上在类的结构确定下来的时候就... -
java类和对象的生命周期
2018-07-09 06:55:00Java 对象生命周期在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结... -
vue生命周期总结
2022-03-30 16:02:30一、vue生命周期 vue生命周期是什么? Vue生命周期是指vue实例对象从创建之初到销毁的过程,vue所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数可以实现组件数据管理和DOM渲染两... -
项目管理 之三 项目生命周期、项目管理生命周期、产品生命周期、阶段方法、质量管理、配置管理等
2021-02-01 15:52:09项目生命周期 项目的生命周期是描述项目从开始到结束所经历...项目设阶段的目的是为了管控的需要,每一个阶段都可以当成是一个子项目,每一个阶段中都可以执行项目管理生命周期定义的五大过程组。阶段结束时要进行阶段 -
描述一下Spring Bean的生命周期
2021-04-01 21:46:21描述一下Spring Bean的生命周期 解析类得到BeanDefinition 如果有多个构造方法,则要推断构造方法 确定好构造方法后,进行实例化得到一个对象 对对象的加了@Autowired注解的属性进行属性填充 回调Aware方法,比如... -
描述Servlet的生命周期
2019-07-04 14:58:43所以说,Servlet是在接收到第一个处理请求时被创建的,也可以通过配置load-on-startup的值为1设置Servlet随服务器启动就创建对象。(特殊业务需求才使用程序启动自动创建Servlet对象,否则会增加服务器的启... -
C++对象生命周期管理--通过引用计数指针对象来封装管理对象生命周期
2015-03-04 20:04:53在实际的软件开发过程中,对象的生命周期管理一直是软件开发过程中的一个重点和难点,在C++标准库中也逐渐的出现了一系列的智能指针(标准库/boost库中都涉及到),但是这种智能指针的设计,只是对Object进行封装,... -
线程的生命周期?线程有几种状态?
2021-10-22 13:54:31在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。 尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换... -
Spring IOC -bean对象的生命周期详解
2016-07-16 15:29:55生命周期执行的过程如下: 1)spring对bean进行实例化,默认bean是单例 2)spring对bean进行依赖注入 3)如果bean实现了BeanNameAware接口,spring将bean的id传给setBeanName()方法 4)如果bean实现了BeanFactoryAware... -
详解Unity中的生命周期函数
2022-04-02 21:04:26Unity 中,有一个特别重要的知识点,生命周期函数。这些东西全部都是系统定义好的,运行时自动调用,但需要继承 MonoBehaviour 类才能使用。这个类是从 Unity 中创建脚本就自动继承了。正是因为继承了 MonoBehaviour... -
Hibernate对象的生命周期及三种状态
2016-08-08 16:35:53Hibernate对象生命周期及三种状态:瞬时状态 (Transient)、持久状态(Persistent)、脱管状态(Detached)的理解。 -
关于Servlet对象的生命周期
2017-02-15 20:18:08生命周期表示一个java对象从最初被创建到最终被销毁,经历的所有过程。 2、Servlet对象的生命周期是谁来管理的?程序员可以干涉吗? Servlet对象的生命周期,javaweb程序员是无权干涉的,包括该Servlet对象的相关... -
面试专题:描述一下Spring Bean的生命周期
2021-02-27 19:40:53Spring Bean的生命周期是Spring中面试频率相当高的面试题,如果了解bean的生命周期对于Spring的扩展和运行流程可以掌握的很好,这个问题主要考察开发人员对执行的流程和以及有哪些扩展点,对我们写代码也会有很多... -
视图控制对象的生命周期
2012-05-29 11:20:40下面是某个视图控制对象的生命周期图 -
Servlet的生命周期
2022-05-31 18:39:37Servlet的生命周期 ...servlet生命周期三个方法: init()初始化阶段 service()处理客户端请求阶段 destroy()终止阶段 容器(tomcat等)装载servlet 实例化阶段 1.1 当客户端首次发送第一次请求后,由Serv -
unity -- 生命周期
2021-10-15 13:53:43unity -- 生命周期 -
一文读懂 Spring Bean 的生命周期
2021-07-05 23:02:56因为 Spring Bean 的生命周期是除了 IoC、AOP 几个核心概念之外最重要概念,大家务必拿下。可 Spring 源代码又比较复杂,跟着跟着就不知道跟到哪里去了,不太好拿下呀。这倒是真的,而且网上一上来就各种贴流程源码... -
java对象的生命周期
2021-03-06 22:30:24java对象的生命周期包括 创建、使用、回收 三个过程,本文将详细剖析每个过程。一、对象的创建当运行时执行代码遇到new指令、反序列化、反射、克隆时会创建新的对象。创建对象的过程如下图:对象创建过程中重要步骤... -
servlet的生命周期 (四个阶段).
2021-10-26 08:25:51servlet容器启动时,读取web.xml文件的信息,指定servlet对象,根据配置文件信息创建servletConfig对象,并将参数信息传递给init()对象 3、响应客户请求响应阶段 客户端第一次进行访问servlet进行执行 ----- doget ... -
Hibernate实体类对象生命周期的3种状态
2016-08-16 17:15:15Hibernate的对象有3种状态,分别为:瞬时态(Transient)、持久态(Persistent)、脱管态(Detached).处于持久态的对象也称为PO(PersistenceObject),瞬时对象和脱管对象也称为VO(ValueObject). -
常用的几种软件生命周期模型
2019-09-27 12:15:08瀑布模型将软件生命周期的各项活动自上而下如瀑布流水依次连接,上一阶段的输出作为下一阶段的输入,同时,在每一个阶段如果发现问题,都可以逆流而上,向上一阶段进行反馈,然后做适当的修改,但是只能逐层反馈,不... -
servlet的生命周期描述
2018-10-21 19:37:55加载并实例化 -> 初始化 -> 应用阶段(请求处理) -> 销毁 1.创建 Servlet 实例。 2.Web 容器调用 Servlet 的init()方法,对Servlet 进行初始化。 ...3.Servlet 初始化后,将一直存在于容器中,用于响应...