Java程序的执行流程
内存分配的发生从类加载器加载各个类的字节码文件开始,到程序执行期间,JVM会用一段空间来存储和管理此期间用到的数据和相关信息,这段空间被称为Runtime Data Area,这个过程被称为内存管理。
JVM内存分区(运行时数据区Runtime Data Area)
重点关注:堆区,栈区,方法区
堆区
存放对象,不存放对象的引用和基本数据类型。
栈区
变量(对象的引用,和基本数据类型本身)——对应局部变量区
每个线程对应一个栈区,每个栈区的数据是私有的,不和其他栈区共享
方法区
class(class的原始代码),static变量
永久代:是hotspot(jvm的实现之一)的一个概念,jrocket(Oracle),J9(IBM)未必有。java8之前hotspot在内存中划分出一块区域来存储类的元信息、类变量以及内部字符串等内容,把它当做方法区来用。
从JDK1.7就开始了移除永久代的工作,JDK1.8永久代被使用本地内存(不是虚拟机)的元空间替代,都是对JVM中的方法区实现。这种架构下,元数据突破了原来-XX:MaxPermSize的限制,可以使用更多的内存,受本地内存大小限制。但是升级后堆空间可能会增加。
程序计数器
字节码行号指示器。分支、循环跳转、异常处理、线程恢复等依赖计数器来完成。
本地方法栈
功能类似栈区,为虚拟机使用到的Native方法服务。
Java内存分配的过程
- 每运行一个Java程序,都会产生一个Java进程,这个进程先从classpath读取.class(含有main方法)文件,读取文件的二进制数据,加载到方法区
- 不含有main方法的.class类并不在创建进程的时候加载,而是在创建对象的时候才加载到方法区。
- 每个Java进程会创建1个或多个线程。每个Java进程对应唯一一个JVM实例,每个JVM实例对应一个堆,每个线程有一个自己私有的栈。
- 进程中创建的所有的类(或数组)的实例都放在堆中,由该进程的所有线程共享。Java中分配的堆内存是自动初始化的,即为一个对象分配内存的时候,会初始化这个对象中的变量。
- 但是对这个对象的引用这是在栈区分配的。局部变量被new出来时,堆区和栈区都有分配空间,当局部变量的生命周期结束后,栈区的内存被回收,堆区的内存则等待被GC。
- 栈的存取速度比堆快。
- 基本数据类型 int a=3,变量和3(字面量)都存储在栈区,当生命周期结束后,立即被被回收
- 字面量是共享的,创建a时会创建一个为a的引用,然后查找有没有字面量为3的引用,如果没找到就开辟一个字面量为3的地址,然后将a指向3的地址,如果已有3的字面量,直接指向即可。
内存回收机制
判断内存可回收的两种机制
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。然而在主流的 Java 虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。
可达性分析法:通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
了解GC Roots种类
- Class——由系统加载器(System class loader)加载的对象,这些是不能被回收的,他们可以以静态字段的方式保存持有其他对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非对应的java.lang.Class实例以其他的某种(或多种)方式成为roots,否则他们不是roots。
- Thread-活着的线程
- Stack Local-Java方法的local变量或参数
- JNI Local-JNI方法的local变量或参数
- JNI Global-全局JNI引用
- Monitor Used-用于同步的监控对象
- Held by JVM-用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其他信息,因此需要去确定哪些是属于”JVM持有”的了。
GC(Garbage Collector)原理解析
一般讨论的垃圾回收主要指Java堆内存的新生代和老年代。
Java堆内存结构
堆内存有3大块:新生代,老年代,永久代
新生代又分为Eden,s0,s1区(s—survivor)
1 | +---------------------------+-------------------------------+-------------------+ |
从左到右,生命周期越来越长。
- S0和S1是2个大小相等的区域,分配内存空间只会在其中一个进行,另外一个空间来辅助进行新生代进行垃圾回收,因为新生代的垃圾回收策略基于复制算法,其思想是将Eden区及2个Survivor中的某个区,如S0区里面需要存活的对象复制到另外一个空的Survivor去,如S0区里面需要存活的对象复制到另一个空的Survivor,如S1区,然后可以回收Eden和S0区域里的死亡对象。下一次回收就对调S0和S1两个区的角色,S1用来存放或对象而S1用来辅助回收垃圾,如此循环利用。
- 有些文章并不将永久代纳入堆内存,实际上永久代指的是方法区,而方法区经常被称为Non-Heap(非堆)。仅仅在HotSpot虚拟机的实现中,才将GC分代收集扩展至方法区,或者说使用永久代来实现方法区,对于其他的虚拟机是不存在永久代这个概念的。
- 并非所有的对象创建都会在Eden区中分配内存空间。对于Serial和ParNew垃圾收集器,通过制定
-XX:PretenureSizeThreshold={Size}
来设置超过这个阈值大小的对象进入老年代。
分代回收算法
新生代的垃圾回收和老年代的垃圾回收采用的是不同回收算法。
针对新生代,主要采用复制算法,而老年代,通常采用的是标记-清除算法或者标记-整理算法来进行回收。
复制算法
HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1:1。算法参考上文S0和S1的生命周期,牺牲10%的空间用于复制。如果复制时,Survivor区放不下存活的对象,则那么将对象存到老年代。
标记-清除算法
在标记阶段标记需要回收的对象空间,然后在清除阶段里面,将这些标记出来的空间回收掉。这种算法有2个问题:一个是标记和清除的效率不高,另一个问题时清理后会产生大量不连续的内存碎片,这样会导致在分配大对象的时候无法找到足够的内存而触发另一次垃圾回收动作。
标记-整理算法
标记-整理算法有效预防了标记-清除算法中可能产生过多内存碎片的问题。在标记需要回收的对象后,它会将所有存活的对象挪到一起,然后再执行清理。一般作为标记-清除的备选方案。
GC的工作流
标记
找出所有live不为0的实例
找出所有GC的根节点(GC Root),将他们放在队列里,遍历所有的节点,子节点,以及子子节点,将他们标记为live,弱引用不被考虑在内。
计划和清理
计划:遍历所有标记节点(live),根据特点算法,判断是否需要压缩。
清理:遍历所有标记节点(live和Dead),将所有live节点移到可用节点,释放free空间。
引用更新和压缩
引用更新:计算所有压缩节点的新地址,并按新地址将节点移动到GC Root的队列中,遍历所有的根节点,子节点,子子节点,对所有节点的引用进行地址更新,包括弱引用。
压缩:根据计算出的新地址,把对象移动到新地址。
触发GC的条件
当应用程序分配新的对象,GC代的大小已经达到阈值
代码显示调用System.gc()。一般很少手动调用,测试垃圾回收时,注意调用的时机,根据对象的生命周期,比如方法的局部对象,不要在方法内部最后一行调用,而应该在方法调用完之后手动gc。
GC日志
了解GC日志可以更好的排查一些线上问题,如OOM、应用停顿时长等等,和调优。不同的GC收集器产生的GC日志会稍有不同,但虚拟机的设计者为了方便用户阅读,日志格式大致相同:
1 | <datestamp>:[GC[<collector>:<start occupancy1>-><end occupancy1>(total size1),<pause time1> secs]<start occupancy2>-><end occupancy2>(total size2),<pause time2> secs] [Times:<user time> <system time>, <real time>] |
- datestamp : 表示GC日志产生的时间点,如果指定的jvm参数是-XX:+PrintGCTimeStamps,那么输出的是相对于虚拟机启动时间的时间戳,如果指定的是-XX:+PrintGCDateStamps,那么输出的是具体的时间格式,可读性更高
- GC : 表示发生GC的类型,有GC(代表MinorGC)和FullGC(显式调用System.gc())两种情况
- collector : 表示GC收集器类型,取值可能是DefNew、ParNew、PSYoungGen、Tenured、ParOldGen、PSPermGen等等
- start occupancy1 : 表示发生回收之前占用的内存空间
- end occupancy1 : 表示发生回收以后还占用的内存空间
- total size1 : 该堆区域所拥有的总内存空间
- pause time1 : 发生垃圾收集的时间
- start occupancy2 : 表示回收前Java堆内存总占用空间
- end occupancy2 : 表示回收后Java堆内存还占用的总空间
- total size2 : 表示Java堆内存总空间
- pause time2 : 表示整个堆回收消耗时间
- Times :user=0.00 sys=0.00, real=0.01 secs
一个段完整的GC日志例子
1 | -XX:+UseParallelGC -XX:+UseParallelOldGC//使用的垃圾收集器 |
JVM参数使用
堆内存相关
- -Xms与-Xmx:最小堆内存和最大堆内存。过小的堆内存可能造成OOM异常,一般情况下为了避免调整堆内存造成的性能问题,会将2个值设成一样大小。eg:-Xms1024m -Xmx1024m
- -Xmn:堆内存新生代容量,以此达到控制老年代的作用。eg.-Xmn256m
- -XX:PermSize与-XX:MaxPermSize:永久代容量和最大容量。eg.-XX:PermSize=64m -XX:MaxPerSize。移除永久代后已弃用。
- -XX:MetaspaceSize与-XX:MaxMetaspaceSize:元数据参数,替代永久代参数。
- -XX:SurvivorRatio:eg.-XX:SurvivorRatio=8,表示Eden和Survivor的大小比例是8:1:1
垃圾收集器相关
指定垃圾收集器组合
- -XX:+UserSerialGC
- -XX:+UseParNewGc
- -XX:+UseConcMarkSweepGC
- -XX:+UseParallelGC
- -XX:+UseParalleOldGC
其他触发类参数
- -XX:PretenureSizeThreshold:设置直接晋升到老年代的对象大小,大于这个参数的对象将直接在老年代分配,而不是在新生代分配。注意这个值只能设置为字节,如-XX:PretenureSizeThreshold=3145728表示超过3M的对象将直接在老年代分配。
- -XX:MaxTenuringThreshold:设置晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个值时就进入老年代。默认设置为-XX:MaxTenuringThreshold=15。
- -XX:ParellelGCThreads:设置并行GC时进行内存回收的线程数。只有当采用的垃圾回收器是采用多线程模式,包括ParNew、Parallel Scavenge、Parallel Old、CMS,这个参数的设置才会有效。
- -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少(百分比)后触发垃圾收集。默认设置-XX:CMSInitiatingOccupancyFraction=68表示老年代空间使用比例达到68%时触发CMS垃圾收集。仅当老年代收集器设置为CMS时候这个参数才有效。
- -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
- -XX:CMSFullGCsBeforeCompaction:设置CMS收集器在进行多少次垃圾收集后再进行一次内存碎片整理。如设置-XX:CMSFullGCsBeforeCompaction=2表示CMS收集器进行了2次垃圾收集之后,进行一次内存碎片整理。仅当老年代收集器设置为CMS时候这个参数才有效。
GC日志相关
- -XX:+PrintGCDetails : 表示输出GC的详细情况
- -XX:+PrintGCDateStamps : 指定输出GC时的时间格式,比-XX:+PrintGCTimeStamps可读性更高
- -Xloggc : 指定gc日志的存放位置。如-Xloggc:/var/log/myapp-gc.log表示将gc日志保存在磁盘/var/log/目录,文件名为myapp-gc.log。
垃圾回收扩展知识
垃圾收集器
新生代垃圾收集器:Serial收集器,ParNew收集器,Parallel Scavenge收集器。
老年代垃圾收集器:Serial Old收集器,Parallel Old收集器,CMS收集器。
新生代,老年代通用的G1收集器。
注意:CMS收集器如果发生concurrent mode failure或promotion failed,都可能会触发Full GC。