最近处理客户高并发应用的时候,经常会遇到GC的问题,于是是时候把原理学习过的JVM和GC的内容拿出来温习一下了。
一些准备
-XX:+
-XX:-
-XX:
-Xmx:HeapDumpPath=./dump.core堆内存快照存储区
JVM标准结构
类的加载机制
一:装载(load)
由ClassLoader负责加载;
(ClassNotFoundException)
二:链接(Link)
校验(verify)、准备(Prepare)、初始化静态变量赋默认值;
(NoClassDefFoundError)
三:初始化
执行静态初始化代码、赋值静态变量。
ClassLoader
一:BootStrap ClassLoader
由Sun用C++实现此类,JDK启动时负责加载jre/lib/rt.jar里的所有
class,及java规范中定义的接口及实现;
二:Extension ClassLoader
JVM用于加载扩展功能的jar包,如DNS.jar
Jdk中的名称:sun.misc.Launcher$ExtClassLoader
三:System ClassLoader
加载启动参数中指定ClassPath的Jar包
Jdk中的名称: sun.misc.Launcher$AppClassLoader
一般的程序使用的都是AppClassLoader
四:自定义ClassLoader
用户也可以自定ClassLoader用于加载其他路径的Jar如网络上加载
类的执行机制
虽然类已加载成功且静态属性及实例对象皆以创建,但是,执行静态方法或者实例方法时仍需要对JVM字节码进行处理。
JVM有以下三种执行方式:
(1):解释执行
(2):编译执行
(3):反射执行
解释执行
采用经典的冯诺依曼FDX循环方式,即获取下一条指令,解码并分派,然后执行。
解释执行的优点:
简单、占资源少、启动速度快
解释执行的缺点:
效率低
编译执行
JDK将字节码编译成机器码,编译在运行时进行,故称作JIT编译器(Just-in-time)•策略:对执行频率频繁的代码使用编译执行,对执行频率不高的仍采用解释执行
编译执行的两种方式:
(1)ClinetCompiler:又称C1较轻量级,java -client
(2)ServerCompiler: 又称C2 较重量级,java -server
Client
Compiler
占用内存较少,适合于桌面应用•优化方法:
一:方法内联,将调用的方法指令直接植入到当前方法中;
二:去虚拟化,针对只有一个实现类的方法;
三:消除冗余,在编译时进行代码清理;
Server
Compiler
C2采用了大量优化技巧,占内存较多,适合于服务器应用;
优化方法:标量替换、栈上分配、同步削除;
默认当CPU个数超过两个且内存超过2G自动采用Server模式,否则为Client模式,但在32位的windows上始终都是Client模式,可通过java-server强制使用Server模式,或者java-client强制使用client模式。
JVM内存管理
JVM内存结构图:
JVM方法区
用于存放类信息、类的属性、方法等信息。
又称为持久代PermanentGeneration,默认最小值16MB,最大值64MB。持久代在一定条件下也会被GC(垃圾回收),当空间不够时,会抛出OutOfMemory错误信息。
可通过-XX:PermSize -XX:MaxPermSize来指定最小最大值。
PC寄存器与方法栈
每个线程均会创建自己的PC寄存器和方法栈。
PC寄存器存放每条指令的地址。
方法栈为线程私有,当方法执行完毕时,其栈帧所用内存会自动被回收。
当栈空间不足时会抛出StackOverflowError,可通过设置-Xss来指定其大小,以避免空间不够用。
JVM堆
堆用于存储对象实例和数组值。
在32位机上最大2GB,在64位机则无限制
可通过-Xms设置最小值,默认为物理内存的1/64但小于1GG
通过-Xmx设置最大值,默认为物理内存的1/4但小于1GB
默认当空余堆内存小于40%时,JVM会增大Heap到最大值,可通过-XX:MinHeapFreeRatio=来设定这个比例。
当空余空间大于70%时,JVM会减小到最小值,可通过-XX:MaxHeapFreeRatio=来设定这个比例。
为避免运行时JVM频繁调整Heap大小,通常将-Xms与-Xmx设成相同值。
JVM堆结构
新生代 new generation
旧生代 Old Generation
-
用于存放在新生代中多次回收仍然存活的对象。
-
有两种情况新建的对象会直接在老生代分配:
-
旧生代的大小为-Xmx减去-Xmn的值。
内存回收-GC
-
所谓内存回收就是我们所熟知的GC(Garbage Collection)垃圾回收。
-
JVM通过GC来回收堆和方法区的内存,GC的原理为首先找到内存中不再被引用的对象,然后回收。
-
通常采用收集器的方式来实现GC,主要的收集器有引用计数收集器和跟踪收集器。
引用计数收集器
引用计数收集器通过记录对象的引用次数,当次数为零时,可进行回收。
但当出现循环引用时该收集方法则无法回收:
所以引用计数方式不适合面向对象这种有复杂引用关系的语言,SunJDK在实现GC时也未使用过此方式。
跟踪收集器
-
跟踪收集器采用集中式的管理方式,全局记录数据的引用状态。基于一定的条件触发例如:空间不足,定时。
-
执行时需要从根对象来扫描对象间的引用关系,这会造成应用的暂停。
-
主要实现算法有:赋值(Copying)、标记-清除(Mark-Sweep)、标记-压缩(Mark-Compact)。
复制算法-Copying
复制算法采用的方式为从根集合扫描出存活的对象,并将存活的对象复制到一块新的完全未使用的空间。
当存活对象少时,Copying算法是很高效的,其代价是需要一块新的存储区和进行对象移动。
标记-清除 Mark-Sweep
标记-压缩 Mark-Compact
新生代可用GC
串行GC(Serial GC)
-
串行GC从根集合扫描存活的对象。JVM认为根对象为当前线程栈上引用的对象、常量、静态变量、传到本地方法还未被释放的引用。
-
串行GC扫描存活的对象,然后将存活的对象复制到S0或S1中(S0与S1同时只能有一个被使用,另一个为空)。
-
为了避免扫描过程中引用关系的改变,JDK采用了暂停应用的方式。
-
通常只有经过几次MinorGC仍存活的对象才放入旧生代中。该次数可通过-XX:MaxTenuringThreshold设置(只在串行与ParNew方式下生效默认值为15)。但该项并不是唯一的规则。串行和ParNew在每次GC后计算可存活的次数,规则为累计每个age对象占用的内存,如果累计超过SurvivorSpace的一半,则以age为准,否则,以MaxTenuringThreshold为准。
-
如果ToSpace空间满则直接转入旧生代。
SerialGC采用单线程方式,适用于单CPU,对暂停时间要求不高的应用,也是Client级别(CPU小于2个或物理内存小于2GB,或32位Windows机器上)的GC方式,可通过-XX:+UseSerialGC来强制执行。