0%

Java基于C++实现,与C++的一大区别在于它能够进行自动内存管理。

先从垃圾说起,所谓的垃圾就是无用的对象,主要存放在堆区中。对于C++来说,创建对象的时候需要malloc,用完对象之后需要free,以免无用对象占据内存空间,这一切的操作都是手动的。对于Java来说,通过Java虚拟机垃圾收集器可以自动化的实现对象内存的分配及回收

jvm-gc

那么,如何标识哪些内存对象是无用的呢?

判断对象是否应该存活的算法有:引用计数算法、根搜索算法。

引用计数算法,给每一个对象添加一个引用计数器,只要一个对象被引用了,那么计数值就加1,如果引用失效了,那么计数值就减1,直到为0,那么这个对象就可以被回收了,这种方式还是比较简单高效的。但是,如果存在循环引用引用计数算法就无能为力了,引用会一直存在,计数值不会为0,从而造成内存无法被回收。

jvm-ref-d

根搜索算法,通过一系列名为GC Roots的对象为起始点,从这些节点向下搜索,如果对象是可达的,那么这个对象就是存活的,反之,就是不存活的,这个对象就是可回收的。

jvm-ref-r

决定对象是否应该存活的直接影响因素就是引用,在Java中存在有四种引用:强引用、软引用、弱引用、虚引用。

强引用 普遍存在,类似Object obj = new Object(),只要对象引用还存在,对象的内存就不会被回收。
软引用 表示有用但非必需的对象,当内存不够的时候,软引用对象就可以被回收。
弱引用 表示非必需对象,只要发生了GC,弱引用对象就可能被回收。
虚引用 用于在对象被回收的时候能够收到通知。

在标识对象存活的基础上,需要利用垃圾收集算法来实现内存的回收。

首先是标记-清除算法,分为标记清除两个阶段。标记的对象是可回收对象,先进行标记,然后再去执行清除。这种算法的缺点就是会产生大量的内存碎片,使得连续内存不足,导致GC的触发,并且标记、清除阶段效率都不高。

jvm-ms

为了解决标记-清除算法的缺点,又产生了复制算法。将内存空间分为2个区域,各占50%,1个区域正常使用,1个区域空闲着,每次发生GC的时候,会将使用着的区域的存活对象复制到另一个区域,并按照顺序存放。通过这种方式,解决了内存碎片的问题,但同时又浪费了50%的内存空间。

jvm-gc-cp由于新生代的对象,98%左右都是朝生夕死的,将新生代分为Eden区、Surviror0区、Survivor1区,并且比例为8:1:1,使用复制算法也不至于浪费过多内存空间,并且不会产生内存碎片。

对于老年代里头的对象,一般存活率都是比较高的,如果采用标记-清除算法,那还不如复制算法呢,但如果采用复制算法,又因为老年代对象存活率高,频繁的移动内存中的对象,难免会造成回收效率的下降。因此,又产生了一种标记-整理算法,这种算法不会产生内存碎片,但效率比起复制算法来说也不高,但适用于老年代。

jvm-mc

根据以上信息可以得知,复制算法更适用于新生代的内存回收,而标记-清除算法标记-整理算法更适用于老年代的内存回收,因此,垃圾收集器基本都是基于分代收集算法,将内存区域划分为不同年代,按照每个区域合适的垃圾回收算法回收内存。

实际上,垃圾收集器在分代收集算法的基础上以串行、并行、并发方式提供。
新生代老年代
串行Serial收集器Serial Old收集器
并行ParNew收集器、Parallel Scavenge收集器Parallel Old收集器
并发CMS收集器
其它G1收集器G1收集器

新生代的内存回收(YGC、Minor GC、Young GC)使用的复制算法。Serial收集器是单线程的,适用于Client模式,而ParNew收集器与Serial收集器相比除了采用多线程没有多大区别,适用于Server模式,Parallel Scavenge收集器表面上看起来和ParNew收集器没有多大区别,但实际上Parallel Scavenge收集器更关注的是吞吐量,通过减少Stop The World时间来提高吞吐量,同时可能导致GC次数更加频繁

jvm-gc-new

老年代的内存回收(Major GC、FGC、Full GC同时也会回收新生代)使用的标记-整理算法、标记-清除算法。Serial Old收集器是单线程的,采用标记-整理算法,适用于Client模式。Parallel Old收集器是Parallel Scavenge收集器的老年代版本,是多线程的,注重吞吐量,采用的标记-整理算法。CMS收集器采用的标记-清除算法,注重低停顿,在初始标记重新标记阶段会Stop The World,而并发标记并发清除阶段与用户线程并行存在,由于采用的标记-清除算法,因此会产生内存碎片,导致出现Concurrent Mode Failure,触发Serial Old收集器来执行标记-整理。

jvm-gc-old在CMS收集器的基础上又产生了G1收集器,与之不同的是G1收集器采用的标记-整理算法,不会产生内存碎片并且没有明显的分代概念,而是将内存划分为若干个固定大小区域,可以在保证吞吐量的同时完成低停顿的内存回收,在回收内存的时候不会全区域的去回收,而是优先回收内存垃圾比较多的区域。

jvm-gc-g1

内存的分配也随着JDK的发展与各种技术的提升而更加智能。

对象优先在Eden区分配
大部分的对象都具备朝生夕死的特点,更适合在新生代中分配,并且Minor GC速度比较快。

大对象直接进入老年代
由于新生代采用的复制算法,Minor GC会比较频繁,因此大对象最好直接进老年代,避免发生频繁的内存复制。当然,也有一些朝生夕死的大对象,如果过多这种大对象进入了老年代可能会导致Major GC的频繁发生,甚至导致Full GC的出现。可以通过配置-XX:PretenureSizeThreshold参数来定义直接进入老年代的大对象的大小门槛。

