7

[toc]

JAVA GC垃圾回收(及一次内存泄漏处理)

20170610165140237

JVM内存分布

上图展示了JVM的架构图,本篇我们主要关注,运行时数据区。GC垃圾回收发生在这个区的堆上。

Java使用了垃圾回收机制,极大的减轻了程序员的工作,是程序员能够更加焦距在业务上。
但是垃圾回收并不能百分百保证不会出现内存泄漏,所以了解垃圾回收,对于我们遇到内存泄漏时能更加清晰的分析原因,也能帮助我们写出更加安全,可靠的程序。

memorypic

方法区 Method Area

methodarea

类加载器加载类之后,把类的信息存储到方法区(即加载类时需要加载的信息,包括版本、域、方法、接口等信息)。所以方法区是存储类级别的数据,包括静态变量。
每个jvm实例只有一个方法区,这里会被jvm下的线程共享,so方法区是线程不安全的。

常量池是方法区的一部分,string对象的引用就存储在这里。

 String s1 = "abc";//这里“abc”就存储在常量池 s1在栈区指向方法区的一个内存地址

下面看一个面试题来理解一下:

String s=new String("xyz")
//创建了几个String Object?
两个:
    "xyz"创建一个对象
    new String()创建一个
一个:
 “xyz”在其他程序中已经创建,并且还没有死亡,
 那么本次只会创建一对象new String()
 

堆区 heap Area

垃圾回收主要集中在这个内存区。

堆区存放对象的实例变量以及数组将被存储在这里。
堆区和方法区一样在JVM的实例中只有一个,会被JVM下的线程共享,所以堆区是线程不全的。

堆区分为:新生代和老年代(方法区是持久代)
新生代分为三个区:
eden(伊甸园 新的对象最先在这里产生),to survivor, from survivor
在后面讨论GC的时候,再详细说明这一块的工作过程。

heapArea

栈区 Stack Area

stack Area也可以叫虚拟机栈
栈区是线程安全的,每个线程都会创建自己私有的栈区。
在每个线程运行的时候会单独创建一个运行时栈,栈区会分为三个实体:

  1. 局部变量:存储方法中的局部变量
  2. 操作数栈:即执行的指令,a+b:a入栈+入栈b入栈出栈计算结果
  3. 帧数据: 方法所有符号都保存在这里。异常情况下catch块的信息将会被保存在桢数据中。

程序计数器

程序计数器也称pc寄存器。从寄存器的概念上我们就可以了解到空间很小但是很重要。
程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到第几行,可以理解为当前线程的行号指示器(字节码的哦)字节码解释器在工作时,会通过改变这个计数器的值来取下一条指令。

本地方法栈 native method stack

还记得在看源码的时候看到有些方法被声明为navite吗?
navite的声明方法为本地方法,一般是C语言实现。
本地方法栈在作用,运行机制、异常类型等方面都与虚拟机相同,唯一的区别是:虚拟机栈是执行Java方法,而本地方法栈使用执行navite方法的。在很多虚拟机(hotspot)会将本地方法栈与虚拟机栈放在一起使用。

直接内存

内存一部分被jvm管理,一部分没有被jvm管理,没有的那部分就是直接内存。

Object o = new Object()的jvm分布

Object o 表示一个本地引用,存储在jvm栈的本地变量表里,表示一个reference类型数据,
new Object():作为实例对象存储在对堆中。
类的信息(即加载类时需要加载的信息、包括版本、file、方法、接口等信息)存储在方法区。

三个代(新生、老年、持久代)

堆内存 = 新生代 + 老年代

新生代(年轻代 yong :so s1 eden)

对象被创建之后会被存储在新生代(新生代空间足够,否则会放在老年代,如果老年代内存满了,则会抛出 out of memory异常)
新生代分为3个区:eden,to survivor, from survivor
servivor永远有一个是没有被使用的(空闲的),因为新生代的垃圾回收算法使用的是复制算法,所以永远有一个survivor是没有被使用的。
复制算法过程:

当新生代需要垃圾回收的时候, 把eden和其中一个survivor存活的对象复制到另一个survivor,然后进行清理,之后在使用存放存活的对象的survivor和eden,下次再按照本次的复制算法进行复制。

新生代的三个区的默认空间比例是(由于绝大多数对象都是短命的,所以eden相比survivor会比较大):
eden: from: to = 8:1 :1

老年代(old)

