对应区域 | 存放内容 | 是否线程私有 |
---|---|---|
程序计数器 | 字节码的行号指示器 | 是 |
虚拟机栈 | java方法被执行的时候会创建一个栈帧,里面包含局部变量表、操作数栈、动态连接、方法出口。 | 是 |
本地方法栈 | 本地方法,包含内容同虚拟机栈 | 是 |
堆 | 存放对象实例,GC管理的内存区域 | 线程共享,但是可以划分出多个线程私有的分配缓冲区提升对象分配时候的效率 |
方法区 | 加载好的类型信息、常量、静态变量、即时编译器编译后的代码缓存 | 线程共享 |
运行时常量池 | 方法区的一部分,编译期生成的各种字面量与符号引用 | 线程共享 |
本机直接内存 | NIO等类型会使用本地方法直接分配堆外的内存 |
垃圾回收算法
-
标记复制算法,适用于新生代(young GC),因为对象存活率较低,需要分配的存活区域(存活对象复制到这个区域),可以不是1:1,Serial、ParNew垃圾收集器是Eden:Survivor:Survivor=8:1:1(有研究表明98%的新生代对象会被gc)。老年代存活率比较高,需要分配的存活区域就需要1:1比较浪费空间,而且相应的复制存活对象复制操作也会变多。所以一般采用标记-整理算法(更关注吞吐量)和标记-清除算法(更关注延迟)。
-
标记-整理算法:因为算法整理完后不会产生碎片化区域,使得内存分配和访问(不整理不是连续内存的概率比整理的高)可以更快,因此可以有更大的吞吐量。
-
标记-清除算法:减少了移动对象的时间,在gc的时候会产生的停顿时间比标记-整理算法少,因此会有更低的延迟。
cms采用两种算法混合,当影响对象内存分配的时候再进行对象移动(整理) -
gcroots算法中根节点枚举一定要stop the world ,因为不暂停,根节点集合的对象引用关系还在变化,无法保证准确性。
HotSpot采用一个OopMap去记录所有的对象引用,这样就不需要遍历所有的数据 -
记忆集:主要用于解决跨代引用的问题,避免扫描整个老年代,而且把老年代分区,并标记每个分区是否有跨代引用,扫描这一些数据,这些数据相对同代引用会很少,因为老年代的数据很难消亡,这样使得跨代的引用,也很难消亡,随着年龄增长也就变为同代引用了。ps:卡集就是以内存块为分区条件。
垃圾收集器
衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。
- Parallel Scavenge收集器是一款新生代收集器,提供了两个参数用来控制吞吐量(运行用户代码时间/(垃圾收集时间+用户代码运行时间))(-XX:GCTimeRatio)和垃圾收集停顿的时间(-XX:MaxGCPauseMillis),当停顿时间小的时候,分配的新生代空间会变小,也就加快了收集速度,但是会导致gc频繁发生,影响吞吐量,反之亦然。
- CMS收集器(Concurrent Mark Sweep) 以获取最短回收停顿时间为目标。主要分为一下四个步骤:
- 初始标记 标记GC Roots直接关联到的对象,也就是roots直接的子节点。 需要stop the world,时间比较短
- 并发标记 并发的全局扫描,需要较长时间,而且当处理器核心数量较低的时候会导致, 需要分出一部分线程进行操作,数量较多时候不会出现这种问题。
- 重新标记 2是在并发的情况下,所以用户可能修改一部分标记记录,3主要用来修正这一部分,需要stop the world,时间比较短
- 并发清理 使用标记-清除算法,不需要移动对象,所以可以并发进行,但是需要预留空间给用户操作,不然空间不够会导致并发清除失败,用serial old来进行替代,导致更长的gc时间。同时由于使用标记清除算法会导致碎片空间,需要在一段时间之后进行清理,可设置几次fullgc后进行整理。同时并发清理过程中也会产生新的垃圾(浮动垃圾),所以不能在老年代满的时候再进行清理,可以通过CMSInitiatingOccupancyFraction设置到达百分之几的时候进行。
- G1收集器,将java堆空间分为多个region区域,每次根据每一个区域的回收价值来部分回收其中的几个区域,这也就可以间接的让用户设定允许的收集停顿时间。而对于大的对象会存放在一个Humongous Region中,而且也存在分代的思想。
G1需要解决的问题:- 跨region引用问题,使用记忆集,但他的卡表是双向的(可以找到我引用的对象,也可以找到谁引用了我),这种设计比较复杂,也就导致G1收集器需要消耗更多的额外内存。
- 并发标记问题,cms采用增量更新来解决在并发标记阶段用户修改的内容,而G1则采用原始快照的方式。而在这个阶段新增加的对象,会分配到一个规定的区域,且默认他们是存活的。
- 建立起可靠的停顿预测模型,通过每个region中的脏卡数量等信息,计算出来每个region回收的价值。
步骤: - 初始标记,与cms类似。并且会修改TAMS指针,保证下一阶段用户线程可以并发的分配内存。同时可以借用Minor GC时候同步完成,所以这一阶段基本上没有额外的停顿。
- 并发标记:与cms类似。
- 最终标记:处理SATB(原始快照)记录。
- 筛选回收,回收的region会采用标记复制算法复制到其他空的region中,并清除掉整个旧的区域,由于需要进行存活对象的移动,所以需要暂停用户线程,由多线程并行操作。
几款垃圾收集器的关联
- cms主要和pernew搭配,serial old只要作为cms的备用收集器,也就是当cms并行清理失败时候使用。
- serial是parnew的并行版本
- serial是新生代收集器 serial old是老年代收集器
- pernew是新生代收集器 pernew old是老年代收集器
- parallel scavenge 与parnew类似但是提供了可控制的吞吐量和最大回收时间,一般与parallel old搭配,在parallel old出来前一般与msc(sarial old)搭配
类加载的过程
加载
在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载与连接(验证、准备、解析)交叉进行
验证
- 文件格式验证:主要校验class文件是否符合java虚拟机规范中的要求。这阶段是根据二进制字节流进行的。文件格式验证校验成功后会将数据存储到java的方法区,后三个验证都是基于方法区的存储结构。
- 元数据验证:主要验证是否有父类,父类是否允许继承等元数据信息。
- 字节码验证:主要验证类的方法体(Class文件中的Code属性)
- 符号引用验证:主要验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。比如符号引用中通过字符串描述的全限定名是否可以找到对应的类;符号引用中的类、字段、方法的可访问性等。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
这里的初始值如果对象没有被final修饰,则会是对应的0值,因为将方法赋值的putstatic指令是存放在类构造器中,需要初始化阶段才会被执行。但是如果类字段的字段属性表中存在ConstantValue属性(也就是final类型),那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
也就是执行jvm虚拟机自动生成的<clinit>()
方法
运行时栈帧结构
- 局部变量表
局部变量表所需要的内存大小,在编译时候就已经被确定,是确定的,与方法区和堆中的内容不同(会动态变化)。因此这一块不需要gc 而相对的,可以通过变量槽的重用来节省空间。另外局部变量不像全局变量那样有准备阶段,初始化零值,所以必须要手动赋初始值。 - 操作数栈
- 动态连接
- 方法返回