`
shuaijie506
  • 浏览: 136147 次
  • 性别: Icon_minigender_1
  • 来自: 郑州
社区版块
存档分类
最新评论

java对象的生命周期及回收

    博客分类:
  • java
阅读更多

在网上看到一篇不错的文章,记录下来备忘。

 

要理解java对象的生命周期,我们需要要明白两个问题,

 

 1、java是怎么分配内存的 ,2、java是怎么回收内存的。

 

喜欢java的人,往往因为它的内存自动管理机制,不喜欢java的人,往往也是因为它的内存自动管理。我属于前者,这几年的coding经验让我认识到,要写好java程序,理解java的内存管理机制是多么的重要。任何语言,内存管理无外乎分配和回收,在C中我们可以用malloc动态申请内存,调用free释放申请的内存;在C++中,我们可以用new操作符在堆中动态申请内存,编写析构函数调用delete释放申请的内存;那么在java中究竟是内存怎样管理的呢?要弄清这个问题,我们首先要了解java内存的分配机制,在java虚拟机规范里,JVM被分为7个内存区域,但是规范这毕竟只是规范,就像我们编写的接口一样,虽然最终行为一致,但是个人的实现可能千差万别,各个厂商的JVM实现也不尽相同,在这里,我们只针对sun的Hotspot虚拟机讨论,该虚拟机也是目前应用最广泛的虚拟机。

 

  虚拟器规范中的7个内存区域分别是三个线程私有的和四个线程共享的内存区,线程私有的内存区域与线程具有相同的生命周期,它们分别是: 指令计数器、 线程栈和本地线程栈,四个共享区是所有线程共享的,在JVM启动时就会分配,分别是:方法区、 常量池、直接内存区和堆(即我们通常所说的JVM的内存分为堆和栈中的堆,后者就是前面的线程栈)。接下来我们逐一了解这几个内存区域。

 

    1 指令计数器。我们都知道java的多线程是通过JVM切换时间片运行的,因此每个线程在某个时刻可能在运行也可能被挂起,那么当线程挂起之后,JVM再次调度它时怎么知道该线程要运行那条字节码指令呢?这就需要一个与该线程相关的内存区域记录该线程下一条指令,而指令计数器就是实现这种功能的内存区域。有多少线程在编译时是不确定的,因此该区域也没有办法在编译时分配,只能在创建线程时分配,所以说该区域是线程私有的,该区域只是指令的计数,占用的空间非常少,所以虚拟机规范中没有为该区域规定OutofMemoryError。

 

   2 线程栈。先让我看以下一段代码:

 

 

class Test{
    public static void main(String[] args) {
        Thread th = new Thread();
        th.start();
    }
}

 

 

  在运行以上代码时,JVM将分配一块栈空间给线程th,用于保存方法内的局部变量,方法的入口和出口等,这些局部变量包括基本类型和对象引用类型,这里可能有人会问,java的对象引用不是分配在堆上吗?有这样疑惑的人,可能是没有理解java中引用和对象之间的区别,当我们写出以下代码时:

 

public Object test(){
    Object obj = new Object();
    return obj;
}

 

 

  其中的Object obj就是我们所说的引用类型,这样的声明本身是要占用4个字节,而这4个字节在这里就是在栈空间里分配的,准确的说是在线程栈中为test方法分配的栈帧中分配的,当方法退出时,将会随栈帧的弹出而自动销毁,而new Object()则是在堆中分配的,由GC在适当的时间收回其占用的空间。每个栈空间的默认大小为0.5M,在1.7里调整为1M,每调用一次方法就会压入一个栈帧,如果压入的栈帧深度过大,即方法调用层次过深,就会抛出StackOverFlow,,SOF最常见的场景就是递归中,当递归没办法退出时,就会抛此异常,Hotspot提供了参数设置改区域的大小,使用-Xss:xxK,就可以修改默认大小。 3 本地线程栈.顾名思义,该区域主要是给调用本地方法的线程分配的,该区域和线程栈的最大区别就是,在该线程的申请的内存不受GC管理,需要调用者自己管理,JDK中的Math类的大部分方法都是本地方法,一个值得注意的问题是,在执行本地方法时,并不是运行字节码,所以之前所说的指令计数器是没法记录下一条字节码指令的,当执行本地方法时,指令计数器置为undefined。 接下来是四个线程共享区。 1 方法区。这块区域是用来存放JVM装载的class的类信息,包括:类的方法、静态变量、类型信息(接口/父类),我们使用反射技术时,所需的信息就是从这里获取的。 2 常量池。当我们编写如下的代码时:

 

class Test1{
     private final int size=50;
 }

 

 

  这个程序中size因为用final修饰,不能再修改它的值,所以就成为常量,而这常量将会存放在常量区,这些常量在编译时就知道占用空间的大小,但并不是说明该区域编译就固定了,运行期也可以修改常量池的大小,典型的场景是在使用String时,你可以调用String的 intern(),JVM会判断当前所创建的String对象是否在常量池中,若有,则从常量区取,否则把该字符放入常量池并返回,这时就会修改常量池的大小,比如JDK中java.io.ObjectStreamField的一段代码:

 

ObjectStreamField(Field field, boolean unshared, boolean showType) {
    this.field = field;
    this.unshared = unshared;
    name = field.getName();
    Class ftype = field.getType();
    type = (showType || ftype.isPrimitive()) ? ftype : Object.class;
    signature = ObjectStreamClass.getClassSignature(ftype).intern();
}

 

 

  这段代码将获取的类的签名放入常量池。HotSpot中并没有单独为该区域分配,而是合并到方法区中。 3 直接内存区。直接内存区并不是JVM可管理的内存区。在JDK1.4中提供的NIO中,实现了高效的R/W操作,这种高效的R/W操作就是通过管道机制实现的,而管道机制实际上使用了本地内存,这样就避免了从本地源文件复制JVM内存,再从JVM复制到目标文件的过程,直接从源文件复制到目标文件,JVM通过DirectByteBuffer操作直接内存。 4 堆。主角总是最后出场,堆绝对是JVM中的一等公民,绝对的主角,我们通常所说的GC主要就是在这块区域中进行的,所有的java对象都在这里分配,这也是JVM中最大的内存区域,被所有线程共享,成千上万的对象在这里创建,也在这里被销毁。 java内存分配到这就算是一个完结了,接下来我们将讨论java内存的回收机制, 内存回收主要包含以下几个方面理解: 第一,局部变量占用内存的回收,所谓局部变量,就是指在方法内创建的变量,其中变量又分为基本类型和引用类型。如下代码:

 

public void test(){
    int x=1;
    char y='a';
    long z=10L;
} 

 

 

  变量x y z即为局部变量,占用的空间将在test()所在的线程栈中分配,test()执行完了后会自动从栈中弹出,释放其占用的内存,再来看一段代码:

 

public void test2(){
    Date d = new Date();
    System.out.println("Now is "+d);
}

 

 

  我们都知道上述代码会创建两个对象,一个是Date d另一个是new Date。Date d叫做声明了一个date类型的引用,引用就是一种类型,和int x一样,它表明了这种类型要占用多少空间,在java中引用类型和int类型一样占用4字节的空间,如果只声明引用而不赋值,这4个字节将指向JVM中地址为0的空间,表示未初始化,对它的任何操作都会引发空指针异常。 如果进行赋值如d = new Date()那么这个d就保存了new Date()这个对象的地址,通过之前的内存分配策略,我知道new Date()是在jvm的heap中分配的,其占用的空间的回收我们将在后面着重分析,这里我们要知道的是这个Date d所占用的空间是在test2()所在的线程栈分配的,方法执行完后同样会被弹出栈,释放其占用的空间。 第二.非局部变量的内存回收,在上面的代码中new Date()就和C++里的new创建的对象一样,是在heap中分配,其占用的空间不会随着方法的结束而自动释放需要一定的机制去删除,在C++中必须由程序员在适当时候delete掉,在java中这部分内存是由GC自动回收的,但是要进行内存回收必须解决两问题:那些对象需要回收、怎么回收。判定那些对象需要回收,我们熟知的有以下方法: 一,引用计数法,这应是绝大数的的java 程序员听说的方法了,也是很多书上甚至很多老师讲的方法,该方法是这样描述的,为每个对象维护一个引用计数器,当有引用时就加1,引用解除时就减1,那些长时间引用为0的对象就判定为回收对象,理论上这样的判定是最准确的,判定的效率也高,但是却有一个致命的缺陷,请看以下代码:

 

package tmp;

import java.util.ArrayList;
import java.util.List;

public class Test {

	private byte[] buffer;
	private List ls;

	public Test() {
		this.buffer = new byte[4 * 1024 * 1024];
		this.ls = new ArrayList();
	}

	private List getList() {
		return ls;
	}

	public static void main(String[] args) {
		Test t1 = new Test();
		Test t2 = new Test();
		t1.getList().add(t2);
		t2.getList().add(t1);
		t1 = t2 = null;
		Test t3 = new Test();
		System.out.println(t3);
	}

}

 

 

 我们用以下参数运行:-Xmx10M -Xms10M M 将jvm的大小设置为10M,不允许扩展,按引用计数法,t1和t2相互引用,他们的引用计数都不可能为0,那么他们将永远不会回收,在我们的环境中JVM共10M,t1 t2占用8m,那么剩下的2M,是不足以创建t3的,理论上应该抛出OOM。但是,程序正常运行了,这说明JVM应该是回收了t1和t2的我们加上-XX:+PrintGCDetails运行,将打印GC的回收日记:

[GC [DefNew: 252K->64K(960K), 0.0030166 secs][Tenured: 8265K->137K(9216K), 0.0109869 secs] 8444K->137K(10176K), [Perm : 2051K->2051K(12288K)], 0.0140892 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 

 

[GC [DefNew: 252K->64K(960K), 0.0030166 secs][Tenured: 8265K->137K(9216K), 0.0109869 secs] 8444K->137K(10176K), [Perm : 2051K->2051K(12288K)], 0.0140892 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
com.mail.czp.Test@2ce908
Heap
 def new generation   total 960K, used 27K [0x029e0000, 0x02ae0000, 0x02ae0000)
  eden space 896K,   3% used [0x029e0000, 0x029e6c40, 0x02ac0000)
  from space 64K,   0% used [0x02ad0000, 0x02ad0000, 0x02ae0000)
  to   space 64K,   0% used [0x02ac0000, 0x02ac0000, 0x02ad0000)
 tenured generation   total 9216K, used 4233K [0x02ae0000, 0x033e0000, 0x033e0000)
   the space 9216K,  45% used [0x02ae0000, 0x02f02500, 0x02f02600, 0x033e0000)
 compacting perm gen  total 12288K, used 2077K [0x033e0000, 0x03fe0000, 0x073e0000)
   the space 12288K,  16% used [0x033e0000, 0x035e74d8, 0x035e7600, 0x03fe0000)
No shared spaces configured.

 

从打印的日志我们可以看出,GC照常回收了t1 t2,这就从侧面证明jvm不是采用这种策略判定对象是否可以回收的。

 

二,根搜索算法,这是当前的大部分虚拟机采用的判定策略,GC线程运行时,它会以一些特定的引用作为起点称为GCRoot,从这些起点开始搜索,把所用与这些起点相关联的对象标记,形成几条链路,扫描完时,那些没有与任何链路想连接的对象就会判定为可回收对象。具体那些引用作为起点呢,一种是类级别的引用:静态变量引用、常量引用,另一种是方法内的引用,如之前的test()方法中的Date d对new Date()的引用,在我们的测试代码中,在创建t3时,jvm发现当前的空间不足以创建对象,会出发一次GC,虽然t1和t2相互引用,但是执行t1=t2=null后,他们不和上面的3个根引用中的任何一个相连接,所以GC会判定他们是可回收对象,并在随后将其回收,从而为t3的创建创造空间,当进行回收后发现空间还是不够时,就会抛出OOM。

 

接下来我们就该讨论GC 是怎么回收的了,目前版本的Hotspot虚拟机采用分代回收算法,它把heap分为新生代和老年代两块区域,如下图:

 


 

 默认的配置中老年代占90% 新生代占10%,其中新生代又被分为一个eden区和两个survivor区,每次使用eden和其中的一个survivor区,一般对象都在eden和其中的一个survivor区分配,但是那些占用空间较大的对象,就会直接在老年代分配,比如我们在进行文件操作时设置的缓冲区,如byte[] buffer = new byte[1024*1024],这样的对象如果在新生代分配将会导致新生代的内存不足而频繁的gc,GC运行时首先会进行会在新生代进行,会把那些标记还在引用的对象复制到另一块survivor空间中,然后把整个eden区和另一个survivor区里所有的对象进行清除,但也并不是立即清除,如果这些对象重写了finalize方法,那么GC会把这些对象先复制到一个队列里,以一个低级别的线程去触发finalize方法,然后回收该对象,而那些没有覆写finalize方法的对象,将会直接被回收。在复制存活对象到另一个survivor空间的过程中可能会出现空间不足的情况,在这种情况下GC回直接把这些存活对象复制到老年代中,如果老年代的空间也不够时,将会触发一次Full GC,Full gc会回收老年代中那些没有和任何GC Root相连的对象,如果Full GC后发现内存还是不足,将会出现OutofMemoryError。

 

Hotspot虚拟机下java对象内存的分配和回收到此就算完结了

  • 大小: 11.1 KB
分享到:
评论

相关推荐

    Java 详解垃圾回收与对象生命周期

    主要介绍了Java 详解垃圾回收与对象生命周期的相关资料,这里对堆内存与栈内存进行详解及JVM 的生命周期介绍,需要的朋友可以参考下

    Java对象的生命周期

    要理解java对象的生命周期,我们需要要明白两个问题,  1、java是怎么分配内存的 ,2、java是怎么回收内存的。  喜欢java的人,往往因为它的内存自动管理机制,不喜欢java的人,往往也是因为它的内存自动管理...

    Java内存与垃圾回收调优.docx

    例如,如果对象生命周期短,可增大Eden区大小;如果对象生命周期长,考虑增大老年代大小。 根据垃圾回收的停顿时间目标(Stop-The-World时间)和吞吐量目标调整垃圾回收器的参数。 垃圾回收器选择与调优: Java 9+ ...

    java对象实体类属性字段对比变化过程

    Java对象实体类属性字段对比变化过程是指在一个Java对象实体类的生命周期中,其属性字段的变化情况。这种变化过程通常涉及到对象的创建、使用和销毁等阶段。 在对象的创建阶段,Java对象实体类会根据预设的属性字段...

    类和对象的生命周期

    一个完整的类的生命周期如下: 注意: 加载并不是类加载,类加载包括加载到初始化的五个阶段。而加载是第一步。 1、类加载 加载: 将类的二进制流加载进VM,储存在方法区,并且生成java.lang.Class对象 类加载器有三...

    java实现内存动态分配

    局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。 java实现内存动态分配目录 一.实验原理 二.实验流程图 三.实验代码 四.实验结果

    [java]读书笔记整理:一切都是对象

    Java对象不具备和基本类型一样的生命周期。当用new创建一个java对象时,它可以存活于作用域之外。所以假如你采用代码: { String s=new String(”this is a string”) ; } 引用s在作用域终点就消失了。然而,...

    java经典面试题目-面经-java-Java语言的进阶概念-常用的库和框架-并发编程-网络编程-Web开发-面经

    什么是Java中的生命周期回调方法?列举一些常见的生命周期回调方法。 什么是Java中的注解处理器?如何自定义和使用注解处理器? 什么是Java中的并发编程?列举一些常见的并发类和工具。 什么是Java中的线程池?...

    Java零基础-对象的创建和使用-内存分析.md

    了解对象的生命周期和垃圾回收机制。 阅读建议: 阅读前建议已具备基本的Java语法知识和编程经验。 请按照顺序阅读对象的创建、内存分配和使用的部分,确保理解每一部分的内容。 在阅读示例代码时,结合自己的实际...

    java高级工程师、技术专家、架构师、项目经理面试宝典.rar

    2.Java对象的生命周期 答:创建阶段 、 应用阶段 、不可见阶段 、不可达阶段 、收集阶段 、终结阶段、 对象空间重新分配阶段等等。 3.Map或者HashMap的存储原理 答:HashMap是由数组+链表的一个结构组成。

    Java清华教程.rar

     ■ 多线程程序设计,进程和线程的联系和区别,多线程程序设计的一般方法,线程的生命周期,线程状态的控制,多线程的互斥和同步;  ■ Java语言的网络编程技术和应用,Socket程序设计,Client/Server程序设计; ...

    免费超全面的Java基础类型,容器,并发,IO流,面向对象,Web编程等代码总结

    线程的创建方式与生命周期 线程核心机制,基础概念扩展 多线程并发访问,同步控制 线程间通信,等待/通知机制 锁锁机制,API详解 Fork/Join 框架机制详解 Executor线程池框架简介 面向对象 泛型机制与反射原理 ...

    java虚拟机(jvm)介绍以及相关参数设置与调优

    虚拟机的相关参数设置与调优,介绍了一些jvm的体系结构,垃圾回收的方法,java对象的生命周期与分代。

    Java后端面试问题整理.docx

    • 熟悉Java多线程并发中线程基本方法,线程池,线程生命周期,熟悉Java锁中常见锁分类(乐观/悲观锁、自旋锁、独/共享锁、可重入锁、公平/非公平锁、分段锁、偏向锁,轻/重量级锁)和基本的锁升级策略

    清华大学java教程

     ■ 多线程程序设计,进程和线程的联系和区别,多线程程序设计的一般方法,线程的生命周期,线程状态的控制,多线程的互斥和同步;  ■ Java语言的网络编程技术和应用,Socket程序设计,Client/Server程序设计; ...

    JAVA清华教程 详细

     ■ 多线程程序设计,进程和线程的联系和区别,多线程程序设计的一般方法,线程的生命周期,线程状态的控制,多线程的互斥和同步;  ■ Java语言的网络编程技术和应用,Socket程序设计,Client/Server程序设计; ...

    Java面试技术面知识扩展包第三弹.rar

    开发者不需要手动进行内存分配和释放,通过“对象生命周期”的概念,垃圾收集器会自动回收不再使用的内存。 4. 异常处理:Java提供了异常处理机制,可以在出现异常情况时捕获、处理并恢复程序的运行。通过try-catch...

    Java面试技术面知识扩展包第二弹.rar

    开发者不需要手动进行内存分配和释放,通过“对象生命周期”的概念,垃圾收集器会自动回收不再使用的内存。 4. 异常处理:Java提供了异常处理机制,可以在出现异常情况时捕获、处理并恢复程序的运行。通过try-catch...

    学会这些java面试题让你吊打面试官

    1.Java中内存管理及各区域内存储数据的生命周期? 2.JVM堆内存的分代管理以及垃圾回收流程? 3.面向对象三大特征及详细理解? 4.重载与重写的区别? 5.接口和抽象类区别? 6.实例变量和静态变量的区别? 7.八大基本类型及...

Global site tag (gtag.js) - Google Analytics