赞
踩
这个名词听起来陌生也不陌生…但是它的定义到底是什么呢?
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
大意是:
Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。
其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会销毁。
其他数据区域是每个线程,每个线程的数据区域在线程创建时创建,在线程退出时销毁。
1)方法区:
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is created on virtual machine start-up.
Although the method area is logically part of the heap,…
It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
在上一篇文章的类加载篇幅,我们提到了Classs文件除了有类的类版本、字段、方法、接口等描述信息(也含有常量池信息(javaP查看)),用于存放编译期产生的各种字面量和符号引用。
呢么这部分内容将在类加载后保存到方法区的运行时常量池。
2)堆:
The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
The heap is created on virtual machine start-up.
在上一篇文章中,介绍了对象的创建流程,就是这里的对象创建。产生了对象,便可以作为对方法区访问的入口。
但是,随着JIT编译器的发展与逃逸分析技术逐渐成熟,在栈上分配内存、标量替换优化,所有的对象分配在堆上也不绝对。
堆内存区域在细分可以分为 Eden
、From Survivor
、To Survivor
,以及线程私有的分配缓冲区(TLAB
)
3)虚拟机栈:
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread.
A Java Virtual Machine stack stores frames (§2.6).
A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes.
每个方法在执行的同时都会创建一个栈帧,用于存储局部表量表
、动态链接
、方法出口
等;
每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中从入到出的过程。
栈帧中的分配布局:
局部表量表:
操作数栈:
以压栈和出栈的方式存储操作数的。
它主要保存计算过程中的结果,同时作为计算过程临时变量的存储空间。
帧数据区:
每个栈帧都包含一个指向运行时常量池
中该栈帧所属性方法的引用
,持有这个引用是为了支持方法调用过程中的动态连接
。
在Class文件的常量池中存有大量的符号引用
,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用
,这种转化称为静态解析。
方法返回地址:
当一个方法开始执行后,只有两种方式可以退出。 一种是遇到了方法返回的字节码指令; 一种是遇见异常,并且这个异常没有在方法体内得到处理。
4)程序计数器:
我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空;
5)本地方法栈:
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用native的方法呢?
以上5个部分按照线程独享/共享分类 :
以上5个结构按照GC分类:
运行时数据区跟JVM内存模型两个概念对于初次接触,是很容易产生懵逼感的一对概念。我们暂时先理解为 JVM运行时数据区是一种规范,而JVM内存模型是对该规范的实现。 在该实现中,我们关注堆与方法区(非堆),其余几块区域我们暂且按住不表。
堆区 与 非堆 在jmm概念中, 分配大致如下:
对以上区域划分,我们结合GC相关的概念,以FAQ的形式进行讨论,来阐明它这几个区域都是什么意思,以及为什么这么分(你需要知道GC的大概流程)。
case 1
: GC 名词解释
case 2
: 为什么需要Survivor区(S0、S1)? 只有Eden不行吗?
如果没有Survivor区,Eden区每一次的minor GC 后,仍旧存活的对象只能投递到老年代里面,这样老年代的空间会很快被打满,进而触发Major GC(因为Major GC 一般伴随着minor GC ,也可以看做触发了Full GC)。
老年代的内存空间是远大于新生代的,触发一次FULL GC 消耗的时间会很长 ,并且FULL GC 的情景下,根据垃圾回收器的不同,会生产STW,暂停业务线程。
诚然,我之前觉得这种STW中断业务线程是差强人意可以接受的,但随着业务量增加,生产触发了一次3S的STW ,百笔交接直接被网关熔至超时,这个月的成功率又达不到相关指标了。。。
所以Survivor区存在的意义,可以理解为一个中间区域。尽可能在这里对对象进行GC,提供老年代入场门槛,最终规避大的FULL GC。
case 3
: 为什么需要两个Survivor区?
这里个人理解是 跟GC算法中的复制算法初衷相似,主要是避免空间碎片化。
eden满后了 -> minor GC -> s0 。下一次s0 与 s1 进行对调。
case 4
: 新生代中Eden:S1:S2为什么是8:1:1 ?
新生代中的可用内存:复制算法用来担保的内存为9:1。
可用内存中 Eden:S1 = 8:1
即新生代中 Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的。
case 5
: 堆内存中都是线程共享的区域吗?
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
以上篇幅尝试解释了jmm实现的思路,呢么管对象创建也得管’‘埋’’ , GC 相关内容与行为又是怎么样的呢?
先问是不是在,在问为什么。
GC 这里的知识也同样适合这句话,是不是垃圾?如果是垃圾如何回收?
引用计数器法
:对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。 但是 A->B 、B->A 这样的指向关系难道不算垃圾? 其实是算的,他们理应称之为 ‘‘一对垃圾’’ 。可达性分析
。可达性分析
:通过GC ROOT 对象,开始向下寻找,看某个对象是否可达。
而可以做GC ROOT的对象有 类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。
而从跟进行检索的时候,又引入了三色标记的概念。
GC Roots开始遍历,怎么才能高效的标记这些对象,这就是三色标记法的作用了。因为如果堆内的对象越多,那么显然标记产生的停顿时间就越长。
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
但下述操作有可能会触发GC:
但GC也不是触发了就会发生,只有stw(stop the world)
即用户线程停下来后才会执行,那么如果千万线程如何治理呢?就是此处的安全点
和安全区域
。
安全点的选择
安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性能问题。
大部分指令的执行时间都非常短暂,通常会根据“ 是否具有让程序长时间执行的特征” 为标准。
比如: 选择一些执行时间较长的指令作为safe Point ,如:
- 循环的末尾
- 方法临返回前
- 调用方法之后
- 抛异常的位置
GC时线程的中断策略
如何在GC生时, 检查所有线程都跑到最近的安全点停顿下来呢?
抢先式中断
: ( 目前没有虚拟机采用了)
首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。
主动式中断
:
设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。
(客栈门上安装了一个显示器,上面会显示ture,或者false。如果系统需要垃圾回收,就会更新这个状态为true,线程到了客栈后看到为true,就进店别别出来了。)
安全区域Safe Region
SafePoint 机制保证了程序执行时, 在不太长的时间内就会遇到可进入GC的safepoint 。但是, 程序“ 不执行” 的时候呢?
例如线程处于Sleep 状态或Blocked 状态, 这时候线程无法响应JVM的中断请求, “ 走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况, 就需要安全区域(Safe Region) 来解决。
安全区域是指在一段代码片段中, 对象的引用关系不会发生变化, 在这个区域中的任何位置开始GC 都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint
安全区域执行流程
实际执行时:
1 、当线程运行到 Safe Region 的代码时, 首先标识己经进入了safe Region ,如果这段时间内发生GC ,JVM会忽略标识为 Safe Region 状态的线程(认为它是安全的):
2 、当线程即将离开时, 会检查JVM是否己经完成GC , 如果完成 , 则继续运行, 否则线程必须等待直到收到可以安全离开Safe Region 的信号为止。
举个生活列子:
一句话,睡觉可以,请先进入酒店再睡觉,并且进去是在屏幕上说一声我进入安全区域了,在睡觉。这样做的目的就是当要GC的时候不至于找不到你,如果看到屏幕上有你这个线程的名字,就知道你是安全的,就会忽略你;
你(线程)睡醒了要出门了。抬头看看大屏幕是不是安全在出去(true),如果不安全(false)就在酒店待着,别出门,等到gc完成后,状态变为false在出门。
R大发表于知乎的解释:https://www.zhihu.com/question/29268019/answer/43762165
第一种 :
从堆中所有的对象进行一次扫描,一个个问对象还活着吗?谁该被回收了,打上一个TAG。 这个行为我们称之为 标记
。
接下来我们对刚打上tag的对象,进行回收,做相应空间的释放。这个行为我们称之为清除
产生出来Plan A : 标记-清除
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
第二种 :
我们直接把内存分为两组相等的区域,每次只用一组。当一组空间告罄时,我们将存活的对象复制到另外一块区域,随后将使用的内存空间整体清理掉。
产生出来Plan B : 标记-复制
缺点:
第三种 :
在第一种标记-清除
中,如果后续步骤不直接对可回收的对象进行清理,而是让所有存活的对象都移向另一端,随后直接清理掉边界以外的内存。
呢么这种做法 我们可以称之为 标记-整理
;
第四种 :
Young区:对象在被分配之后,可能生命周期比较短。
Old区:Old区对象存活时间比较长,复制来复制去没必要。
新生代、老年代 各自特点是不一致的,如果可以有针对的选择垃圾回收算法,呢么应该是一种不错的选择。
对于新生代的"朝生夕死“ ,标记-复制
或许是一个不错的选择。
对于老年代的GC成功率,标记-整理
或许更为稳妥。
呢么Plan C ,就是 分代收集
了。
CMS:是一种以获取最短回收停顿时间
为目标的收集器。
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
优点: 并发收集 ,停顿低。
缺点: 产生大量空间碎片、并发阶段会降低系统吞吐量。
G1:
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中。
设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域:
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 MixGC;
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生;
在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。