-
**JVM**(Java Virtual Machine的简称),意为Java虚拟机虚拟机:指通过软件模拟的具有完整硬件功能的,运行在一个完全隔离的环境中的完整的计算机系统,常见的虚拟机有:JVM, VMwave, Virtual Box。JVM和其他两个虚拟机的区别:VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪JVM是一台被定制过的现实当中不存在的计算机。
-
作者:宋尧飞编者按:笔者遇到一个非常典型的问题,应用在 X86 正常运行,在 AArch64 上 JVM 就会崩溃。这个典型的 JVM 内部问题。笔者通过分析最终定位到是由于 JVM 中模板解释器代码存在 bug 导致在弱内存模型的平台上 Crash。在分析过程中,涉及到非常多的 JVM 内部知识,比如对象头、GC 复制算法操作、CAS 操作、字节码执行、内存序等,希望对读者有所帮助。本文介绍了一般分析 JVM crash 的方法,并且深入介绍了为什么在 aarch64 平台上引起这样的问题,最后还给出了修改方法并推送到上游社区中。**对于使用非毕昇 JDK 的其他 JDK 只有在 jdk8u292、jdk11.0.9、jdk15以后的版本才得到修复,读者使用时需要注意版本选择避免这类问题发生。背景知识java 程序在发生 crash 时,会生成 hs_err_pid.log 文件,以及 core 文件(需要操作系统开启相关设置),其中 hs_err 文件以文本格式记录了 crash 发生位置的小范围精确现场信息(调用栈、寄存器、线程栈、致命信号、指令上下文等)、jvm 各组件状态信息(java 堆、jit 事件、gc 事件)、系统层面信息(环境变量、入参、内存使用信息、系统版本)等,精简记录了关键信息。而 core 文件是程序崩溃时进程的二进制快照,完整记录了崩溃现场信息,可以使用 gdb 工具来打开 core 文件,恢复出一个崩溃现场,方便分析。约束文中描述的问题适用于 jdk8u292 之前的版本。现象某业务线隔十天半个月总会报过来 crash 问题,crash 位置比较统一,都是在某处执行 young gc 的上下文中,crash 的直接原因是 java 对象的头被写坏了,比如这样:而正常的对象头由 markoop 和 metadata 两部分组成,前者存放该对象的 hash 值、年龄、锁信息等,后者存放该对象所属的 Klass 指针。这里关注的是 markoop,64 位机器上它的具体布局如下:每种布局中每个字段的详细含义可以在 jdk 源码 jdk8u/hotspot/src/share/vm/oops/markOop.hpp 中找到,这里简单给出结论就是 gc 阶段一个正常对象头中的 markoop 不可能是全 0,而是比如这样:此外,crash 时间上也有个特点:基本每次都发生在程序刚启动时的几秒内。分析发生 crash 的 java 对象有个一致的特点,就是总位于 eden 区,我们仔细分析了 crash 位置的 gc 过程逻辑,特别是会在 gc 期间修改对象头的相关源码更是重点关注对象,因为那块代码为了追求性能,使用了无锁编程:补充介绍一下 CAS(Compare And Swap),CAS 的完整意思是比较并替换,并且确保整个操作原子性。CAS 需要 3 个操作数:内存地址 dst,比较值 cmp,要更新的目标值 value。当且仅当内存地址 dst 上的值跟比较值 cmp 相等时,将内存地址 dst 上的值改写为 value,否则就什么都不做,其在 aarch64 上的汇编实现类似如下:然而我们经过反复推敲,这块 gc 逻辑似乎无懈可击,而且位于 eden 区也意味着没有被 gc 搬移过的可能性,这个问题在很长时间里陷入了停滞……直到某一天又收到了一个类似的 crash,这个问题才迎来了转机。在这个 crash 里,也是 java 对象的头被写坏了,但特殊的地方在于,头上的错误值是 0x2000,凭着职业敏感,我们猜测这个特殊的错误值是否来自这个 java 对象本身呢?这个对象的 Java 名字叫 DynamicByteBuffer,来自某个基础组件。反编译得到了问题类 DynamicByteBuffer 的代码:再结合 core 信息中其他正常 DynamicByteBuffer 对象的布局,确定了这个特殊的 0x2000 值原本应该位于 segmentSize 字段上,而且从代码中注意到这个 segmentSize 字段是 final 属性,意味着其值只可能在实例构造函数中被设置,使用 jdk 自带的命令 javap 进行反汇编,得到对应的字节码如下:putfield 这条字节码的作用是给 java 对象的一个字段赋值,在红框中的语义就是给 DynamicByteBuffer 对象的 segmentSize 字段赋值。分析到这里,我们做一下小结,crash 的第一现场并非在 gc 上下文中,而是得往前追溯,发生在这个 java 对象被初始化期间,这期间在初始化它的 segmentSize 字段时,因为某种原因,0x2000 被写到了对象头上。接下来继续分析, JDK 在发生 crash 时会自动生成的 hs_err 日志,其中有记录最近发生的编译事件 “Compilation events (250 events)”,从中没有发现 DynamicByteBuffer 构造函数相关的编译事件,所以可以推断 crash 时 DynamicByteBuffer 这个类的构造函数尚未被编译过(由于 crash 发生在程序启动那几秒,JIT 往往需要预热后才会介入,所以可以假设记录的比较完整),这意味着,它的构造函数只会通过模板解释器去执行,更具体地说,是去执行模板解释器中的 putfield 指令来把 0x2000 写到 segmentSize 字段位置。具体怎么写其实很简单,就是先拿到 segmentSize 字段的偏移量,根据偏移量定位到写的位置,然后写入。然而 JVM 的模板解释器在实现这个 putfield 指令时,额外增加了一条快速实现路径,在 runtime 期间会自动(具体的时间点是 “完整” 执行完第一次 putfield 指令后)从慢速路径切到快速路径上,这个切换操作的实现全程没有加锁,同步完全依赖 barrier,由于整个过程比较复杂,这里首先给一个比较容易理解的并行流程图:注:图中 bcp 指的是 bytecode pointer,就是读字节码。上图表示接近同一时间点前后,两条并行流分别构建一个 DynamicByteBuffer 类型的对象过程中,各自完成 segmentSize 字段赋值的过程,用 Java 代码简单示意如下:其中第一条执行流走的慢速路径,第二条走的快速路径,可以留意到,红色标识的是几次公共内存的访存操作,barrier 就分布在这些位置前后(标在下图中)。接下来再给一个更加精确一点的指令流模型:简单介绍一下这个设计模型:线程从记录了指令的内存地址 bcp(bytecode pointer) 上取出指令,然后跳转到该指令地址上执行,当取出的指令是 bcp1(比如 putfeild 指令的慢速路径)时就是图中左边的指令流;左边的指令流就是计算出字段的 offset 并 str 到指定内存地址,然后插入 barrier,最后将 bcp2 指令(比如 putfeild 指令的快速路径)覆写到步骤 1 中的内存地址 addr 上;后续线程继续执行步骤 1 时,由于取出的指令变成了 bcp2,就改为跳转到图中右边的指令流;右边的指令流就是直接取出步骤 2 中已经存到指定内存地址中的 offset。回顾整个设计模型,左边的指令流通过一个等效于完整 dmb 的 barrier 来保证 str offset 和 str bcp2 这两条 str 指令的执行顺序并且全局可见;而右边的指令流中,ldr bcp 和 ldr offset 这两条 ldr 指令之间没有任何 barrier,设计者可能认为一个无条件跳转指令可以为两条 ldr 指令建立依赖,从而保证执行顺序,然而从实测结果来看是不成立的。这里先来简单补充介绍一下内存顺序模型的概念,现代 CPU 为了提高执行效率,在指令的执行顺序上拥有很大的自主权,对每个独立的 CPU 来说,只要确保语义不变,实际如何执行都有可能,这种方式对于单个 CPU 来说没有问题,当放到多个 CPU 共享数据的时候,这种乱序执行的行为就会引发每个 CPU 看到数据的顺序不一致问题,导致跨 CPU 的程序逻辑乱套了。这就需要对读、写内存指令进行约束,来规范每个 CPU 看到的内存生效行为,由此提出了内存顺序模型的概念:其中 ARM 采用的是一种弱内存模型,这种模型默认对读、写指令没有任何约束,需要由程序员自己通过插入 barrier 来手动保证。再回到这个问题上,测试方式是在 ldr offset 指令后额外加了检测指令:就是检查 offset 值是否为 0,如果为 0 则直接强制 crash(设计上保证了 java 对象的任何实例字段的 offset 不可能是 0)。经过长时间测试,程序果然在这个位置触发了 crash!这说明上面提到的两条 ldr 指令不存在依赖关系,或者说这种依赖关系类似 ARMv8 手册中描述的条件依赖,并不能保证执行顺序。ldr offset 指令先于 ldr bcp 执行,使得读到一个非法的 offset 值 0。更说明了,这才是这个案例的第一案发现场!找到了问题的根因后,解决方法也就顺利出炉了,那就是在两条 ldr 指令之间插入 barrier 来确保这两条 ldr 指令不发生乱序。实测证明,这种修复方案非常有效,这类 crash 现象消失。详细的修复 patch 见 https://hg.openjdk.java.net/jdk/jdk/rev/b9529fcbbd33。目前已经 backport 到 jdk8u292、jdk11.0.9、jdk15。总结Java 虚拟机 (JVM) 为了追求性能,大量使用了无锁编程进行设计,而且这么多年以来 JDK(特别是 JDK8)主要都是面向 X86 平台开发的,如今才慢慢的开始支持 aarch64 平台,所以 aarch64 弱内存序问题是我们面临的一个比较严峻的挑战。后记如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 官网查找相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。原文转载自 openEuler-一个 JVM 解释器 bug 在 AArch64 平台导致应用崩溃的问题分析
-
作者:宋尧飞编者按:笔者在AArch64中遇到一个G1 GC挂起,CPU利用率高达300%的案例。经过分析发现问题是由JVM的锁机制导致,该问题根因是并发编程中没有正确理解内存序导致。本文着重介绍JVM中Monitor的基本原理,同时演示了在什么情况下会触发该问题。希望通过本文的分析,读者能够了解到内存序对性能、正确性的影响,在并发编程时更加仔细。现象本案例是一个典型的弱内存模型案例,大致的现象就是AArch64平台上,业务挂死,而进程占用CPU持续维持在300%。配合top和gdb,可以看到是3个GC线程在offer_termination处陷入了死循环: 多个并行GC线程在Minor GC结束时调用offer_termination,在offer_termination中自旋等待其他并行GC线程到达该位置,才说明GC任务完成,可以终止。(关于并行任务的中止协议问题,可以参考相关论文,这里不做着重介绍。简单地说,在并行任务执行时,多个任务之间可能存在任务不均衡,所以JVM内部设计了任务均衡机制,同时必须设计任务终止的机制来保证多个任务都能完成,这里的offer_termination就是尝试终止任务)。在该案例中,部分GC线程完成自己的任务,等待其他的GC线程。此时出现挂起,很有可能是因为发生了死锁。所以问题很可能是由于那些尚未完成任务的GC线程上错误地使用锁。所以使用gdb观察了一下其他GC线程,发现其他GC线程全都阻塞在一把JVM的锁上: 而这把Monitor中的情况如下:cxq上积累了大量GC线程OnDeck记录的GC线程已经消失_owner记录的锁持有者为NULL分析在进一步分析前,首先普及一下JVM锁组件Monitor的基本原理,Monitor类主要包含4个核心字段:“Thread * volatile _owner;” 字段指向这把锁的持有线程“SplitWord _LockWord;” 字段被设计为1个机器字长,目的是为了确保操作时天然的原子性,它的最低位被设计为上锁标记位,而高位区域用来存放256字节对齐的竞争队列(cxq)地址“ParkEvent * volatile _EntryList;” 字段指向一个等待队列,跟cxq差别不大,个人理解只是为了缓解cxq的竞争压力而设计“ParkEvent * volatile _OnDeck;” 字段指向这把锁的法定继承人,同时最低位还充当了内部锁的角色接下来通过一组流程图来介绍加解锁的具体流程:上图是加锁的一个整体流程,大致分为3步:1. 首先走快速上锁流程,主要对应锁本身无人持有的最理想情况2. 接着是自旋上锁流程,这是预期将在短时间内获取锁的情况3. 最后是慢速上锁流程,申请者将会加入等待队列(cxq),然后进入睡眠,直到被唤醒后发现自己变成了法定继承者,于是进入自旋,直到完成上锁。 而且,基于性能考虑,整个上锁流程中的每一步几乎都做了“插队”的尝试:如上图代码中所示,“插队”的意思就是不经过排队(cxq),直接尝试置上锁标志位。 上图就是整个解锁流程了,显然真正的解锁操作在第二步中就已经完成了(意味着接下来时刻有“插队”现象发生),剩下的主要就是选出继承者的过程,大致分为以下几步:解锁线程首先需要将内部锁(_OnDeck)标记上锁从竞争队列(cxq)抽取所有等待者放入等待队列(_EntryList)_ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,这代表选出了继承者唤醒继承者当然伴随着整个解锁流程每一步的,还有对“插队”行为的处理。至此,JVM锁组件Monitor的原理就介绍到这里,再回归到问题本身,一个疑问就是_OnDeck上记录的继承者为何消失?作为继承者,既然已经消失在竞争队列和等待队列里,显然意味着它大概率已经持有锁、然后解锁走人了,所以问题很可能跟继承者选取过程有关。基于这种猜测,我们对相关代码着重进行了梳理,就发现了下图两处红框标记位置存在疑点,那就是在选继承者过程第3步中:写_ EntryList和写_OnDeck之间没有barrier来保证执行顺序,这可能出现_OnDeck先于_ EntryList写入的情况,一旦继承人提前持有锁,后果就可能非常糟糕… 这里贴了一张可能的问题场景:线程A处于解锁流程中,由于乱序,先写入了继承者同时解除内部锁线程B处于上锁流程,发现自己就是法定继承者后,立刻完成上锁线程B又迅速进入解锁流程,并从_EntryList中取出头元素(也就是线程B!)作为继承者写入_OnDeck,完成解锁走人线程A此时才更新_EntryList,然后唤醒继承者(也就是线程B!),完成解锁走人_OnDeck上的继承者线程B,实际已经完成加解锁离开,后续等待线程再也无法被唤醒正巧在社区的高版本上找到了一个相关的修复记录,这里贴出2个关键的代码片段: 上面这段代码位于慢速上锁流程,被唤醒后检查继承者是否是自己,修复后的代码在读_OnDeck时加了Load-Acquire的barrier。 上面这段代码位于解锁时选继承者流程,从_ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,修复后的代码在写_OnDeck时加了Store-Release的barrier。显然,围绕_OnDeck添加的这对One-way barrier可以确保:当继承者线程被唤醒时,该线程可以“看”到_EntryList已经被及时更新。总结在AArch64这种弱内存模型的平台上(关于内存序更多的知识在接下来的分享中会详细介绍),一旦涉及多线程对公共内存的每一次访问,必须反复确认是否需要通过barrier来严格保序,而且除非存在有效的依赖关系,否则barrier需要在读写端成对使用。后记如果遇到相关技术问题(包括不限于毕昇JDK),可以在论坛求助(目前毕昇JDK最新的官网http://bishengjdk.openeuler.org已经上线,可以进入官网查找所有相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等)。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流群讨论GCC、LLVM、JDK和V8等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复Compiler入群。原文转载自 openEuler-JVM 锁 bug 导致 G1 GC 挂起问题分析和解决
-
首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:旧生代空间不足调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 Pemanet Generation空间不足增大Perm Gen空间,避免太多静态对象 统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间控制好新生代和旧生代的比例 System.gc()被显示调用垃圾回收不要手动触发,尽量依靠JVM自身的机制 调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果1)新生代设置过小一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC2)新生代设置过大一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加一般说来新生代占整个堆1/3比较合适3)Survivor设置过小导致对象从eden直接到达旧生代,降低了在新生代的存活时间4)Survivor设置过大导致eden过小,增加了GC频率另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收JVM提供两种较为简单的GC策略的设置方式1)吞吐量优先JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置2)暂停时间优先JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置 最后汇总一下JVM常见配置堆设置-Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:设置年轻代大小-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数。并行收集线程数。
-
在Java中,一个类的全名(包名+类名)作为其标识,但在JVM中,一个类用其 全名 + 类加载器作为唯一标识,不同类加载器加载的类置于不同的命名空间中,这叫做类加载器隔离。
-
一个类编译完成后,下一步就是开始使用类,怎么使用?类编译完成后,开始使用类,在程序执行中JVM通过装载,链接,初始化这3个步骤完成。装载:由类加载器完成,找到对应的字节码,创建一个Class对象。类加载器首先会检查这个类的Class对象是否已经被加载过,如果没有加载,默认的类加载器就会根据类名查找对应的.class文件。加载器将.class文件的二进制文件装入JVM的方法区,并且在堆区创建描述这个类的java.lang.Class对象,用来封装数据,但是同一个类只会被类装载器装载一次。链接:就是把二进制数据组装为可以运行的状态 校验:一般用来确认此二进制文件是否适合当前的JVM(版本) 准备:为静态成员分配内存空间,并设置默认值。 解析:转换常量池中的代码作为直接引用的过程,直到所有的符号都可以被运行程序使用(建立完整的对应关系)验证类中的字节码,为静态域分配空间。初始化:如果该类有父类,则对其初始化,执行静态初始化器和静态初始化块。
-
这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机https://blog.csdn.net/column/details/21960.htmlJVM介绍和源码首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。JVM内存模型内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。字符串常量池则会存放使用intern的字符串变量。JVM OOM和内存泄漏这里指的是oom和内存泄漏这类错误。oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。常见调试工具命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。class文件结构class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。JVM的类加载机制jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。defineclass findclass和loadclass类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。JVM虚拟机字节码执行引擎jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。编译期优化和运行期优化编译期优化主要有几种1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。3 条件编译,比如if(true)直接可得。运行期优化主要有几种1 JIT即时编译Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。3 数组边界擦除,方法内联,比较偏,意义不大。4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。1JVM的垃圾回收1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc,当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。一般年轻代使用停止复制,老年代使用标记清除。3 垃圾收集器serial串行parallel并行它们都有年轻代与老年代的不同实现。然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。
-
首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:旧生代空间不足调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 Pemanet Generation空间不足增大Perm Gen空间,避免太多静态对象 统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间控制好新生代和旧生代的比例 System.gc()被显示调用垃圾回收不要手动触发,尽量依靠JVM自身的机制 调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果1)新生代设置过小一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC2)新生代设置过大一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加一般说来新生代占整个堆1/3比较合适3)Survivor设置过小导致对象从eden直接到达旧生代,降低了在新生代的存活时间4)Survivor设置过大导致eden过小,增加了GC频率另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收JVM提供两种较为简单的GC策略的设置方式1)吞吐量优先JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置2)暂停时间优先JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置 最后汇总一下JVM常见配置堆设置-Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:设置年轻代大小-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数。并行收集线程数。
-
>>数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。 基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。 “引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。 基本类型包括:byte,boolean(1 byte),short,char(2 bytes),int,float(4 bytes),long,double(8 bytes),returnAddress(不确定是否是基本类型) 引用类型包括:类类型,接口类型和数组。 >>堆与栈 栈是运行时的单位,而堆是存储的单位。 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。 在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。 堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。 堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。 Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。 >>Java中的参数传递时传值呢?还是传引用? 要说明这个问题,先要明确两点: 1. 不要试图与C进行类比,Java中没有指针的概念。 2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。 明确以上两点后。Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说Java是进行传值调用,这点没有问题,而且也简化的C中复杂性。 传值传引用都不够准确,可以理解成传引用变量的副本值。引用变量分为字面值引用变量(即基本数据类型引用变量)和对象引用变量 。 详情需要了解数据类型使用机制和堆栈的概念:http://www.cnblogs.com/alexlo/archive/2013/02/21/2920209.html 对象引用变量:即普通java对象的引用变量 ,如 String a = "abc" , a就是对象引用变量。java 是不能直接操作对象的,只能通过对“对象引用的操作”来操作对象。而对象的引用的表示就是对象变量。可以多个对象引用变量指向同一个对象。 字面值引用变量:即普通数据类型的引用变量 ,如 int b = 1 , b就是字面值引用变量。可以有多个字面值引用变量指向同一字面值,但其中一个引用修改字面值,不会影响另一个引用字面值,这点要与对象引用区别开。 >>Java对象的大小 基本数据的类型的大小是固定的,Java基本数据类型与位运算,这里就不多说了。对于非基本类型的Java对象,其大小就值得商榷。在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句:Object ob = new Object(); 这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte(栈)+8byte(堆)。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。Class NewObject {int count;boolean flag;Object ob;} 其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。 但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。 这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。 因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。 >>引用类型 对象引用类型分为强引用、软引用、弱引用和虚引用。 强引用:就是我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收。 软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。 弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。 强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。 JVM的生命周期一、首先分析两个概念 JVM实例和JVM执行引擎实例 (1)JVM实例对应了一个独立运行的java程序,它是进程级别。 (2)JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别的。二、JVM的生命周期 (1)JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。 (2)JVM实例的运行 main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。 (3)JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
-
程序计数器当前线程所执行的字节码的行号指示器。虚拟机栈Java方法执行的内存模型,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈本地方法执行的内存模型,和虚拟机栈非常相似,其区别是本地方法栈为JVM使用到的Native方法服务。堆用于存储对象实例,是垃圾收集器管理的主要区域。方法区用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
什么是jvm呢? gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作。在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法,便可以进行优化配置垃圾收集器。垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。1.1.引用计数 引用计数存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,jvm必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。1.2.对象引用遍历 早期的jvm使用引用计数,现在大多数jvm采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,gc必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。下一步,gc要删除不可到达的对象。删除时,有些gc只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以生成新的对象,这叫做清除(sweeping)。这种方法的问题在于内存会分成好多小段,而它们不足以用于新的对象,但是组合起来却很大。因此,许多gc可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。 为此,gc需要停止其他的活动活动。这种方法意味着所有与应用程序相关的工作停止,只有gc运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的gc不断增加或同时运行以减少或者清除应用程序的中断。有的gc使用单线程完成这项工作,有的则采用多线程以增加效率。2.几种垃圾回收机制2.1.标记-清除收集器 这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。2.2.标记-压缩收集器 有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。2.3.复制收集器 这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,jvm生成的新对象则放在另一半空间中。gc运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。2.4.增量收集器 增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。2.5.分代收集器 这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。jvm生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。2.6.并发收集器 并发收集器与应用程序同时运行。这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。2.7.并行收集器 并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多cpu机器上使用多线程技术可以显著的提高java应用程序的可扩展性。3.Sun HotSpot 1.4.1 JVM堆大小的调整 Sun HotSpot 1.4.1使用分代收集器,它把堆分为三个主要的域:新域、旧域以及永久域。Jvm生成的所有新对象放在新域中。一旦对象经历了一定数量的垃圾收集循环后,便获得使用期并进入旧域。在永久域中jvm则存储class和method对象。就配置而言,永久域是一个独立域并且不认为是堆的一部分。 下面介绍如何控制这些域的大小。可使用-Xms和-Xmx 控制整个堆的原始大小或最大值。 下面的命令是把初始大小设置为128M: java –Xms128m –Xmx256m为控制新域的大小,可使用-XX:NewRatio设置新域在堆中所占的比例。 下面的命令把整个堆设置成128m,新域比率设置成3,即新域与旧域比例为1:3,新域为堆的1/4或32M: java –Xms128m –Xmx128m –XX:NewRatio =3可使用-XX:NewSize和-XX:MaxNewsize设置新域的初始值和最大值。 下面的命令把新域的初始值和最大值设置成64m: java –Xms256m –Xmx256m –Xmn64m 永久域默认大小为4m。运行程序时,jvm会调整永久域的大小以满足需要。每次调整时,jvm会对堆进行一次完全的垃圾收集。 使用-XX:MaxPerSize标志来增加永久域搭大小。在WebLogic Server应用程序加载较多类时,经常需要增加永久域的最大值。当jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小。为了避免调整,可使用-XX:PerSize标志设置初始值。下面把永久域初始值设置成32m,最大值设置成64m。 java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m 默认状态下,HotSpot在新域中使用复制收集器。该域一般分为三个部分。第一部分为Eden,用于生成新的对象。另两部分称为救助空间,当Eden充满时,收集器停止应用程序,把所有可到达对象复制到当前的from救助空间,一旦当前的from救助空间充满,收集器则把可到达对象复制到当前的to救助空间。From和to救助空间互换角色。维持活动的对象将在救助空间不断复制,直到它们获得使用期并转入旧域。使用-XX:SurvivorRatio可控制新域子空间的大小。 同NewRation一样,SurvivorRation规定某救助域与Eden空间的比值。比如,以下命令把新域设置成64m,Eden占32m,每个救助域各占16m: java -Xms256m -Xmx256m -Xmn64m -XX:SurvivorRation =2如前所述,默认状态下HotSpot对新域使用复制收集器,对旧域使用标记-清除-压缩收集器。在新域中使用复制收集器有很多意义,因为应用程序生成的大部分对象是短寿命的。理想状态下,所有过渡对象在移出Eden空间时将被收集。如果能够这样的话,并且移出Eden空间的对象是长寿命的,那么理论上可以立即把它们移进旧域,避免在救助空间反复复制。但是,应用程序不能适合这种理想状态,因为它们有一小部分中长寿命的对象。最好是保持这些中长寿命的对象并放在新域中,因为复制小部分的对象总比压缩旧域廉价。为控制新域中对象的复制,可用-XX:TargetSurvivorRatio控制救助空间的比例(该值是设置救助空间的使用比例。如救助空间位1M,该值50表示可用500K)。该值是一个百分比,默认值是50。当较大的堆栈使用较低的sruvivorratio时,应增加该值到80至90,以更好利用救助空间。用-XX:maxtenuring threshold可控制上限。 为放置所有的复制全部发生以及希望对象从eden扩展到旧域,可以把MaxTenuring Threshold设置成0。设置完成后,实际上就不再使用救助空间了,因此应把SurvivorRatio设成最大值以最大化Eden空间,设置如下: java … -XX:MaxTenuringThreshold=0 –XX:SurvivorRatio=50000 …4.BEA JRockit JVM的使用 Bea WebLogic 8.1使用的新的JVM用于Intel平台。在Bea安装完毕的目录下可以看到有一个类似于jrockit81sp1_141_03的文件夹。这就是Bea新JVM所在目录。不同于HotSpot把Java字节码编译成本地码,它预先编译成类。JRockit还提供了更细致的功能用以观察JVM的运行状态,主要是独立的GUI控制台(只能适用于使用Jrockit才能使用jrockit81sp1_141_03自带的console监控一些cpu及memory参数)或者WebLogic Server控制台。 Bea JRockit JVM支持4种垃圾收集器: 4.1.1.分代复制收集器 它与默认的分代收集器工作策略类似。对象在新域中分配,即JRockit文档中的nursery。这种收集器最适合单cpu机上小型堆操作。 4.1.2.单空间并发收集器 该收集器使用完整堆,并与背景线程共同工作。尽管这种收集器可以消除中断,但是收集器需花费较长的时间寻找死对象,而且处理应用程序时收集器经常运行。如果处理器不能应付应用程序产生的垃圾,它会中断应用程序并关闭收集。 分代并发收集器 这种收集器在护理域使用排它复制收集器,在旧域中则使用并发收集器。由于它比单空间共同发生收集器中断频繁,因此它需要较少的内存,应用程序的运行效率也较高,注意,过小的护理域可以导致大量的临时对象被扩展到旧域中。这会造成收集器超负荷运作,甚至采用排它性工作方式完成收集。 4.1.3.并行收集器 该收集器也停止其他进程的工作,但使用多线程以加速收集进程。尽管它比其他的收集器易于引起长时间的中断,但一般能更好的利用内存,程序效率也较高。 默认状态下,JRockit使用分代并发收集器。要改变收集器,可使用-Xgc:,对应四个收集器分别为gencopy,singlecon,gencon以及parallel。可使用-Xms和-Xmx设置堆的初始大小和最大值。要设置护理域,则使用-Xns:java –jrockit –Xms512m –Xmx512m –Xgc:gencon –Xns128m…尽管JRockit支持-verbose:gc开关,但它输出的信息会因收集器的不同而异。JRockit还支持memory、load和codegen的输出。 注意 :如果 使用JRockit JVM的话还可以使用WLS自带的console(C:\bea\jrockit81sp1_141_03\bin下)来监控一些数据,如cpu,memery等。要想能构监控必须在启动服务时startWeblogic.cmd中加入-Xmanagement参数。5.如何从JVM中获取信息来进行调整 -verbose.gc开关可显示gc的操作内容。打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等。打开-xx:+ printgcdetails开关,可以详细了解gc中的变化。打开-XX: + PrintGCTimeStamps开关,可以了解这些垃圾收集发生的时间,自jvm启动以后以秒计量。最后,通过-xx: + PrintHeapAtGC开关了解堆的更详细的信息。为了了解新域的情况,可以通过-XX:=PrintTenuringDistribution开关了解获得使用期的对象权。6.Pdm系统JVM调整 6.1.服务器:前提内存1G 单CPU 可通过如下参数进行调整:-server 启用服务器模式(如果CPU多,服务器机建议使用此项) -Xms,-Xmx一般设为同样大小。 800m -Xmn 是将NewSize与MaxNewSize设为一致。320m -XX:PerSize 64m -XX:NewSize 320m 此值设大可调大新对象区,减少Full GC次数 -XX:MaxNewSize 320m -XX:NewRato NewSize设了可不设。4 -XX: SurvivorRatio 4 -XX:userParNewGC 可用来设置并行收集 -XX:ParallelGCThreads 可用来增加并行度 4 -XXUseParallelGC 设置后可以使用并行清除收集器 -XX:UseAdaptiveSizePolicy 与上面一个联合使用效果更好,利用它可以自动优化新域大小以及救助空间比值 6.2.客户机:通过在JNLP文件中设置参数来调整客户端JVM JNLP中参数:initial-heap-size和max-heap-size 这可以在framework的RequestManager中生成JNLP文件时加入上述参数,但是这些值是要求根据客户机的硬件状态变化的(如客户机的内存大小等)。建议这两个参数值设为客户机可用内存的60%(有待测试)。为了在动态生成JNLP时以上两个参数值能够随客户机不同而不同,可靠虑获得客户机系统信息并将这些嵌到首页index.jsp中作为连接请求的参数。
-
JVM 预热是一个非常头疼而又难解决的问题。基于 JVM 的应用程序在达到最高性能之前,需要一些时间来“预热”。当应用程序启动时,通常会从较低的性能开始。这归因于像即时(JIT)编译这些事儿,它会通过收集使用配置文件信息来优化常用代码。最终这样的负面影响是,与平均水平相比,预热期间接收的 request 将具有非常高的响应时间。在容器化、高吞吐量、频繁部署和自动伸缩的环境中,这个问题可能会加剧。在这篇文章中,我们将讨论在运行在 Kubernetes 集群中的 Java 服务如何解决 JVM 预热问题的经验。起因几年前,我们逐步从整体中分离出服务,开始在 Kubernetes 上进行迁移到基于微服务的体系结构。大多数新服务都是在 Java 中开发的。当我们在印度市场上运行一个这样的服务时,我们第一次遇到了这个问题。我们通过负载测试进行了通常的容量规划过程,并确定 N 个 Pod 足以处理超过预期的峰值流量。尽管该服务在轻松处理高峰流量,但我们在部署过程中发现了问题。我们的每个 Pod 在高峰时间处理的 RPM 都超过 10k,而我们使用的是 Kubernetes 滚动更新机制。在部署过程中,服务的响应时间会激增几分钟,然后再稳定到通常的稳定状态。在我们的仪表板中,会看到类似的图表:与此同时,我们开始收到来自部署时间段内的大量投诉,几乎都关于高响应时间和超时错误。第一步:花钱解决问题我们很快意识到这个问题与 JVM 预热阶段有关,但当时有其他的重要事情,因此我们没有太多时间进行调查,直接尝试了最简单的解决方案——增加 Pod 数量,以减少每个 Pod 的吞吐量。我们将 Pod 数量增加了近三倍,以便每个 Pod 在峰值处理约 4k RPM 的吞吐量。我们还调整了部署策略,以确保一次最多滚动更新 25%(使用 maxSurge 和 maxUnavailable 参数)。这样就解决了问题,尽管我们的运行容量是稳定状态所需容量的 3 倍,但我们能够在我们的服务中或任何相关服务中没有问题地进行部署。随着后面几个月里更多的迁移服务,我们开始在其他服务中常常看到这个问题。因此我们决定花一些时间来调查这个问题并找到更好的解决方案。第二步:预热脚本在仔细阅读了各种文章后,我们决定尝试一下预热脚本。我们的想法是运行一个预热脚本,向服务发送几分钟的综合请求,来完成 JVM 预热,然后再允许实际流量通过。为了创建预热脚本,我们从生产流量中抓取了实际的 URL。然后,我们创建了一个 Python 脚本,使用这些 URL 发送并行请求。我们相应地配置了 readiness 探针的 initialDelaySeconds,以确保预热脚本在 Pod 为 ready 并开始接受流量之前完成。令人吃惊的是,尽管结果有一些改进,但并不显著。我们仍然经常观察到高响应时间和错误。此外,预热脚本还带来了新的问题。之前,Pod 可以在 40-50 秒内准备就绪,但用了脚本,它们大约需要 3 分钟,这在部署期间成为了一个问题,更别说在自动伸缩期间。我们在预热机制上做了一些调整,比如允许预热脚本和实际流量有一个短暂的重叠期,但也没有看到显著的改进。最后,我们认为预热脚本的收益太小了,决定放弃。第三步:启发式发现由于预热脚本想法失败了,我们决定尝试一些启发式技术-GC(G1、CMS 和 并行)和各种 GC 参数堆内存CPU 分配经过几轮实验,我们终于取得了突破。测试的服务配置了 Kubernetes 资源 limits: 我们将 CPU request 和 limit 增加到 2000m,并部署服务以查看影响,可以看到响应时间和错误有了巨大的改进,比预热脚本好得多。 第一个 Deployment(大约下午 1 点)使用 2 个 CPU 配置,第二个 Deployment (大约下午 1:25)使用原来 1 个 CPU 配置 为了进一步测试,我们将配置升级到 3000m CPU,令我们惊讶的是,问题完全消失了。正如下面看到的,响应时间没有峰值。 具有 3 个 CPU 配置的 Deployment 很快,我们就发现问题出在 CPU 节流上。在预热阶段,JVM 需要比平均稳定状态下更多的 CPU 时间,但 Kubernetes 资源处理机制(CGroup)根据配置的 limits,从而限制了 CPU。 有一个简单的方法可以验证这一点。Kubernetes 公开了一个每个 Pod 的指标,container_cpu_cfs_throttled_seconds_total 表示这个 Pod 从开始到现在限制了多少秒 CPU。如果我们用 1000m 配置观察这个指标,应该会在开始时看到很多节流,然后在几分钟后稳定下来。我们使用该配置进行了部署,这是 Prometheus 中所有 Pod 的 container_cpu_cfs_throttled_seconds_total 图: 正如预期,在容器启动的前 5 到 7 分钟有很多节流,大部分在 500 秒到 1000 秒之间,然后稳定下来,这证实了我们的假设。 当我们使用 3000m CPU 配置进行部署时,观察到下图: CPU 节流几乎可以忽略不计(几乎所有 Pod 都不到 4 秒)。第四步:改进尽管我们发现了这个问题的根本,但就成本而言,该解决方案并不太理想。因为有这个问题的大多数服务都已经有类似的资源配置,并且在 Pod 数量上超额配置,以避免部署失败,但是没有一个团队有将 CPU 的 request、limits 增加三倍并相应减少 Pod 数量的想法。这种解决方案实际上可能比运行更多的 Pod 更糟糕,因为 Kubernetes 会根据 request 调度 Pod,找到具有 3 个空闲 CPU 容量的节点比找到具有 1 个空闲 CPU 的节点要困难得多。它可能导致集群自动伸缩器频繁触发,从而向集群添加更多节点。我们又回到了原点 但是这次有了一些新的重要信息。现在问题是这样的: 在最初的预热阶段(持续几分钟),JVM 需要比配置的 limits(1000m)更多的 CPU(大约 3000m)。预热后,即使 CPU limits 为 1000m,JVM 也可以充分发挥其潜力。Kubernetes 会使用 request 而不是 limits 来调度 Pod。我们清楚地了解问题后,答案就出现了——Kubernetes Burstable QoS。 Kubernetes 根据配置的资源 request 和 limits 将 QoS 类分配给 Pod。 到目前为止,我们一直在通过指定具有相等值的 request 和 limits(最初是 1000m,然后是 3000m)来使用 Guaranteed QoS。尽管 Guaranteed QoS 有它的好处,但我们不需要在整个 Pod 生命周期中独占 3 个 CPU,我们只需要在最初的几分钟内使用它。Burstable QoS 允许我们指定小于 limits 的 request,例如: 由于 Kubernetes 使用 request 中指定的值来调度 Pod,它会找到具有 1000m CPU 容量的节点来调度这个 Pod。但是由于 3000m 的 limits 要高得多,如果应用程序在任何时候都需要超过 1000m 的 CPU,并且该节点上有空闲的 CPU 容量,那么就不会在 CPU 上限制应用程序。如果可用,它最多可以使用 3000m。 这非常符合我们的问题。在预热阶段,当 JVM 需要更多的 CPU 时,它可以获取需要的 CPU。JVM 被优化后,可以在 request 范围内全速运行。这允许我们使用集群中的冗余的资源(足够可用时)来解决预热问题,而不需要任何额外的成本。 最后,进行假设测试。我们更改了资源配置并部署了应用程序,成功了!我们做了更多的测试以验证结果一致。此外,我们监控了 container_cpu_cfs_throttled_seconds_total 指标,以下是其中一个 Deployment 的图表: 正如我们所看到的,这张图与 3000m CPU 的 Guaranteed QoS 设置非常相似。节流几乎可以忽略不计,它证实了具有 Burstable QoS 的解决方案是有效的。为了使 Burstable QoS 解决方案正常工作,节点上需要有可用的冗余资源。这可以通过两种方式验证:就 CPU 而言,节点资源未完全耗尽;工作负载未使用 request 的 100% CPU。结论尽管花了一些时间,最终找到了一个成本效益高的解决方案。Kubernetes 资源限制是一个重要的概念。我们在所有基于 Java 的服务中实现了该解决方案,部署和自动扩展都运行良好,没有任何问题。要点:在为应用程序设置资源限制时要仔细考虑。花些时间了解应用程序的工作负载并相应地设置 request 和 limits。了解设置资源限制和各种 QoS 类的含义。通过 monitoring/alertingcontainer_cpu_cfs_throttled_seconds_total 来关注 CPU 节流。如果观察到过多的节流,可以调整资源限制。使用 Burstable QoS 时,确保在 request 中指定了稳定状态所需的容量。文章来源:K8sMeetup社区译者:Bach
-
摘要:在用 C 之类的编程语言时,程序员需要自己手动分配和释放内存。而 Java 不一样,它有垃圾回收器,释放内存由回收器负责。本文接下来将介绍Java垃圾收集的知识原理。java内存区域Q: Java虚拟机规范将JVM虚拟机所管理的内存分为几部分?如果是多选题,估计会给一些不在里面的,例如直接内存。A:程序计数器、java虚拟机栈、本地方法栈、方法区、堆。java对象在内存上的分配:§ GC策略Q:java使用根搜索算法来确定对象是否存货,哪些对象可以作为GC Roots?A:虚拟机栈(栈帧中的本地变量表)中的引用的对象方法区中的类静态属性引用的对象方法区中的常量引用的对象本地方法栈中JNI(Native方法)的引用对象Q: 标记清除、标记整理、复制算法哪个块?A: 复制算法较快。3个算法含义如下:Q: SerialOld用的是什么算法?A: 标记整理算法,属于处理老年代算法。各收集器的变化图如下,主要关注一下变化和区别,Q: fullGC 会发生在老年代区还是新生代区?A: 会发生在老年代区。 相反,minorGC一般发送在新生代区。新生代、老生代以及minorGC、fullGC的发生流程如下:Q: 方法区里的class对象(即类对象)什么时候会被回收?A: 所有实例都被回收、对应classLoader也被回收、class对象不会再被引用或者反射(这个咋确定?当初书里看到的,没懂)§ finalized与GCQ: 什么时候会调用对象的finalized方法A: JVM启动垃圾回收,且该对象要被回收时。finalized应该更多是规范吧,很多规范里都要求我们不要自己实现finalized了,毕竟不确定性太大。§ Java虚拟机GC参数配置突然想起来当初看java虚拟机时,把那些参数给跳过了,感觉记不住。但现在发现还是得学的,赶紧恶补一下。Q:-client和-server的对比()启动较快()性能和内存管理效率高(注意启动快和性能好不是一回事)桌面应用一般使用(), 服务器一般使用()A:(-clien)启动较快(-server)性能和内存管理效率高桌面应用一般使用(-clien), 服务器一般使用(-server)有4个跟内存相关的参数-Xmn -Xms -Xmx -Xss回答下面的问题:Q:用于配置java初始堆内存的是()A:-Xms。-X、memory、size ,内存大小Q:用于配置java堆的最大值的是()A:-Xmx。-X、memory、max最大内存Q:如果不设置,-Xms和-Xmx的大小分别默认是多少?A:不设置的话,二者相等,默认是 物理内存/64(小于1G)Q:用于配置新生代内存大小的最大值是:()你问我什么是新生代内存?就是下面这个,1个E区加2个S区的这个内存大小A:-Xmn。-X、memory、new相类似的还有-XX:NewSize 和 -XX:MaxNewSize。Q: 如何根据上面的参数计算老年代内存大小?A:Xmx的值(堆最大值)- Xmn的值(新生代内存)Q: 用于配置线程栈内存的是()? 替代的还有哪个参数?A:-Xss。 另一个是-XX:ThreadStackSize-Xss指 -X stack size有下面3个和gc相关的参数-Xnoclassgc -Xincgc -Xloggc:file回答以下问题:Q:可用于关闭针对类对象的gc功能的是()可用于减少gc的程序停顿时间的是()用于输出gc相关日志的是()A:可用于关闭针对类对象的gc功能的是(-Xnoclassgc)可用于减少gc的程序停顿时间的是(-Xincgc)用于输出gc相关日志的是(-Xloggc:file)Q:-verbose 一般是用于什么的?A:查询gc问题。-verbose:class 输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。-verbose:gc 输出每次GC的相关情况,后面会有更详细的介绍。-verbose:jni 输出native方法调用的相关情况,一般用于诊断jni调用错误信息。Q: -XX:PermSize和-XX:MaxPermSize设置的是什么内存?A:方法区的内存。就是最开始那个图里的这个通过配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,jvm在启动的时候会根据-XX:PermSize初始化分配一块连续的内存块,这样的话,如果-XX:PermSize设置过大,可能会很浪费。而Max如果设置小了,可能会omm。Q:-XX:MetaspaceSize和-XX:MaxMetaspaceSize又是什么内存?A:元数据区内存。 java8引入的,用于替代上面的perm区。无论-XX:MetaspaceSize和-XX:MaxMetaspaceSize两个参数如何设置,随着类加载越来越多不断扩容调整,直到MetaspaceSize(如果没有配置就是默认20.8m)触发FGC,上限是-XX:MaxMetaspaceSize,默认是几乎无穷大本文分享自《Java云服务开发知识学习之 java垃圾收集》,原文作者:breakDraw 。
-
System.out.println( "Hello World!天" ); 在linux终端,正常显示中文,但改变linux的locale为gbk后就显示乱码了。我还以为JVM是根据系统默认编码来输出呢!又在windows下测试,发现了更难以解释的结果:https://segmentfault.com/q/1010000037689821
-
常用的 jvm 调优的参数-Xms2g:初始化推大小为 2g;-Xmx2g:堆最大内存为 2g;-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;-XX:+PrintGC:开启打印 gc 信息;-XX:+PrintGCDetails:打印 gc 详细信息。
上滑加载中
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签