Version: Next

三种JVM

  • Sun公司的HotSpot
  • BEA公司的JRockit
  • IBM公司的J9VM
➜ ~ java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

堆(Heap)

Heap堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件之后,需要把类、方法、变量放到堆内存中,保存所有引用类型的真实信息,以方便执行,堆内存分为三部分

  • 新生代 Young Generation Space Young/New
  • 老年代 Tenure Generation Space Old/Tenure
  • 永久代 Permanent Space Perm JDK 1.7 | 元空间 Metaspace JDK 1.8

image-20200617134608969

新生代——Eden、SurvivorTo、SurvivorFrom

  • JVM新创建的对象,除了大对象外,都会被存放在新生代,默认占1/3堆内存空间。
  • 由于JVM频繁创建新对象,所以新生代会频繁触发MinorGC进行垃圾回收。
  • 新生代又分为Eden、SurvivorTo、SurvivorFrom,谁是空的谁就是To
  • 空间比例: Eden:From:To = 8:1:1
  • Eden伊甸园区:

    • Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接分配到老年代。大对象的定义和具体JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可以通过XX:PretenureSizeThreshold设置其大小。
    • 在Eden区的内存空间不足时会触发MinorGC
  • SurvivorTo区 S1:

    • 保留上一次MinorGC时的幸存者
  • SurvivorFrom区 S0:

    • 将上一次MinorGC的幸存者作为这一次MinorGC的被扫描者

当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾 回收器将对伊甸园区进行垃圾回收(Minor GC)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也 满了,再对该区进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相 交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老 区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常 “OutOfMemoryError ”。

如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因 如下:

  1. Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
  2. 新生代的大小可通过参数-Xmn设置
  3. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环

新生代的GC过程叫做MinorGC,采用复制算法实现,具体过程如下:

    • 把在Eden区和SurvivorFrom区中存活的对象赋值到SurvivorTo区
    • 如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由XX:MaxTenuringThreshold设置,默认为15次,因为对象头中只分配了4bit用来表示分代年龄),将其复制到老年代,同时把这些对象的年龄加1
    • 并非必须达到 MaxTenuringThreshold 才进入老年代,如果同一代中,所有对象占用的堆空间超过了 Survivor 区的一半,则年龄(代数) 这个代数的对象就会直接进入老年代
    • 如果SurvivalTo区的内存空间不够,则也直接将其复制到老年代
    • 如果对象属于大对象(一般约2~128KB),则也直接将其复制到老年代
  1. 清空Eden区和SurvivorFrom区的对象
  2. 将SurvivorTo区和SurvivorFrom区互换,原来的SurvivorTo变为下一次GC时的SurvivorFrom区

老年代

老年代主要存放长生命周期的对象和大对象

  • 老年代的GC过程叫做MajorGC,在老年代,对象比较稳定,不会频繁触发MajorGC
  • 在进行MajorGC之前,JVM会进行一次MinorGC,在MinorGC后仍然出现老年代且当老年代空间不足或无法找到足够大的连续内存空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM堆内存空间
  • MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象
  • 因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。
  • MajorGC的标记清除算法容易产生内存碎片
  • 老年代在没有内存空间可分配时,抛出OOM Out Of Memory Error

空间分配担保机制

  • 在发生Minor GC之前,虚拟机必须先检查 老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看 - XX:HandlePromotionFailure 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允 许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大 于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
  • 解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无 法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代 本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与 老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
  • 取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对 象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实 地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下 都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。参见代码清单3-11,请读 者先以JDK 6 Update 24之前的HotSpot运行测试代码。

永久代、元空间(JDK1.8)

永久代指内存的永久保存区域

  • 主要存放Class和Meta元数据信息,Class在类加载时被放入永久代
  • 永久代和老年代、新生代不同,GC不会在程序运行时对永久代内存进行清理,这导致永久代的内存随着加载的Class文件的增加而增加,加载的Class文件过多就会触发Out Of Memory OOM异常
  • JDK 1.8起将永久代替换为元空间,元空间与永久代的功能类似。区别是:元空间不使用虚拟机内存,而是直接使用操作系统本地内存。因此,元空间的大小不受JVM内存限制,只与操作系统内存有关
  • JDK1.8后,JVM将类的元数据放入本地内存,将字符串常量池和静态变量放到Java堆中。这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而是由操作系统实际可用的内存空间决定

对于方法区的实现,为什么从永久代更换到元空间?

出于内存空间与内存利用的角度考量

  • 永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出java.lang.OutOfMemoryError:PermGen
    • 永久代调优困难,虽然可以设置永久代大小,但是很难确定一个合适的大小,影响因素过多,不如类的数量、常量的数量具体有多少
    • 永久代中的数据会随着一次full GC发生移动,比较消耗虚拟机性能
    • HotSpot JVM的每种垃圾回收器都需要特殊处理永久代中的元数据
  • 元空间使用本地内存,理论上系统可以使用的内存有多大,元空间就有多大,不会出现内存溢出
    • 实现对元空间的无缝管理
    • 可以通过-XX:MetaspaceSize-XX:MaxMetaspaceSize配置元空间大小
    • 如果Metaspace占用达到了设定的最大值,就会触发GC来收集死亡对象和类的加载器
    • 简化了full GC以及对后续并发隔离元数据等方面进行了优化

对象结构图

一个对象的大小是8个字节的整数倍

image-20200618203819622

JVM参数设置

Xms 起始内存

Xmx 最大内存

Xmn 新生代内存

Xss 栈大小。 就是创建线程后,分配给每一个线程的内存大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小

  • 收集器设置

    -XX:+UseSerialGC:设置串行收集器

    -XX:+UseParallelGC:设置并行收集器

    -XX:+UseParalledlOldGC:设置并行年老代收集器

    -XX:+UseConcMarkSweepGC:设置并发收集器

  • 垃圾回收统计信息 -XX:+PrintGC

    -XX:+PrintGCDetails

    -XX:+PrintGCTimeStamps

    -Xloggc:filename

  • 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

  • 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数

例题:

-Xms1G -Xmx2G -Xmn500M -XX:MaxPermSize=64M -XX:+UseConcMarkSweepGC -XX:SurvivorRatio=3, 请问eden区最终分配的大小是多少?

-Xms1G 设置Java堆最小值为1G

-Xmx2G 设置Java堆最大值为2G

-Xmn500M 设置新生代大小为500M(一个Eden区,两个Survivor区)

-XX:MaxPermSize=64M 设置永久代大小为64M

-XX:+UseConcMarkSweepGC 设置使用CMS收集器

-XX:SurvivorRatio=3 设置Eden区与Survivor区大小的比例

本题看新生代大小,新生代为500M,三个区比例默认为8:1:1,现在设置为3:1:1,很容易计算出Eden大小为300M