JAVA虚拟机
1.JAVA虚拟机架构图
2. JVM内存模型
栈是运行时的单位,里面存储的信息与当前线程或程序相关信息,包括局部变量、程序运行状态、方法返回值等。而堆是存储的单元,只保存对象信息。
2.1 JAVA堆内存
-
JAVA堆是所有线程共享的一块内存区域,是JVM内存区域中占比最大的一块区域,垃圾收集器管理的主要区域,在JVM启动时创建。JAVA堆用于存放对象实例数据,new Object();
-
Java堆可以处于物理上的不连续,而逻辑上连续即可;
-
Java堆是垃圾收集器管理的主要区域,由于JVM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区 和 To Survivor 区)和老年代;
-
Java堆无法扩展时,将会抛出 OutOfMemoryError 异常;
-
Java堆参数设置:-Xms最小值,-Xmx最大值
-
Java堆内存可划分为:新生代(Eden、S0、S1),老年代(Tenured),永久代(JDK1.8之前)和元空间(JDK1.8之后)
-
新生代(年轻代 Young):主要用于存储那些刚创建的对象,一般占堆内存的 1/3 空间。由于频繁创建对象,所以新生代会频繁的触发 Minor GC 进行垃圾回收。Minor GC 采用复制算法(复制 -> 清空 -> 互换)。
-
Eden区:Java 新对象的出生地,如果创建的对象占用的内存较大(人为设置的阈值),则直接分配到老年代中。当 Eden 区内存不够时会触发 Minor GC,对新生代进行一次垃圾回收,垃圾回收之后依然存在的对象年龄 +1,当年龄达到一定值时该对象直接进入老年代。
-
From Servivor:上一次Minor GC 回收的幸存者,作为下一次 GC 的被扫描者。
-
To Servivor:将 Eden 区 和 From Servivor 中经过 Minor GC 回收之后依然存在的对象复制到 To Servivor 中,即保留一次 Minor GC 的幸存者,下一次 GC 作为 From Servivor 区。
-
-
老年代:主要用于存储那些在新生代经过多次(设置的阈值,一般设置为15)Minor GC清理之后,依然存在的对象,以及用于存放新生代创建的某个大对象(设置的阈值),即大对象直接进入老年代。
-
永久代与元空间:永久代使用的是 JVM 的堆内存,而元空间使用的是本机物理内存,所以元空间的大小受限于本机物理内存的限制。
-
- 新生代优化算法:实际运行中,由于 Eden 区总是会保存大量的新生对象,所以 HotSpot 虚拟机为了加快此空间的内存分配,采用 “Bump-The-Pointer” 和 “TLAB(Thread-Local Allocation Buffers)”两种技术。
- Bump-The-Pointer 算法主要特点是跟踪在 Eden 区保存的最后一个对象,最后保存的对象一般会保存在 Eden 区的顶部,这样每次只需要在创建新对象的时候判断最后保存的对象后面是否还有足够的空间,这样就可极大的提高内存的分配速度。
-
优点:可提高内存分配速度
-
缺点:不适合多线程操作的情况
-
- TLAB(Thread-Local Allocation Buffers)算法将 Eden 区分为多个数据块,每个数据块分别使用 “Bump-Th-Pointer”算法进行对象保存于内存分配。
- Bump-The-Pointer 算法主要特点是跟踪在 Eden 区保存的最后一个对象,最后保存的对象一般会保存在 Eden 区的顶部,这样每次只需要在创建新对象的时候判断最后保存的对象后面是否还有足够的空间,这样就可极大的提高内存的分配速度。
JDK1.8之前堆内存划分:
JDK1.8之后堆内存划分:
堆内存空间参数调整(+:表示开启某项功能,-:表示关闭功能):
参数名称 | 描述 | |
1 | -Xms | 设置堆内存初始分配大小,默认为物理内存的 1/64 |
2 | -Xmx | 设置最大堆内存,默认为物理内存的 1/4 |
3 | -XX:PrintGCDetails | 输出详细的GC收集日志 |
4 | -XX:+PrintGCTimeStamps | 输出GC的时间戳信息 |
5 | -XX:+PrintGCDateStamps | 输出GC时间戳信息(以日期的形式) |
6 | -XX:+PrintHeapAtGC | 在GC进行处理的前后打印对内存信息 |
7 | -Xloggc:保存路径 | 设置GC回收日志信息保存文件 |
年轻代内存参数设置:
参数名称 | 描述 | |
1 | -Xms | 设置年轻代堆内存代销,默认为物理内存的 1/64 |
2 | -Xss | 设置每个线程栈大小,JDK1.5之后默认每个线程分配 1M 的栈大小,减少次数值可以产生更多的线程对象,但是不能无限生成。 |
3 | -XX:SurvivorRatio | 设置 Eden 与 Survivor 空间的大小比例,默认为 “8:1:1”,不建议修改。 |
4 | -XX:NewSize | 设置新生代内存区大小 |
5 | -XX:NewRatio | 设置年轻代与老年代的比率 |
老年代内存参数设置:
参数名称 | 描述 | |
1 | -XX:+UseAdaptiveSizePolicy | 控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
2 | -XX:PretenureSizeThreshold | 控制直接进入老年代的对象大小,大于这个值的对象会直接分配到老年代中 |
永久代内存参数设置:
参数名称 | 描述 | |
1 | -XX:PermSize | 设置永久代的初始大小 |
2 | -XX:MaxPermSize | 设置永久代的最大值 |
元空间内存参数设置:
参数名称 | 描述 | |
1 | -XX:MetaspaceSize | 设置元空间的初始大小 |
2 | -XX:MaxMetaspaceSize | 设置元空间的最大容量,默认是没有限制的(受限于本地物理机的内存限制) |
3 | -XX:MinMetaspaceFreeRatio | 执行GC之后,最小的剩余元空间百分比,减少为分配空间所导致的垃圾收集 |
4 | -XX:MaxMetaspaceFreeRatio | 执行GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 |
2.2方法区/永久代(线程共享区域)
-
方法区与JAVA堆一样,是线程共享的内存区域。方法区即我们常说的永久代(Permanent Generation)主要存储JVM加载的类信息、常量、静态变量、指向class实例引用等。HotSpot VM把GC分代收集扩展至方法区,即使用 Java 堆的永久代来实现方法区,这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存(永久代的内存回收的主要目标针对的是常量池的回收和类的卸载),从而不用专门开发额外的内存管理器来管理这部分内存。
-
垃圾回收在这个区域出现较少,主要针对常量池的回收和类的卸载。
-
运行时常量池(Runtime Constant Pool)也是方法区的一部分。
-
编译后的class文件中的常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放在运行常量池中。
-
方法区(运行常量池申请不到内存资源时)也会抛出 OutOfMemoryError 异常。
2.3 JAVA虚拟机栈
JAVA虚拟机栈(Java Virtual Machine Stack)描述的是JAVA方法执行的动态内存模型,是线程私有的,它的生命周期与线程相同。
JAVA虚拟机每执行一个方法时会产生一个栈帧,随后将其保存到栈(后进先出)的顶部,方法执行完毕后会自动将此栈帧进行出栈,栈的顶部表示当前方法。
JAVA虚拟机栈帧由以下部分组成:
-
局部变量表(Local Variables):存放编译期可知的各种基本数据类型,引用类型,方法返回地址(Return Address)。局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧分配多少内存是固定的,在方法运行期间不会改变局部变量表的大小。
-
操作数栈(Operate Stack):表达式计算在栈中完成。
-
指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool):引用其它类的常量或者使用String池中的字符串。
-
方法返回地址(Return Address):方法执行完后需要返回调用此方法的位置,所以需要在栈帧中保存方法返回地址。
Java虚拟机栈可能会抛出两种异常:
-
如果线程请求栈的深度过大,虚拟机可能会抛出StackOverflowError异常。
-
如果虚拟机的实现中允许虚拟机栈动态扩展,当内存不足以扩展栈的时候,会抛出 OutOfMemoryError 异常。
2.4 程序计数器
程序计数器是JVM中划分的一小块内存,用于保存当前执行线程字节码的行号,字节解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,是JVM中唯一不会抛出 OutOfMemoryError 的区域。程序计数器可以使中断的线程恢复到正确的执行位置,每个线程都有自己独立的程序计数器。
2.5 本地方法栈
Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行 native 方法服务。
2.6 垃圾收集算法&垃圾收集器
2.6.1 如何确定垃圾
-
引用计数法(Reference Counting):在Java中,需要操作一个对象时必须先对其进行引用。因此,可通过引用计数来判断一个对象是否可以回收。即一个对象如果没有任何与之关联的引用,即他们的引用计数器都为0时,说明该对象没有被引用,为垃圾对象,那么这个对象就可进行垃圾回收。引用计数法无法解决循环引用的问题。(详解可阅读文章:https://www.zhihu.com/question/21539353)
-
根搜索算法(GC Roots Tracing):通过一系列的“GC Roots”对象作为起点搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象不可达,即该对象为垃圾对象。不可达对象变为可回收对象至少要经过两次标记过程,两次标记过程仍然是可回收对象,则将面临回收。JAVA中可以作为 GC Roots的对象包括以下几种:
-
JAVA虚拟机栈中引用的对象;
-
方法区中静态属性引用的对象;
-
方法区中常量引用的对象;
-
本地方法栈中本地方法引用的对象。
-
2.6.2 垃圾收集算法
-
标记-清除(Mark-Sweep):从根集合开始扫描,对存活的对象进行标记,标记完毕之后,再扫苗真个空间中未标记的对象,并进行回收。红色表示不存活对象,绿色表示存活对象,蓝色表示清除后剩余空间。
-
优点:在空间中存活对象较多的情况下较为高效。
-
缺点:由于该算法回收不存活对象所占用的内存,因此会造成内存碎片。
-
-
标记-压缩(Mark-Compact):该算法在标记阶段与“标记-清除”算法相同,清除阶段时,该算法对不存活的对象所占用的空间进行回收之后,会将所有存活的对象都忘左端空闲的空间进行移动,并更新引用其对象指针。红色表示不存活对象,绿色表示存活对象,蓝色表示清除后剩余空间。
-
优点:不产生内存碎片。
-
缺点:清除不存活对象所占用的内存之后,需要对存活对象进行移动,成本相对较高。
-
-
复制算法(Copy):从根集合扫描出存活的对象,并将找到的存活对象复制到一块完全未使用的空间。红色表示不存活对象,绿色表示存活对象,蓝色表示清除后剩余空间。
-
分代收集算法(Generational Collecting):
2.6.3 垃圾收集器
-
串行GC(Serial Copying):
- 使用算法:复制(Copying)清理算法
- 操作步骤:
- 扫描年轻代中所有存活的对象;
- 使用 Minor GC 进行垃圾回收,同时将存活的对象保存到 “S0”或 “S1”区;
- 在上一次 Minor GC 的基础上进行 “S0”和 “S1”去的角色互换;
- 经历过许多次 Minor GC 依然存活的对象晋升到老年代。
-
并行回收GC(Parallel Scavenge):
- 使用算法:复制算法;
- 操作步骤:在扫描和复制时均采用多线程方式处理,并行回收GC为空间较大的年轻代回收提供许多优化。
- 优势:在多CPU的机器上对其GC耗时会比串行方式段,适合多CPU、对暂停时间要求较短的应用。
-
Parallel Scavenge
-
Serial Old
-
Parallel Old
-
CMS-Concurrent Mark-Sweep
2.6.4 如何手动触发GC
-
System.gc()方法;
-
Runtime.getRuntime().gc()方法。
3. 对象创建流程
-
当现在程序之中需要产生新生的实例化对象(关键字new,对象克隆,反射实例化)的时候那么就一定要进行内存空间的开辟,所以此时需要申请新的内存空间;
-
新对象申请空间默认都在伊甸园区(新生代)进行开辟空间,所以首先需要判断伊甸园区是否有空余的内存空间,如果有空余的内存空间,则直接在伊甸园区开辟新的堆内存空间,此时不会发生有GC处理;
-
如果新对象无法在伊甸园空间申请出新的空间,那么就表示现在的伊甸园区内存空间不足,不足就需要将那些无用的新对象进行回收(Minor GC),当回收完成后要继续判断伊甸园区的剩余空间是否可以容纳下新的对象,如果可以容纳,则开辟新空间,保存新对象。
-
若果此时伊甸园区即使执行了Minor GC之后发现依然没有可以被回收的对象,那么这个时候将继续判断存活区是否有剩余空间(存活区一般与伊甸园区比率为 1:1:8),如果存活区有空余空间,则将伊甸园区中活跃的部分对象直接保存到存活区,相当于伊甸园区可以腾出部分的空间(这个空间非常的小)来供新对象使用;
-
如果此时存活区依然满了(空间不足),则继续向老年代进行内存空间的申请,首先会判断老年代的空间是否空余,如果现在有空余的空间,则将存活区的活跃对象保存到老年代,而后存活区得到了空间释放,伊甸园区也得到了空间释放,则对象空间申请成功。
-
如果现在老年代也是满的,那么这个时候会执行FULL GC(完全GC,major GC)进行老年代的内存释放,如果可以释放成功,则进行对象的保存;如果释放不成功,则表示已经没有无用的内存空间了,那么此时就会跑出OOM(out of memory error)错误提示;
自动的GC什么时候触发?
答:GC的触发有两类:
-
Minor GC:发生在年轻代内存空间,当年轻代内存空间不足时会进行触发,释放年轻代中不活跃的对象
-
FULL GC(Major GC):发生在老年代内存空间,当老年代内存空间不足时会进行触发FULL GC,如果触发full gc之后,内存空间依然不足,则会产生OOM错误提示。
何对JVM进行调优的?
-
在堆内存之中会存在有一个伸缩区的概念,堆内存默认情况下最大可用内存为整体内存的四分之一,默认使用的内存为整体内存的六十四分之一,这个时候只需要避免伸缩区的频繁变更,就可以提升JVM的运行性能;
-
可以在程序执行的时候使用“-Xmx”设置最大可用内存,“-Xms”设置初始化内存空间大小,将这两个内容设置为一样的空间大小,就可以提升JVM的运行性能。