java虚拟机管理的内存分为五大区域:
方法区、堆 ;虚拟机栈、本地方法栈、程序计数器
程序计数器:线程私有、记录当前线程的行号指示器,内存模型中唯一没有OOM错误的区域
虚拟机栈:方法执行时创建,存储局部变量表、操作数栈、动态链接、方法返回地址;先进后出;局部变量表的大小在编译时期就确认了;当请求的栈深度大于当前的栈深度时,报StackOverflowError;栈空间可以动态扩展,当无法申请到足够的空间时,报OOM错误。
本地方法栈:与虚拟机栈类似,虚拟机栈执行java方法,本地方法栈执行native方法,底层使用c或者c++编写。
堆:java虚拟机内存中最大的一块内存区域,存放发对象为线程共享。所有的对象实例及数组都要在堆上分配内存,但是随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。可能会报OOM错误。
JIT编译器:可以把java字节码转换为可以直接发送给处理器的指令。
逃逸分析:通过逃逸分析来决定哪些实例或者变量要在堆中进行分配,哪些可以直接在栈上进行分配。这些变量的指针可以被全局引用也可以被其他线程引用。
方法区:线程共享;存储类信息、常量、静态变量、运行时常量池(1.7移除);1.7之前可通过-XX:PermSize和-XX:MaxPermSize限制方法区大小。1.8真正开始废弃永久代,而使用元空间(Metaspace)
对象在堆中的布局分为:对象头、实例数据、对齐填充
对象头(Markword):存储hash码、分代年龄、锁标志位(无锁状态、偏向锁、轻量级锁、重量级锁);
栈中局部变量表中的对象引用存储的是堆中句柄池中的句柄地址,在对象被移动后只需要改变句柄中的实例数据的指针,无需改变ref本身。
如果直接存储对象的实例数据,优点就是速度快,少了一次指针定位的开销时间
GC:java堆、方法区需要垃圾回收
堆回收区域:新生代(Eden,from survivor,to survivor)、老年代
判断对象是否存活算法:
1.引用计数算法 优点:效率高,实现简单 缺点:循环引用的问题
2.可达性分析算法
通过“GC Roots”的对象作为起始点,搜索经过的路径称为引用链,当一个对象到GC roots没有任何引用跟它连接则证明对象是不可用的。
可以作为GC ROOTS的对象有四种:
1.栈帧中的本地变量表中的引用对象,就是平时所说的java对象,存放在堆中
2.方法区中的类静态属性引用的对象,也就是static修饰引用的对象,加载类时就加载到内存中
3.方法区中的常量引用的对象
4.本地方法栈中JNI(native方法)引用的对象
要真正宣告对象死亡需经过两个过程:
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]
垃圾收集算法:
三大垃圾收集算法:
1.标记/清除算法
2.复制算法
3.标记/整理算法
jvm采用‘分代收集算法’对不同区域采用不同的回收算法
新生代采用复制算法 :Eden from survivor to survivor 分成8:1:1 ,to survivor为保留空间;GC开始时,对象只会存在于Eden和from survivor区域,Eden所有存活的对象会复制到To survivor中,from survivor存活的对象会根据他们的年龄值决定去向,年龄超过年龄阈值(默认15),则移动到老年代,没有则移动到To survivor。
老年代采用标记/清除算法或者标记/整理算法:
GC的标记阶段需要STW(stop the world),让所有的java线程挂起,这样JVM才能安全地标记对象
OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。
GC不是在任意位置都可以进入,只能在safepoint处才能进入,safepoint不能太少,否则gc等待的时间会很久,也不能太多,否则将增加gc的负担。
safepoint主要存放在:
1.循环的末尾
2.方法临返回前
3.调用方法的call 指令后
4.可能抛出异常的位置
垃圾收集器:
如果说垃圾回收算法是内存回收的方法论,name垃圾收集器就是具体实现。JVM会结合针对不同的场景及用户的配置使用不同的收集器。
年轻代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old,parallel Old CMS收集器
特殊收集器:G1收集器
Serial:单线程、收集时必须停掉其它线程,等待收集工作完成后其它线程才能继续工作,client模式使用
ParNew收集器:serial的多线程版
Parallel Scavenge收集器:采用复制算法,支持多线程,吞吐量优先,适合后台运算
Serial Old:单线程,标记整理算法,client模式使用
Parallel Old:多线程,标记整理算法,吞吐量优先
CMS:多线程,支持并发,标记清除算法,最短回收停顿时间优先
它的运作分为4个阶段:
1.初始标记:标记一下GC ROOTS 能直接关联到的对象,速度很快,需要STW
2.并发标记:可达性分析
3.重新标记:修正因并发标记期间用户程序运作而产生变动的那一本部分对象的标记记录,会有些许停顿,需要STW,时间上一般 初始标记 < 重新标记 < 并发标记
4.并发清除
CMS缺点:
1.cms性能很容易受到cpu核数的影响
2.cms无法处理浮动垃圾(并发清除过程中产生的新垃圾)
3.cms采用标记清除算法,会存在垃圾碎片的问题
G1收集器:
G1(garbage first:尽可能多的收集垃圾,避免full gc),最短回收停顿时间优先,强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代和老年代收集器;
用到的算法为标记-清除、复制算法,jdk1.7和jdk1.8都是默认关闭的。g1是区域化的,它将java堆划分为若干个大小相同的区域(region),jvm可以设置每个region的大小(必须是2的幂),它会根据当前堆内存分配合理的region大小。
特点:
1.不会等内存耗尽或者快耗尽的时候开始垃圾收集,而是在内部才用了启发式算法,在老年代找出具有高收集收益的分区进行垃圾收集。同时G1会根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标时间越短年轻代空间就越小、总空间就越大;
2.G1采用内存分区(region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位回收,存活的对象复制到能一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案。
3.G1虽然也是分代收集器,但是是逻辑上的分代概念,不存在物理上的年轻代和老年代,每个分区可能随G1的运行在不同代之间切换,年轻代优化分为Eden和survivor
4.G1的收集都是STW的,每次收集可能既收集年轻代,也收集老年代
5.region的大小在1m - 32m之间 ,必须是2的幂
6.region分区内部又被分成了若干个大小为512byte的卡片(card),卡片会记录到全局卡片表中
7.垃圾收集过程也分为:初始标记、并发标记、重新标记、清除
简单gc日志查看:-XX:+PrintGCDetails、jconsole 工具、jstat命令
jstat -gcutil pid:查看对应java进程gc情况