如果新生代的对象经过了几次新生代gc(一般是15次)还没有被回收,那么新生代的对象会被移到老年代。
老年代存储的对象比年轻代多得多,而且很多都是大对象,老年代的清理算法采用的是标记清除法: 当老年代进行内存清理额时候,先标记出需要被清理的空间,然后统一进行清理(清除的时候会使程序停止)。

持久代(永久代 perm)

按照内存存储的数据的生命周期,方法区也被称为持久代。表示此空间很少被回收,但是不表示不会被回收。
持久代的回收有两种:

  1. 常量池中的常量,常量如果没有被引用则可以被回收
  2. 无用的类信息(同时满足以下条件):
    2.1. 类的所有实例都已经被回收了
    2.2. 加载类的ClassLoader已经被回收
    2.3. 类对象的class对象没有被引用(即没有通过反射引用该类的地方)

GC算法

GC是在清除那些对象?

通过一下两个算法,我们可以看到那些引用计数器为0或着不具有可达性的对象会被清除回收。

引用计数法

在对象中记录一个引用计数器,如果对象被引用则计数器加一,如果引用被释放则计数器减一。当引用计数器为0的是否则对象被回收,但是这个算法有一个问题如果,两个对象相互引用,则一直都不会被回收,导致内存泄漏

内存泄漏:是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

内存溢出:通俗的说就是系统内存不够,导致程序崩溃,一般内存泄漏很严重会导致内存溢出。

    /**
    *引用计数器算法导致内存泄漏示例
    * @author: xuelongjiang 
    **/
    
    public class countDemo{
    
    public static void main(String [] args){
        
        DemoObject object1 = new DemoObject();//(1) object1引用计数器 = 1
        DemoObject object2 = new DemoObject();//(2) obejct2 引用计数器 = 1
        object1.instance = object2;//(3) object2引用计数器 = 2
        object2.instance = object1;//(4) object1引用计数 = 2
        object1 = null;//(5) object1引用计数器 = 1
        object2 = null // (6) obejct2引用计数器 = 1
       //到程序结束obejct1,object2的引用计数器都没有被置为0 
    }
}

public class DemoObject{
    public Object instance = null;
}    

so hotspot虚拟机并没有采用引用计数器算法。

可达性算法

现在我们来看可达性分析是如何避免上面循环引用导致内存泄漏

可达性算法核心是从GC Roots对象作为起始点,GC Roots可到达的则为存活对象,不可到达的则为需要清理的对象。

GC2017080101

图中的 Object10,object11,obejct4, object5 为不可达对象。

GC Roots的条件:

  1. 虚拟机栈的栈桢的局部变量表所引用的对象
  2. 本地方法栈的JNI所引用的对象
  3. 方法区的静态变量和常量所引用的对象

图 可达性实例图。参考知乎答案

从上图可以看reference1(满足上面条件3)、reference2(满足条件1)、
reference3(满足条件2)

reference1 引用 对象实例1
reference2 引用 对象实例2
reference3 引用 对象实例4(间接引用 实例对象6)

从上图中可以看出实例1,2,4,6都具有GC Roots可达性也就是存活对象,不会被GC回收。而实例3,5虽然直接连通,但是由于没有和GC Roots 连通不是可达对象。在可达性算法实例3、5是会被GC回收的。

回到引用计数器算法那个示例我们通过可达性分析,最终 object1,object2会被GC回收。

标记-清除算法

标记-清除算法分为两步,第一步:标记从GC Root 根的可达对象。 第二步:清除不可达对象,清除没有被标记的对象,此时会使程序停止运行,如果不停止程序,那么新产生的可达对象没有被标记则会被清除。

缺点:会产生不连续的内存空间,并且会暂时停止程序。

复制算法

将内存区分为两部分:空闲区域和活动区域,首先标记可达对象,标记之后把可达对象复制到空闲区,将空闲区变为活动区,同时清除掉之前的活动区,并且变为空闲区。
速度快但是耗费空间。

标记-整理算法

标记可达对象,清除不可达对象,整理内存空间。

各代使用的算法

新生代采用 复制算法
老年代采用 标记-整理算法

GC中的一些值解释

YGC:年轻代的GC
FGC: 全范围的GC

JVM的一些参数说明

-XmsxxM : -Xms64M 设置最小堆内存为64MB
-Xmxxxm : -XMx128M 设置最大堆内存128MB

