java static 变量 内存释放_浅谈静态变量的回收问题

本文探讨了Java中静态变量内存释放的问题,特别是当静态变量`cachebuffer`被新对象替换时,旧对象是否会被GC回收。文章分析了JVM内存结构,包括堆、栈、方法区等,并详细讲解了垃圾回收的原理,如引用计数法、可达性分析算法、各种垃圾回收算法(标记-清除、复制、标记-整理、分代收集)。最后指出,静态变量通常不会轻易被GC回收,除非满足特定条件(所有实例被回收、加载类的ClassLoader被回收、Class对象无法访问)。

今天工作中遇到一个用于缓存数据到内存的静态变量Stringbuffer;当缓存数据大小达到5M的时候就把该缓存数据写到S3上;然后清空该缓存buffer;看了这段代码我觉得是不是有点问题;先贴大概的代码

package com.sanpang.demo;

public class StaticVariableDemo {

//定义一个缓存buffer;存放数据到内存;数据达到5M的时候就写到S3上面;

public static StringBuffer cachebuffer = new StringBuffer();

public static void main(String[] args) {

cachebuffer.append("存的数据 达到5M");

//将数据存到S3 ;然后清空buffer

cachebuffer = new StringBuffer();

}

}

看了以上代码;我担心一个问题是这里通过new 个新对象来起到清空buffer的作用:

cachebuffer = new StringBuffer();

这样的写法会不会导致这个静态变量cachebuffer 之前引用的实例还存在不;有没有被GC回收;如果没有会不会出现OOM的问题?

带着这个疑惑问了很多技术小伙伴得到的回复都没有解决我疑惑的问题;于是自己分析了一下;自己主要疑惑的一个是这个静态变量在JVM 中的内存存储;还有一个疑惑就是这个静态对象之前引用的地址所指向的实例会不会被GC立即回收;

带着问题看了些关于JVM GC 的文章;下面写下我看了这些文章的自己的理解;不对的地方希望可以指出;

首先是JVM内存结构

根据《java虚拟机规范》规定,JVM的基本结构一般如下图所示

17920a658328

Paste_Image.png

从左图可知,JVM主要包括四个部分:

1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。(右图表示了从java源文件到JVM的整个过程,可配合理解。 关于类的加载机制,可以参考http://blog.youkuaiyun.com/tonytfjing/article/details/47212291)

2.执行引擎:负责执行class文件中包含的字节码指令(执行引擎的工作机制,这里也不细说了,这里主要介绍JVM结构);

3.内存区(也叫运行时数据区):是在JVM运行的时候操作所分配的内存区。运行时内存区主要可以划分为5个区域,如图:

17920a658328

Paste_Image.png

方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。

java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

4.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。二、内存分配

我觉得了解垃圾回收之前,得先了解JVM是怎么分配内存的,然后识别哪些内存是垃圾需要回收,最后才是用什么方式回收。

Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。

java一般内存申请有两种:静态内存和动态内存。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。根据上面我们知道,java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

总之Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题;

三、垃圾检测、回收算法

垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:

引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。

好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:

可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象

本地方法中引用的对象等。

总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。一般回收算法也有如下几种:

1.标记-清除(Mark-sweep)

算法和名字一样,分为两个阶段:标记和清除。标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。

不足:效率低;标记清除之后会产生大量碎片。效果图如下:

17920a658328

Paste_Image.png

17920a658328

Paste_Image.png

2. 复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。效果图如下:

17920a658328

Paste_Image.png

3.标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。效果图如下:

17920a658328

Paste_Image.png

4.分代收集算法

这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

年轻代:是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。

持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。

分代回收的效果图如下:

17920a658328

Paste_Image.png

最后讲分代,是因为分代里涉及了前面几种算法。年轻代:涉及了复制算法;年老代:涉及了“标记-整理(Mark-Sweep)”的算法。

总结

通过对以上知识点的理解;再来看我的问题:

这里就先不说那么细;我的理解是在JVM启动时候通过类装载器将class加载到JVM中;

静态变量

public static StringBuffer cachebuffer = new StringBuffer();

cachebuffer 被JVM存储在方法区;

new StringBuffer(); 这个对象被JVM存储在java堆(Heap)中

后面创建的StringBuffer();也存在java堆(Heap)中;

当静态变量指向新的对象的时候

cachebuffer = new StringBuffer();

之前的5M的StringBuffer对象是还在内存中的;没有立马被GC回收的;

按照GC收集的机制:触发GC回收年轻代的数据是年轻代的堆内存满了;才会触发;

如果之前cachebuffer 还没被清空(创建新的对象取代)的时候就触发了年轻代的GC 那么cachebuffer 就会会被从Enden区到survivor区甚至到Old Memory区域;

可以想象出这个代码是有问题的;

后来又研究了下这个静态变量会什么时候被GC回收:

静态变量是属于类的不是属于实例对象的;所以这个类存在静态变量就会一直存在;静态变量存储在方法区;用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,按照分代属于持久代;这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

1、所有实例被回收

2、加载该类的ClassLoader 被回收

3、Class 对象无法通过任何途径访问(包括反射)

所以一般来说 静态变量很少会被GC回收的;

参考链接:(准确的来说是copy链接~下面两篇写的都很不错)

https://zhuanlan.zhihu.com/p/25539690

http://blog.youkuaiyun.com/tonytfjing/article/details/44278233

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值