导航
内存划分
- 方法区(永久代)
- 线程之间共享的区域
- 常量、静态变量以及 jit 编译后的代码都在方法区
- 主要存储已被虚拟机加载的类信息
- 运行时常量池
- Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 堆内存
- 线程之间共享,垃圾回收的主要场所
- -Xmx 和-Xms 控制大小
- 虚拟机栈(栈内存)
- 局部变量、基本数据类型变量、引用变量
- 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
- 程序计数器
- 当前线程执行的字节码位置指示器
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
- 本地方法栈
- 提供 native 方法服务
jdk8 的变化
- 元空间代替了永久代
- 元空间和永久代类似,都是对 JVM 规范中方法区的实现。区别在于元空间并不在虚拟机中,而是使用本地内存,默认情况下元空间的大小仅受本地内存限制,也可以通过-XX:MetaspaceSize 指定元空间大小
- 原因
- 字符串在永久代中,容易出现性能问题和内存溢出的问题。类和方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。使用元空间则使用了本地内存。
内存分配与垃圾回收
堆内存
jvm 内存可以分为堆内存与非堆内存,堆内存分为年轻代和老年代,年轻代分为一个 eden(伊甸)区和两个 survivor(幸存)区。
- 参数设置
- -Xms,最小内存,也是初始内存,默认物理内存 1/64
- -Xmx,最大内存,默认物理内存 1/4
- 默认空余堆内存小于 40%时,JVM 就会增大堆直到-Xmx 的最大限制。空余堆内存大于 70%时,JVM 会减少堆直到-Xms 的最小限制。因此我们一般设置-Xms 和-Xmx 相等以避免在每次 GC 后调整堆的大小。
- -Xmn,设置年轻代大小
- -XX:SurvivorRatio,设置年轻代 eden 区和 survivor 区(一块)的比值
非堆内存
- 参数设置
- -XX:PermSize,非堆内存初始值,默认是物理内存的 1/64
- -XX:MaxPermSize,最大非堆内存的大小,默认是物理内存的 1/4
创建对象时的内存分配
一般情况下我们通过 new 指令来创建对象,当虚拟机遇到一条 new 指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。
通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从 Java 堆中划分出来,在分配的内存上完成对象的创建工作。
- 指针碰撞:如果 java 堆内存是规整的,用过的在一边,没用过的在另一边,那只是挪动指针
- 空闲列表:如果 java 堆内存是不规整的,那需要维护一个空闲内存的列表
是否规整由垃圾收集器是否有压缩整理的功能来决定
如何保证内存分配的线程安全?
- cas+失败重试
- 本地线程分配缓存(TLAB):每个线程都预先分配一小段内存,只有用完并分配新的缓存时,才需要进行同步锁定。是否用 TLAB 可以用过-XX:+/-UserTLAB 设定
创建的对象会优先在 Eden 分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个-XX:PretenureSizeThreshold 参数,令大于这个参数值的对象直接在老年代中分配,避免在 Eden 区和两个 Survivor 区发生大量的内存拷贝。另外,长期存活的对象将进入老年代,每一次 MinorGC(年轻代 GC),对象年龄就大一岁,默认 15 岁晋升到老年代,通过-XX:MaxTenuringThreshold 设置晋升年龄。
如何判定对象是否应该回收
- 引用计数法:有一个引用即计数加一,回收时只收集计数为 0 的对象,但无法处理循环引用
- root 根搜索法:通过一系列作为 root 的对象作为起始点,向下搜索。如果一个对象无法被搜索到,即可回收。
- root 对象
- 栈内存中引用的
- 方法区中静态引用和常量引用指向的对象
- 被启动类(bootstrap 加载器)加载的类和创建的对象
- native 方法中 jni(java native interface) 引用的对象
- root 对象
对象的回收(垃圾回收)
- minor gc(年轻代 gc):对象优先在 eden 区分配,当 eden 空间不足事发生一次 minor gc。因为大多数对象朝生夕灭,所以 minor gc 非常频繁,速度也很快
- eden 和 survivorFrom 复制到 survivorTo(如果对象年龄到了老年代的标准,就复制到老年区),age+1
- 清空 edge,survivorFrom
- survivorTo 和 survivorFrom 互换
- full gc(老年代 gc):当老年代没有足够空间时发生 full gc,一般 full gc 都会有一次 minor gc
- 动态对象年龄判定:如果 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold
- 空间分配担保:发生 Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次 Full GC(老年代 GC),如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则改为进行一次 Full GC。
回收算法
- 标记——清除
- 执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,并且会产生内存碎片。
- 执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,并且会产生内存碎片。
- 复制
- 把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
- 把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
- 标记——整理
- 结合了“标记-清除”和“复制”两个算法的优点,也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
- 结合了“标记-清除”和“复制”两个算法的优点,也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
垃圾收集器
- serial
- 新生代,单线程,复制算法,虚拟机运行在 Client 模式下的默认新生代收集器
- 执行垃圾回收的时候需要 Stop The World
- 简单高效,对于限定在单个 CPU 环境来说,Serial 收集器没有多线程交互的开销
- parnew
- serial 多线程版本,新生代,复制算法
- 很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器
- parallel scavenge
- 新生代,复制算法,并行
- 更加关注吞吐量(吞吐量就是 cpu 用于运行用户代码的时间与 cpu 总消耗时间的比值)。可以通过-XX:MaxGCPauseMillis 参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio 参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy 参数可以打开 GC 自适应调节策略,该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略是 Parallel Scavenge 收集器和 ParNew 的主要区别之一。
- serial old
- serial 的老年代版本,标记整理
- 运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器
- 在 Server 模式下存在主要是做为 CMS 垃圾收集器的后备预案,当 CMS 并发收集发生 Concurrent Mode Failure 时使用
- parallel old
- parallel scavenge 的老年代版本,标记整理法
- 吞吐量优先
- cms(Concurrent Mark Sweep)
- 老年代,标记整理,以获取最短回收停顿时间为目标,通常与 ParNew 一起使用
- 多线程的标记清除算法
- 垃圾收集过程
- 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下 GC Root 能直接关联到的对象,速度很快
- 并发标记:主要标记过程,这个标记过程是和用户线程并发执行的
- 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)
- 并发清除:和用户线程并发执行的,基于标记结果来清理对象
- 如果在重新标记之前刚好发生了一次 MinorGC,会不会导致重新标记阶段 Stop the World 时间太长
- 不会的,在并发标记阶段其实还包括了一次并发的预清理阶段,虚拟机会主动等待年轻代发生垃圾回收,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段 Stop The World 的时间
- 优点
- 以降低垃圾回收的停顿时间为目的,很显然其具有并发收集,停顿时间低的优点
- 缺点
- 对 CPU 资源非常敏感,因为并发标记和并发清理阶段和用户线程一起运行,当 CPU 数变小时,性能容易出现问题。
- 收集过程中会产生浮动垃圾,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数-XX:CMSInitiatingOccupancyFraction 的值来控制内存使用百分比。如果该值设置的太高,那么在 CMS 运行期间预留的内存可能无法满足程序所需,会出现 Concurrent Mode Failure 失败,之后会临时使用 Serial Old 收集器做为老年代收集器,会产生更长时间的停顿。
- 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要 20%的预留空间用于这些浮动垃圾
- 标记-清除方式会产生内存碎片,可以使用参数-XX:UseCMSCompactAtFullCollection 来控制是否开启内存整理(无法并发,默认是开启的)。参数-XX:CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的 Full GC 后进行一次带压缩的内存碎片整理(默认值是 0)。
- g1(garbage first)
- 将新生代和老年代取消了,取而代之的是将堆划分为若干的区域,仍然属于分代收集器,区域的一部分包含新生代,新生代采用复制算法,老年代采用标记-整理算法
- 通过将 JVM 堆分为一个个的区域(region),G1 收集器可以避免在 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据回收时间来优先回收价值最大的 region
- 特点
- 并行与并发:G1 能充分利用多 CPU,多核环境下的硬件优势,来缩短 Stop the World,是并发的收集器。
- 分代收集:G1 不需要其他收集器就能独立管理整个 GC 堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次 GC 的对象。
- 空间整合:G1 从整体来看是基于标记-整理算法,从局部(两个 Region)上看基于复制算法实现,G1 运作期间不会产生内存空间碎片。
- 可预测的停顿:能够建立可以预测的停顿时间模型,预测停顿时间。
- 阶段
- 初始标记
- 并发标记
- 最终标记
- 筛选标记:首先对各个 Region 的回收价值和成本进行计算,根据用户期望的 GC 停顿时间来制定回收计划
内存调优
命令
- jps:显示所有 java 进程 pid,-v 输出 jvm 启动参数等
- jinfo:查看进程运行环境参数等信息
- jstack:查看某个 java 进程内的线程堆栈信息
- jstack pid 可查看当前进程中各个线程状态信息,包括持有和等待的锁
- jmap:堆使用情况。j
- jmap -heap pid 可查看当前进程的堆信息和使用的 gc 收集器,包括年轻代、老年代的大小分配等
- 生成 dump 文件 jmap -dump:format=b,file=/home/dump.hprof pid
- jstat:实时命令行的监控,包括堆信息以及实时 gc 等。
- jstat -gcutil pid 1000 每隔一秒查看当前 gc 信息
排查异常
- 查看 jvm 启动参数,看内存是否存在明显问题
- 查看 gc 日志,看 gc 频率(young 10 秒一次,full 1 天 1 次)和时间(young 100ms 内,full 1 秒内)是否明显异常
- 查看当前进程状态信息, top -Hp pid,包括线程个数等信息
- jstack pid 查看当前线程状态,是否存在死锁等关键信息
- jstat -gcutil pid 查看当前进程 gc 情况
- jmap -heap pid 查看当年进程的堆信息,包括使用的垃圾收集器等信息;
- 用 jvisiual 打开 dump 文件,分析 review
类加载
指虚拟机把描述类的数据从 class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。
- 加载:加载是指将类的.class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。
- 验证:验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。
- 准备:准备阶段为类的静态变量分配内存,并将其初始化为默认值。假设一个类变量的定义为 public static int val = 3;那么变量 val 在准备阶段过后的初始值不是 3 而是 0。
- 解析:解析阶段将类中符号引用转换为直接引用。
- 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
- 直接引用:可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
- 初始化:初始化阶段为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
类加载器的分类
- 启动类加载器(Bootstrap ClassLoader):启动类加载器负责加载存放在 JDK\jre\lib(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath 参数指定的路径中的类。
- 扩展类加载器(ExtClassLoader):扩展类加载器负责加载 JDK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.*开头的类)。
- 应用类加载器(AppClassLoader):应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。
类加载器的职责
- 全盘负责:当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 父类委托:类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。父类委托机制是为了防止内存中出现多份同样的字节码,保证 java 程序安全稳定运行。
- 缓存机制:缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,先从缓存区寻找 Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启 JVM,程序的修改才会生效。