如果以上参数设置的过于小会导致频繁的发生GC,导致应用的性能极大下降。如不必要使用默认就可以。
一般JVM调优调整以上两个参数就可以。
还可以设置的更加详细:

-XX:NewSize :设置年轻代的大小
-XX:NewRatio : 设置年轻代和老年代的比值,如:3 表示年轻代与老年代的比值为1:3
-XX:SurvivorRatio :年轻代中eden区与两个survivor区的比值
-XX:MaxPermSize : 设置持久代的大小

一次线上内存泄漏解决

最新线上生产的项目发生了内存泄漏,整个排查思路是这样的:

事故背景

使用websocket(基于netty实现)客户端实时获取其他网站的数据,把返回的数据使用redis缓存起来。
websocket只在程序启动的时候运行一次,之后定时任务(timer)接管websocket的ping,断线重连。

事故原因

由于websocket的消息处理使用了redisClient,onReceive方法中调用redisClient。
redisClient的生命周期是整个应用的生命周期是一致。

redisClient.opsForValue().set(symbol.get(), df.get()+" 美元");//redisClient引用了 symbol 和df 导致symbol,df没有被释放,并且他俩引用了其他的导致都没有被释放,发生了内存泄漏
内存泄漏代码
@SpringBootApplication
@EnableScheduling
public class WalleInt2Application {

public static void main(String[] args) {
        SpringApplication.run(WalleInt2Application.class, args);
    }
    
    
    @Bean
    public TaskRunnerFcion taskRunnerFcion(){
        return new TaskRunnerFcion();
    }
}
/**
* 只在项目启动的时候运行一次(run()方法)
 * @author xuelongjiang
 */
@Order(value = 1)
public class TaskRunnerFcion implements ApplicationRunner {


    private static Logger logger = LoggerFactory.getLogger(TaskRunnerFcion.class);

   /* @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;*/


    @Autowired
    @Qualifier("fcionWebSocketServiceImpl")
    private  WebSocketService fcionWebSocketServiceImpl;



    String fcionUri = "wss://ws.fcoin.com/api/v2/ws";
    String fcion_ping = "{\"cmd\":\"ping\",\"args\":[1532070885000]}";
    String fcion_getData = "{\"id\":\"tickers\",\"cmd\":\"sub\",\"args\":[\"all-tickers\"]}";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("启动fcion websocket客户端............");
       // WebSocketService service = new FcionWebSocketServiceImpl(redisClient);
        WebSocketFcionClient client = new WebSocketFcionClient(fcionUri,fcionWebSocketServiceImpl,fcion_ping);
        client.start();
        client.addChannel(fcion_getData);
        logger.info("启动fcion websocket客户端  完成............");

    }
}
@Service
public class FcionWebSocketServiceImpl implements WebSocketService{

    private Logger log = LoggerFactory.getLogger(FcionWebSocketServiceImpl.class);


    private String get_rate_usedToCNY = "https://www.fcoin.com/api/common/get_rate?from=USD&to=CNY";

    @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;

    public FcionWebSocketServiceImpl() {
    }

    public FcionWebSocketServiceImpl(StringRedisTemplate redisClient) {
        this.redisClient = redisClient;
    }


    @Override
    public void onReceive(String msg){

        log.info("WebSocket fcion Client 接收到消息:{} ",  msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String topic = jsonObject.getString("topic");
        if(topic != null &&topic.equals("all-tickers")){
            JSONArray jsonArray = jsonObject.getJSONArray("tickers");
            for(int i =0; i< jsonArray.size(); i++){

                JSONObject jsonObject1 = jsonArray.getJSONObject(i);
                Double usdPrice =jsonObject1.getJSONArray("ticker").getDouble(0);
                if(usdPrice == null){
                    continue;
                }
                BigDecimal b = new BigDecimal(usdPrice);
                df=b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
                String symbol="fcion_"+jsonObject1.getString("symbol");
                log.info("{}当前价格:{}", symbol, df+"美元");
                redisClient.opsForValue().set(symbol, df+" 美元");//redisClient相当于单例模式没有被释放,导致器引用的symbol,df没有被释放,symbol引用JSONObject, df引用了BigDecimal导致都没有被释放,发生了内存泄漏
            }
        }
    }
}

redisClient相当于单例模式没有被释放,导致引用的symbol,df没有被释放,symbol引用JSONObject, df引用了BigDecimal导致都没有被释放,发生了内存泄漏

修复后的代码

@Service
public class FcionWebSocketServiceImpl implements WebSocketService{

