JAVA内存分析

JAVA通过自带的垃圾回收机制来管理内存,但对于项目的内存分析也要了解,比如项目的内存使用情况,何时回收?或出现内存泄露如何排查?

首先看看JVM内存分配图:

671D4492-75BB-4046-8B2E-199ABDE70734

如上图,jvm包含了堆、本地方法栈,

堆上存放新建(New)的对象,它被划为为新生代,老年代两个区域,新生代又分为伊甸园、幸存者两个区域,不同的区域存放的对象生命周期不同

说说JAVA中几种内存溢出的情况:

1,JVM堆溢出(java.lang.OutOfMemoryError: java heap space)

当生成一个新对象,JVM内存申请如下流程:

1),jvm先尝试在eden分配新对象所需的内存,若内存足够,则将对象放入eden返回

2),若内存不够,jam启动youngGC,试图将eden不活跃的对象释放掉,若释放后仍不足以分配内存,则将Eden活跃的对象放入survivor中。

3),survivor作为Eden和old的中间交换区域,若old空间足够,survivor去对象会被移动到old区,否则留在survivor区

4),当old区不够时,jvm会在old区进行fullGC,若fullGC后,survivor和old仍然无法存放从Eden复制过来的对象,则会出现“outOfMemoryError: java heap space”

解决方法:加大堆内存的大小,通过设置-Xms(java heap初始化大小,默认是物理内存1/64) -Xmx(java heap的最大值) -Xmn(新生代heap的大小,一般为Xmx3或4分之一,注:增加新生代后会减少老年代的大小)

2,方法区内存溢出(java.lang.OutMemoryError: permGen space)

方法区主要是用来存放类信息,常量、静态变量等,所以程序中类加载过多(引入第三方包),或者过多使用反射、cglib这种动态代理,就可能导致该区域内存溢出

解决方法:通过设置-XX:PermSize(内存永久区初始值)和-XX:MaxPerSize(内存永久区最大值)的大小

3,线程栈溢出(java.lang.StackOverflowError)

线程栈是线程独有的一块内存区域,所以线程栈溢出必定是线程运行是出现错误,一般是递归太深,或者方法层级调用太深引起的

解决方法:设置栈区的大小,通常栈的大小是1-2M,可通过-Xss设置线程的栈的大小,jdK5以后每个栈默认大小为1M。

下面针对线上的某个项目,查看它是否出现内存溢出的情况

1,如何查看项目的内存使用情况?

jmap -heap pid

Attaching to process ID 64909, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.91-b14

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2147483648 (2048.0MB)
   NewSize                  = 44564480 (42.5MB)
   MaxNewSize               = 715653120 (682.5MB)
   OldSize                  = 89653248 (85.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 31457280 (30.0MB)
   used     = 12660000 (12.073516845703125MB)
   free     = 18797280 (17.926483154296875MB)
   40.24505615234375% used
From Space:
   capacity = 1572864 (1.5MB)
   used     = 1179648 (1.125MB)
   free     = 393216 (0.375MB)
   75.0% used
To Space:
   capacity = 9437184 (9.0MB)
   used     = 0 (0.0MB)
   free     = 9437184 (9.0MB)
   0.0% used
PS Old Generation
   capacity = 40894464 (39.0MB)
   used     = 4514768 (4.3056182861328125MB)
   free     = 36379696 (34.69438171386719MB)
   11.040046887520033% used

查看Java内存的分配情况及新生代、老年代内存的使用情况,确定是否内存分配过小?

2,判断是否出现内存泄露

jstat -gcutil pid 2000

D09AEF2B-2D3E-4D50-B74C-E603875EAD70

上面命令表示每隔2秒打印出GC的使用回收情况,若FGC很多很可能出现了内存泄露

3,查看占用内存最多的对象

jmap -histo:live pid | more

0B137198-E38B-4466-A554-E68FEC36EC61

按使用大小进行了排序,重点查看排在前面的对象,看是否程序写的有问题。

另外可以将jvm的堆内存导出来分析,使用

jmap -dump:format=b,file=dynamicapi.hprof pid

使用java vistual工具分析,jdk自带了jvisualvm就可以进行内存分析,执行命令:

进入jkd的bin目录,执行jvisualvm

点击文件->装入,选择堆内存文件,即可看到堆中类的使用情况

3E5ECD10-58E2-450B-A8C4-31B276689EC6

 

Twitter分布式ID算法Snowflake

Snowflake是Twitter开发的一个分布式ID生成算法,有以下几个特点:

1)默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id,即理论上1秒产生409万id

2)高性能,不依赖其他第三方服务,稳定性高

3)强依赖于机器时钟

下面看看它的算法结构图:

图片 1

可以看到它是由三部分组成

1)当前时间戳

2)工作机器ID:包括dataCenterId和workId,可自己配置

3)12bit序列号,即从0增长到4095

算法其实很简单,因为不依赖于其它服务器,都是做时间比较和位移操作,流程图如下:

CA1FEEEF-6CBB-4D5F-993B-ED2C51C9D198

下面针对JAVA版的算法具体分析

// ==============================Fields===========================================
    /** 开始时间截 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;

因为算法都是基于二进制的位移操作,所以上面定义了一大堆变量,基本都是一些需要位移的长度

如序列IDsequenceBits在定义了它的二进制长度,序列号最大为4095,它的二进制占用长度就是12

同样datacenterId和workId最大数为31,二进制占用的长度就是5,workerIdShift,datacenterIdShift,timestampLeftShift定义了他们需要的位移数

为啥都要基于二进制的位移来操作呢?因为这样对于机器来说计算更快

核心方法生成ID

    // ==============================Methods==========================================
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

1)首先调用timeGen()获取当前时间戳

2)如果当前时间戳小于上次记录的时间戳,则抛出异常,表示始终回拨了(所以要保证每台机器上时间统一)

3)如果当前时间戳等于上一次时间戳,表示同一秒内有多个并发请求,此时序列号就发挥作用了,递增+1,这里有一个操作(sequence + 1) & sequenceMask,就是要与最大序列号4095做&操作,即如果它大于了最大的序列号,那么sequence就等于0了,此时调用tilNextMillis()方法做等待操作,直到生成的时间戳大于上一次时间戳,因为同一秒只支持4095个并发

4)如果当前时间戳大于上一次,则直接把sequence置0

5)将上一次时间戳更新为当前时间戳

6)最后一步也是关键,通过位移操作,把sequence(序列号),workId(工作ID),datacenterId(数据中心ID),timestamps(时间戳)拼到一起

说明这里还有一个twepoch,表示起始的时间点,这里的作用主要是控制生成ID的大小,如果你想从较小的ID开始递增,那么twepoch就可以设置的大一些,可以等于当前的时间戳,因为(timestamp – twepoch)的值就越小,反之则时间越往前ID越大

 构造方法

传入一个datacenterId和workId就可以了,说明下不同机器可以使用不同的datacenterId,一台机器上不同的项目可以使用不同的wokId

   /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

最后说说snowflake的优缺点:

优点:

1)毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

2)不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

3)可以根据自身业务特性分配bit位,非常灵活。

使用场景如:生成订单ID,因为ID不是连续递增的,所以可以保证订单数的隐蔽性

缺点:

1)强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

2)分布式部署时,服务器最好开启Network Time Protocol (NTP)服务,保证每个机器时间一致

下次说说如何使用zookeeper调度生成ID及保证服务的高可用。