长期存活对象将进入老年代
如果对象长期存活,那么可能是有用的,最好是晋升到老年代,否则随着多次的Minor GC会不断的被复制来复制去,同时也比较占据新生代的内存空间。可以通过配置-XX:MaxTenuringThreshold参数来修改新生代中的对象晋升到老年代的历经GC次数,默认是15次。

动态年龄判断
通过动态年龄判断,只要Surviror区中相同年龄的对象大小总和超过Surviror区大小的一半,那么就允许新生代对象不必历经MaxTenuringThreshold配置的GC次数提前晋升到老年代。

空间分配担保
在发生新生代Minor GC的时候,会去判断要晋升到老年代的对象大小总和是否超过老年代剩余空间的大小,如果超过了,并且配置了HandlePromotionFailure,那么就会进行Minor GC,否则的话,会执行一次Full GC。

逃逸分析、栈上分配
逃逸分析分为线程逃逸、方法逃逸。对于线程逃逸来说,如果一个对象只在一个线程中使用,那么这个对象就逃逸了,不需要在堆中分配内存。对于方法逃逸来说,如果一个对象只在方法内部使用,并且被外部方法所引用,那么这个对象就逃逸了,不需要在堆中分配内存。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

jvm

这些区域包含:虚拟机栈、本地方法栈、程序计数器、堆、方法区。
每个区域都有各自的生命周期以及各自的作用范围及功能。
在线程中,虚拟机栈、本地方法栈、程序计数器是私有的,属于指令,堆、方法区则是共享的,属于数据

程序计数器标识的是线程正在执行的字节码指令地址,由于多线程的存在及CPU可能会来回切换线程。
多个线程之间的程序计数器是相互独立的,可以看作是各自的行号指示器

虚拟机栈,它表示的是Java方法在运行时候的内存模型,是一种先进后出的数据结构。
虚拟机栈,存储了当前方法运行所需要的指令数据返回地址

jvm-stack

方法被调用的时候都会去创建栈帧,从方法执行的开始结束又对应着栈帧虚拟机栈中的入栈出栈
栈帧被用来保存局部变量表操作数栈动态链接方法出口等信息,每一个方法都对应着一个栈帧
jvm-stack-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sankuai.test;

import com.alibaba.fastjson.JSON;

public class Test {
public static void main(String[] args) {
String json = "{\"id\":1,\"name\":\"1\"}";
int a = 1, b = 2;
int c = a + b + 6;
System.out.println(a + b);
System.out.println(c);
System.out.println(JSON.parseObject(json, User.class));
}
}

局部变量表中包含了基本数据类型对象引用类型返回地址

  • 基本数据类型:byte、short、int、long、float、double、boolean、char。
  • 对象引用类型:对象的起始地址(数组)或者对象的句柄(类)。
  • 返回地址:指向的是一条字节码指令的地址。

操作数栈,本质也是一个栈,入栈和出栈代表着变量的操作,比如:两个变量相加。
动态链接,所调用的对象会在常量池中存储并用标号标识,而常量池会指向对象的真实地址。
jvm-cp-0jvm-cp方法出口,当栈帧出栈之后,对应的这个方法应该返回到调用它的地方。

接着是本地方法栈,它跟虚拟机栈是类似的,只不过它调用的不是Java方法,而是Native本地方法
另外,虚拟机栈本地方法栈都是线程私有的,生命周期跟随着线程。
当线程越多、栈的深度越深,就可能造成栈溢出StackOverFlowError,甚至是OutOfMemoryError

是Java虚拟机中内存占用最大的一块区域,也被所有的线程所共享,主要是用来存放对象实例。所有的对象实例和数组都要在上分配内存,不过后来出现了栈上分配、标量替换、逃逸分析等技术,使得对象不一定需要在堆上分配内存。一个堆包含了:新生代(Eden、S0、S1)老年代,比例通常是1:2

jvm-heap也是垃圾收集器GC的重点区域,目前普遍采用的是分代收集算法

最后是方法区,也是被所有线程所共享的,主要存储的是:被虚拟机所加载的类的信息常量静态变量、即使编译器编译后的代码等信息,在JDK1.8之后,方法区元空间所替代,并且被挪到了堆外
方法区中存在着一个运行时常量池,它用来存储字面量符号引用
jvm-cp-2

MySQL的复制是一种高性能高可用的解决方案,在主从之间进行复制、重放数据。

mysql-repl

复制(replication)首先会把服务器的数据更改写入binlog二进制文件中,服务器的I/O线程binlog拉取数据,存入本地的relay log中继日志中,然后由SQL线程读取并重放到从服务器中。

由于整个复制的过程是异步实时的,所以可能会存在在主从同步的延迟现象,这个延迟可能是一秒、一分、一小时,甚至一天,数据量越大,主服务器的压力就越大。

MySQL支持的复制方式包含有:row(行)、statement(语句)、mixed(行+语句)。

首先是基于的复制方式,这种方式只会在binlog文件中记录最终的修改的记录的结果,在一些复杂查询下却返回较少数据情况下比较适用,但兼容性也不好,一旦修改列的话,就会出现问题,并且随着行数的增多,binlog文件也会膨胀

接着是基于语句的复制方式,这种方式会在binlog文件中记录操作的SQL语句,相比基于行的方式来说,SQL语句更容易理解且遇到问题的时候也较好定位,在占用的体积上来说也了许多。当然,基于语句的复制方式也存在着数据操作范围不可控的风险,同样,一旦修改列的话,也可能会出现问题。

最后是mixed这种方式,结合了基于的复制方式和基于语句的复制方式,取二者的优点。