本文共 8834 字,大约阅读时间需要 29 分钟。
JVM 学习总结
PC寄存器 - 每个线程拥有一个PC寄存器 - 在线程创建时被创建 - 指定下一条指令的地址 - 执行本地方法时,PC的值是undefined 方法区 - 保存装载的类信息 - 类型的常量池 - 字段,方法信息 - 方法字节码 【注意】: JDK6:String等常量信息存在于方法区 JDK7:String等常量信息存在于堆内存 - 通常和永久区(Perm)关联在一起(永久区通常保存一些相对稳定相对静止的信息;通常是由Java虚拟机维护) Java堆内存 - 和程序开发密切相关 - 应用系统对象都保存在Java堆中(new Object) - 所有线程共享Java堆 - 对分代GC来说,堆也是分代的 - GC的主要工作区间 【注意】:堆内存每次分配后需要手动清理空间 Java栈内存(数据结构:先进后出) - 线程私有 - 栈由一系列帧组成(因此Java栈也叫做帧栈) - 帧保存一个方法的局部变量,操作数栈,常量池指针 - 每一次方法调用创建一个帧,并压栈 【注意】:栈内存每次分配后无需手动清理空间,函数调用完成自动清理 Java栈 - 局部变量表 【包含参数和局部变量】 Java栈 - 栈上分配 - 一般小对象(几十个bytes),在没有逃逸(线程共用的对象)的情况下,可以直接分配在栈上 - 一般一个栈的空间大概是几百KB到1M左右(尽量分配给小对象) - 直接分配在栈上,可以自动回收,减轻GC压力 - 大对象或者逃逸对象无法栈上分配 堆,栈,方法区交互 - 局部变量实例化后是存入堆中,栈中会存放该对象的引用 - 类本身的信息,方法的字节码是在方法区当中存放 内存模型 - 每一个线程有一个工作内存和主存独立 - 工作内存存放主存中变量的值的拷贝 当数据从主内存复制到工作内存时,必须出现两个动作 第一:由主内存执行的读(read)操作 第二:由工作内存执行的相应的load操作 当数据从工作内存拷贝到主内存时,也出现两个操作 第一:由工作内存执行的存储(store)操作 第二:由主内存执行的相应的写(write)操作 每一个操作都是原子的,即执行期间不会被中断 对于普通变量,一个线程中更新的值,不能麻烦反应在其他变量中 如果需要在其他线程中立即可见,需要使用volatile关键字 可见性: 一个线程修改了变量,其他线程可以立即知道 保证可见性的方法 - 使用volatile关键字 - synchronized(unlock之前,写变量值回主存) - final(一旦初始化完成,其他线程就可见) 有序性: - 在本线程内,操作都是有序的 - 在多线程的情况下,操作都是无序的(指令重排或主内存同步延时) 指令重排: - 线程内串行语义 - 写后读 a=1;b=a; 写一个变量之后,再读这个位置。 - 写后写 a=1;a=2; 写一个变量之后,再写这个变量。 - 读后写 a=b;b=1; 读一个变量之后,再写这个变量。 - 以上语句【不可】重排 - 编译器不考虑多线程间的语义 - 可重排:a=1;b=2; 指令重排的基本原则 - 程序顺序原则:一个线程内保证语义的串行性 - volatile规则:volatile先写,后读 - 锁规则:解锁(unlock)必然发生在随后的加锁(lock)之前 - 传递性:指令A先于指令B,指令B先于指令C,那么指令A必然先于指令C - 线程的start方法限于它的每一个动作 - 线程的所有操作先于线程的中介(Thread.join()) - 线程的中断(interrupt())先于被中断的线程的代码 - 对象的构造函数执行结束先于finalize()方法 解释运行 - 解释执行以解释方式运行字节码 - 解释执行的意思:读一句执行一句 编译执行(JIT: Just In Time) - 将字节码编译成机器码 - 直接执行机器码 - 运行时编译 - 编译后性能有数量级的提升 【注意】:解释运行和编译运行之间的性能差10倍左右 JVM常用参数配置 - Trace跟踪参数 1. -verbose:gc 2. -XX:+PrintGC(发生GC时则会打印相关的GC信息) 示例:- [GC 4790k->374k(15872k), 0.0001606 secs] 解释:GC回收前是占用4790k的空间,回收之后只占用374k,总大小为15872k 3. -XX:+PrintGCDetails(打印GC详细信息) 示例1:- [GC[DefNew: 4416k->0k(4928k),0.0001897 secs] 4790k->374k(15872k),0.0002232 secs] [Times: user=0.00 sys=0.00 real=0.00 secs] 示例2:-XX:+PrintGCDetails在程序执行结束后打印的堆的信息 - Heap - def new generation total 13824k used 11223k [0x27e80000,0x28d80000,0x28d80000] e.x: 新生代共有13M可用,已用11M [低边界,当前边界(所使用,所分配到的位置),最大边界] 计算:(0x28d80000-0x27e80000)/1024/1024=15M 也就是说新生代被分配了15M 正好就是EdenSpace(12288)+FromSpace(1536)+ToSpace(1536)=15M EdenSpace(12288)+FromSpace(1536)=NewGeneration(13824) - eden space 12288k 91% used [0x27e8000,0x28975f20,0x28a80000] e.x: 伊甸园公有12M可用,已用91% - from space 1536k 0% used [0x28a80000,0x28a80000,0x28c00000] e.x: 幸存代 - to space 1536k 0% used [0x28c00000,0x28c00000,0x28d80000] e.x: 幸存代 - tenured generation total 5120k used 0k [0x28d80000,0x29280000,0x34680000] e.x: 老年代共有5M可用,已用0k - the space 5120k 0% used [0x28d80000,0x28d80000,0x28d80200,0x29280000] - compacting perm gen total 12288k used 142k [0x34680000,0x35280000,0x38680000] e.x: 永久区共有12M可用,已用142k - the space 12288k 1% used [0x34680000,0x346a3a90,0x346a3c00,0x35280000] - ro space 10240k 44% used [0x38680000,0x38af73f0,0x38af7400,0x39080000] e.x: 只读共享区 - rw space 12288k 52% used [0x3980000,0x396cdd28,0x396cde00,0x39c80000] e.x: 可读可写区 4. -XX:+PrintGCTimeStamps(打印GC发生的时间戳) 5. -Xloggc:log/gc.log(重定向GC信息到日志文件) 6. -XX:+PrintHeapAtGC(每一次GC前后,都打印【堆】信息) 7. -XX:+TraceClassLoading(监控系统中每个类的加载) 8. -XX:+PrintClassHistogram(按下Ctrl+Break后,打印类的信息) 示例: num #instances #bytes class name 序号 实例数量 总大小 类型 - 堆的分配参数 1. -Xmx -Xms 指定最大堆内存和最小堆内存 示例:-Xmx20m -Xms5m 使用:Xmx = Runtime.getRuntime().maxMemory()/1024/1024 FreeMemory = Runtime.getRuntime().freeMemory()/1024/1024 TotalMemory = Runtime.getRuntime().totalMemory()/1024/1024 结果: 堆内存初始值 Xmx=18.0M FreeMemory=6.200019836425781M TotalMemory=7.0M 分配了1M空间给byte数组 Xmx=18.0M FreeMemory=5.200004577636719M TotalMemory=7.0M 分配了4M空间给数组 Xmx=18.0M FreeMemory=5.699989318847656M TotalMemory=11.5M 回收内存 Xmx=18.0M FreeMemory=6.994834899902344M TotalMemory=11.5M 观察Xmx、FreeMemory、TotalMemory变化情况 2. -Xmn 设置新生代大小(EdenSpace+FromSpace+ToSpace的总和) 3. -XX:NewRatio 新生代(eden + 2*s)和老年代(不包含永久区)的比值 4表示 新生代:老年代=1:4 即:年轻代占堆内存的1/5 4. -XX:SurvivorRatio 设置两个Survivor区和Eden的占比 8表示 两个Servivor:Eden = 2:8 即:一个Servivor占年轻代的1/10 【注意】:理论情况下,发生GC的次数越多,对系统性能的损耗越大 Eden区调大一点,幸存代调小一点,有利于减少GC次数 5. -XX:+HeapDumpOnOutOfMemoryError OOM(Out Of Memory)时导出堆到文件 6. -XX:HeapDumpPath 导出OOm的路径 示例:-Xmx20M -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump 【总结】: - 根据实际情况调整新生代和幸存代的大小 - 官方推荐新生代占堆内存的3/8 - 幸存代占新生代的1/10 - 在OOM时,记得Dump出堆信息,确保可以排查现场问题 7. -XX:PermSize -XX:MaxPermSize 设置永久区的初始空间和最大空间 他们表示一个系统可以容纳多少个类型(一般系统几十兆或者几百兆就够用了) - 栈的分配参数 -Xss - 通常只有几百KB - 决定了函数调用的深度 - 每个线程都有独立的栈空间 - 局部变量、参数分配在栈上 【注意】:若想让系统多跑一些线程,应该把栈空间尽量减少,而不是增大 若系统中有“递归”调用,栈空间不宜太小,若太小,可能会造成OOM 若出现java.lang.StackOverflowError,说明函数调用的深度太深,需要调把栈空间调大一点 若想让函数尽可能被多调用的方式:减少局部变量(栈帧里面的局部变量表包含参数和函数当中的局部变量) 若能减少局部变量的数量,就能够减少每次函数调用所消耗的空间,这样能够让函数多被调用几次 【GC算法】 标记-清除算法 标记-清除算法是现代垃圾回收算法的思想基础。 标记-清除算法是将垃圾回收分为两个阶段 1.标记阶段 2.清除节点 一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。 因此未被标记的对象就是未被引用的垃圾对象。然后,在清除节点,清除所有未被标记的对象。 标记-压缩算法 标记-压缩算法适合用于存活对象较多的场合,如老年代。 它在标记-清除算法的基础上做了一些优化。 和标记-清除算法一样,标记-压缩算法首先需要从根节点开始,对所有可达对象做一次标记 之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。 最后清理边界外所有的空间。 【注意】:标记-清除算法执行后,内存可能会产生一些碎片 标记-压缩算法执行后,内存空间是连续的,没有碎片 复制算法(对内存空间有一定的浪费) 与标记-清除算法相比,复制算法是一种相对高效的回收方法 不适用于存活对象较多的场合 如老年代 将原有的内存空间分为两块(两块大小完全相同),每次只使用其中一跨,在垃圾回收时 将正在使用的内存中的存活对象复制到未使用的内存块中 之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收 分代思想 依据对象的从怒火周期进行分类,短命对象归为新生代,长命对象归为老年代 根据不通代的特点,选取合适的收集算法 - 少量对象存活,适合复制算法 - 大量对象存活,适合标记-清理或者标记-压缩算法 GC算法总结 引用计数(没有被Java采用)【缺点】:没有办法处理循环引用 标记-清除 老年代使用 标记-压缩 老年代使用 复制算法 新生代使用 对象产生在Eden区,使用标记-清除算法 From区和To区空间大小一样,使用复制算法 垃圾对象 可触及性 - 从根节点可以触及到这个对象 什么是根节点 - 节点可以认为是栈中引用的对象 - 方法区中静态成员或者常量引用的对象(全局对象) - JNI方法栈中引用的对象 可复活的 - 一旦所有引用被释放,就是可复活状态 - 因为在finalize()中可能复活该对象 不可触及的 - 在finalize()后,可能会进入不可触及状态 - 不可触及的对象不可能复活 - 可以回收 串行回收器 - 最古老,最稳定 - 效率高 - 可能会产生较长的停顿 - 在多核计算机中可能发挥不了最大的作用(原因:单线程) - -XX:+UseSerialGC - 新生代,老年代使用串行回收器 - 新生代复制算法 - 老年代标记-压缩 并行收集器ParNew(同样会产生Stop-The-World,即:停止用户应用程序的线程) - -XX:+UserParNewGC(New:代表新生代) - 新生代并行 - 老年代串行 - Serial收集器新生代的并行版本 - 复制算法 - 多线程,需要多核支持 - -XX:ParallelGCThreads 限制线程数量 并行收集器Parallel(同样会产生Stop-The-World,即:停止用户应用程序的线程) - 类似ParNew - 新生代复制算法 - 老年代标记-压缩 - 更加关注吞吐量 - -XX:+UseParallelGC 使用Parallel收集器(新生代并行+老年代串行) -XX:+UserParallelOldGC 使用Parallel收集器(新生代并行+老年代并行) -XX:MaxGCPauseMills - 最大停顿时间,单位毫秒 - GC尽力保证回收时间不超过设定值 -XX:GCTimeRatio - 0-100的取值范围 - 垃圾收集时间占总时间的比值 - 默认99,即:最大允许1%时间做GC 【注意】:以上两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优 CMS收集器(和应用程序的线程一起执行) - Concurrent Mark Sweep 并发标记-清除 - 标记-清除算法 - 与标记-压缩算法相比 - 并发节点会降低吞吐量 - 单纯老年代收集器(新生代使用ParNew) - -XX:+UseConcMarkSweepGC CMS运行过程比较复杂,着重实现了标记的过程,可分为 - 初始标记(会产生全局停顿) 1.根可以直接关联到的对象 2.速度快 - 并发标记(和用户应用程序一起) 主要标记过程,标记全部对象 - 重新标记(会产生全局停顿) 1.由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正 - 并发清除(和用户应用程序一起) 1.采用标记-清除算法 原因:因为CMS在执行时,是和应用程序并发执行, 而因为标记-压缩算法是需要将存活的对象移动到堆内存的一端 此时,应用程序有可能找不到存活的对象 而标记-清除算法只是清除不可达的对象,没有对他们进行移动 2.基于标记结果,直接清理对象 CMS的特点 - 尽可能降低停顿 - 会影响系统整体吞吐量和性能 比如:用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半 - 清理不彻底 因为在清理节点,用户线程还在运行,会产生新的垃圾,无法清理 - 因为和用户线程一起运行,不能在空间快满时再清理 - -XX:CMSInitiatingOccupancyFraction设置出发GC的阈值 - 如果不幸内存预留空间不足,就会引起Concurrent mode failure这个错误!!! 【经验】:若遇到Concurrent mode failure错误,使用串行收集器作为后备 因此,应用程序可能会产生一段时间的停顿 - -XX:+UseCMSCompactAtFullCollection Full GC之后,进行一次整理 整理过程是独占的(不与其他线程并发),会引起停顿时间变长 - -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC之后,进行一次碎片整理 - -XX:ParallelCMSThreads 设置CMS的线程数量(一般情况下,约等于CPU的可用线程数量,不宜设置的太大) 【注意】:为了减轻GC压力,我们需要注意些什么? 1.软件如何设计架构 2.代码如何写 3.堆空间如何分配 总结GC参数 1. -XX:+UseSerialGC 在新生代和老年代使用串行收集器 2. -XX:SurvivorRatio 设置Eden区和Survivor区大小的比例 3. -XX:NewRatio 新生代和老年代的比例 4. -XX:+UseParNewGC 在新生代使用并行收集器 5. -XX:+UseParallelGC 新生代使用并行回收收集器 6. -XX:+UseParallelOldGC 老年代使用并行回收收集器 7. -XX:ParallelGCThreads=4 设置用于垃圾回收的线程数 8. -XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器 9. -XX:ParallelCMSThreads 设置CMS的线程数量 10. -XX:CMSInitiatingOccupancyFraction=50 设置CMS收集器在老年代空间被使用多少后触发 11. -XX:+UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的处理 12. -XX:CMSFullGCsBeforeCompaction=2 设置进行多少次CMS垃圾回收后,进行一次内存压缩 13. -XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收 14. -XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到此值设置的百分比时,启动CMS回收 15. -XX:UseCMSInitiationOccupancyOnly 表示只在到达阈值的时候,才进行CMS回收 16. -XX:+DisableExplicitGC 表示禁用代码中System.gc() 17. -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。 对于年老代比较多的应用,可以提高效率。 如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制, 这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。 18. -XX:+PrintTenuringDistribution 打印新生代年龄分布 19. -XX:-ReduceInitialCardMarks 解决放入大对象导致JVM Crash 20. -XX:+CMSParallelRemarkEnabled 并行标记 21. -XX:+UseCompressedOops JVM压缩普通对象指针 jvisualVM监控配置参数: -Dcom.sun.management.jmxremote=true (开启jvisualVM监控) -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx (主机名) -Dcom.sun.management.jmxremote.port=xxxxx (端口) -Dcom.sun.management.jmxremote.ssl=false (是否使用ssl)-Dcom.sun.management.jmxremote.authenticate=false (是否需要用户名密码进行验证)
并且在jdk中需要修改java.policy, 在最后一行追加permission java.security.AllPermission;
并启动jdk中的jstatd这个服务。就可以使用jVisualVM监控JVM的各项参数变化了。
欢迎访问我的个人Github
转载地址:http://ekjob.baihongyu.com/