    private Logger log = LoggerFactory.getLogger(FcionWebSocketServiceImpl.class);


    private String get_rate_usedToCNY = "https://www.fcoin.com/api/common/get_rate?from=USD&to=CNY";

    @Autowired
    @Qualifier("redisClient")
    private StringRedisTemplate redisClient;

    public FcionWebSocketServiceImpl() {
    }

    public FcionWebSocketServiceImpl(StringRedisTemplate redisClient) {
        this.redisClient = redisClient;
    }


    @Override
    public void onReceive(String msg){

        log.info("WebSocket fcion Client 接收到消息:{} ", msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String topic = jsonObject.getString("topic");
        if(topic != null &&topic.equals("all-tickers")){
            JSONArray jsonArray = jsonObject.getJSONArray("tickers");
            for(int i =0; i< jsonArray.size(); i++){

                JSONObject jsonObject1 = jsonArray.getJSONObject(i);
                Double usdPrice =jsonObject1.getJSONArray("ticker").getDouble(0);
                if(usdPrice == null){
                    continue;
                }
                BigDecimal b = new BigDecimal(usdPrice);
                WeakReference<Double> df = new WeakReference<Double>(b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue());
                WeakReference<String> symbol = new WeakReference<String>("fcion_"+jsonObject1.getString("symbol"));
                log.info("{}当前价格:{}", symbol, df+"美元");
                redisClient.opsForValue().set(symbol.get(), df.get()+" 美元");


            }
        }

    }
}

这里使用弱引用修饰 symbol,df,是之能够被释放,当方法被回调完成执行后,会被回收

长对象引用短对象:

longObjectShortObejct

定位过程

快速定位内存泄漏的命令:

jamp -histo:live pid

可以看到哪些类被使用的最多:

watchmemory
(上面使用的是阿里云的提供的服务器网页版 其本质也是执行的上面的命令)

看到了 byte[]占用的内存比较大,开始怀疑是不是使用netty的handler的channelRead0方法里导致的内存泄漏,因为这里处理返回的流,使用到了byte [],之后注释掉onReceive方法的业务处理(由于这个项目只是完成websocket客户端获取三个网站的数据)。跑了四五个小时,再次查看内存使用情况,发现没有发生泄漏。此时定位到问题发生在onReceive方法中。

通过分析redisClient 没有被释放,导致引用的对象没有被释放,发生了内存泄露。
最后使用弱引用来进行释放。

以上是问题解决的时候的步骤(其实当时是直接停掉了websocket,只是跑springboot)

实际排错,比较曲折。最好是用过jmap命令,看到输出对象里有BigDecimal就觉得有问题,因为按照代码BigDecimal的对象不可能20多兆。但是也觉得可能是redisClient导致没有释放对象 。
把symbol , df置为null之后运行了几个小时内存还是泄漏。所以就使用以上关闭部分代码的方法来准确定位(由于这一块理论知识的不足才会导致排错走了很多弯路)。

单例模式引发的内存泄漏

由于单例对象的生命周期是伴随着应用的生命周期的,所以如果单例对象引用了其他对象,会导致其他对象很难被回收(长生命周期对象持有短生命周期对象)。

几种引用方式

强引用

代码中普遍存在的类似 Obejct o = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

弱引用

非必须对象,被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。

软引用

还有用但非必须对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会跑出了内存溢出异常。java 中的类 SoftReference表示软引用。

虚引用

这个引用存在的唯一目的就是这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没有关系。Java中的类PhantomReference表示虚引用。

参考:

https://www.cnblogs.com/first...
https://www.cnblogs.com/study...
https://blog.csdn.net/aijiudu...
https://www.cnblogs.com/xiaox...
https://www.cnblogs.com/yydcd...
http://baijiahao.baidu.com/s?...
https://www.zhihu.com/questio...
https://www.cnblogs.com/soari...
https://www.cnblogs.com/my-ki...
https://blog.csdn.net/u012167...

关注我的公众号第一时间阅读有趣的技术故事
扫码关注:

可以在微信搜索公众号即可关注我:codexiulian

渴望与你一起成长进步!


我爱看明朝
102 声望6 粉丝

待过大型互联金融公司,带过团队创过业,现在安静的在一家中型公司编码。