最全的JVM知识点总结
1:什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
2:JRE/JDK/JVM是什么关系
JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
3:JVM原理
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java的字节码程序。
java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
4:JVM的体系结构
1类装载器(ClassLoader)(用来装载.class文件)
2执行引擎(执行字节码,或者执行本地方法)
3运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
5:JVM运行时数据区
第一块:PC寄存器
PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
第三块:堆(Heap)
它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
(4) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。
第四块:方法区域(Method Area)
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
第五块:运行时常量池(Runtime Constant Pool)
存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
第六块:本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
6:对象“已死”的判定算法
由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。
在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。
有两种算法可以判定对象是否存活:
1.)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
2.)可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。Java中可以作为GC Roots的对象包括:虚拟机栈中引用的对象、本地方法栈中Native方法引用的对象、方法区静态属性引用的对象、方法区常量引用的对象。
在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。
7:JVM垃圾回收
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停
(1)对新生代的对象的收集称为minor GC;
(2)对旧生代的对象的收集称为Full GC;
(3)程序中主动调用System.gc()强制执行的GC为Full GC。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC
8:垃圾收集算法
1、标记-清除算法
最基础的算法,分标记和清除两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。
它有两点不足:一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
2、复制算法
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。
3、标记-整理算法
复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉端边界以外的内存。
4、分代收集算法
当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。
这样就可以根据各个年代的特点采用不同的收集算法。
新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。
老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。
新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。
大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。
长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。没熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。
9:垃圾收集器
垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)如下:
1.Serial收集器
Serial收集器是最基本、历史最久的收集器,曾是新生代手机的唯一选择。他是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其他所有的工作线程,直到它结束,即“Stop the World”。停掉所有的用户线程,对很多应用来说难以接受。比如你在做一件事情,被别人强制停掉,你心里奔腾而过的“羊驼”还数的过来吗?
尽管如此,它仍然是虚拟机运行在client模式下的默认新生代收集器:简单而高效(与其他收集器的单个线程相比,因为没有线程切换的开销等)。
工作示意图:
2.ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样。
是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。
工作示意图:
3.Parallel Scavenge收集器
新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
4.Serial Old收集器
Serial 收集器的老年代版本,单线程,“标记整理”算法,主要是给Client模式下的虚拟机使用。
另外还可以在Server模式下:
JDK 1.5之前的版本中雨Parallel Scavenge 收集器搭配使用
可以作为CMS的后背方案,在CMS发生Concurrent Mode Failure是使用
工作示意图:
5.Parallel Old收集器
Parallel Scavenge的老年代版本,多线程,“标记整理”算法,JDK 1.6才出现。在此之前Parallel Scavenge只能同Serial Old搭配使用,由于Serial Old的性能较差导致Parallel Scavenge的优势发挥不出来,尴了个尬~~
Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。组合的工作示意图如下:
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:
1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”
2)并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。
3)重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
4)并发清除:清除标记为可以回收对象,可以和用户线程并发执行
由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。
工作示意图:
CSM收集器有3个缺点:
1)对CPU资源非常敏感
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
2)无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现"Concurrent Mode Failure"失败。
并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
3)产生大量内存碎片:CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
7.G1收集器
G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服务端应用的垃圾收集器。它的使命是未来可以替换掉CMS收集器。
G1收集器特性:
并行与并发:能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。
分代收集:G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。
空间整合:整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。
可预测的停顿:除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。
为什么能做到可预测的停顿?
是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。
G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
这就保证了在有限的时间内可以获取尽可能高的收集效率。
对象被其他Region的对象引用了怎么办?
判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):新生代回收的时候不得不扫描老年代?无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier 暂时中断操作;然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象);如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
进行垃圾收集时,在GC根节点的枚举范围加入 Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。
不计算维护Remembered Set的操作,回收过程可以分为4个步骤(与CMS较为相似):
1)初始标记:仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象,需要“Stop The World”
2)并发标记:从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行
3)最终标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
4)筛选回收:首先对各个Region的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用复制算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。
工作示意图:
10:基本结构
从Java平台的逻辑结构上来看,我们可以从下图来了解JVM:
从上图能清晰看到Java平台包含的各个逻辑模块,也能了解到JDK与JRE的区别。
大家好,我是救火队队长。不知不觉,《JVM实战高手》专栏已经开始两周了,每篇文章,大家都有各种各样的问题。
在解答这些问题的过程中,我也时不时会受到新的启发,就是这个技术点,我或许应该这么讲,大家能听得更加明白一些。
通过对大家的答疑,反过来让我继续打磨文章,这事儿还挺有意义
下面将大家上周提出的问题,做了一个小汇总:
问题
既然栈帧存放了方法对应的局部变量的数据,也包括了方法执行的其它相关信息。
那为什么不把程序计算器那块记录执行的情况,也放在各个方法自己的栈帧里,而是要单独列一个程序计数区去存储呢?
答:这就是JVM设计者的设计思想了,因为程序计数器针对的是代码指令的执行,Java虚拟栈针对的是放方法的数据,一个是指令,一个是数据,分开设计
问题
思考题回答:什么情况下一个类会被回收?
- 首先该类的所有实例(堆中)都已经被回收;
- 其次该类的ClassLoader已经被回收;
- 最后,对该类对应的Class对象没有任何引用。满足上面三个条件就可以回收该类了。
答:正解
问题
方法执行完后, 栈帧立马被出栈, 那该栈帧中的变量等数据是立马就被回收掉吗?还是需要等垃圾回收线程扫描到再回收掉?
答:出栈就没了
问题
如果把public static int flushInterval = Configuration.getInt("xxx");中的static去掉, 那后面的getInt是在什么时候执行的呢 ?
我自己测试了一下,好像是在构造方法之前执行的, 不明白这个到底属于什么阶段?
答:这是属于类的对象实例初始化的阶段
问题
双亲委派模型设计的出发点很重要,文章漏了。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
也就是说,判断2个类是否“相等”,只有在这2个类是由同一个类加载器加载的前提下才有意义
否则即使这2个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这2个类必定不相等。
基于双亲委派模型设计,那么Java中基础的类,Object类似Object类重复多次的问题就不会存在了
因为经过层层传递,加载请求最终都会被Bootstrap ClassLoader所响应。加载的Object类也会只有一个
否则如果用户自己编写了一个java.lang.Object类,并把它放到了ClassPath中,会出现很多个Object类,这样Java类型体系中最最基础的行为都无法保证,应用程序也将一片混乱
答:这位同学非常不错,对jvm有一定的研究,不过我们第一周的文章,并不是说漏掉你说的这些点,而是我们的写作思路,是循序渐进,这点很重要。
如果在刚开始就给出大段这种说明,那么只有少数人会看懂,回到普通的那种学院派纯理论的知识传递方法了。
你说的很好,不过希望耐心跟着继续看,我们会有意把很多细节放在后面讲,循序渐进,保证很多小白同学都轻松学习,这点很重要。
问题
tomcat需要破坏双亲委派模型的原因:
- tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;
- 同一个第三方类库的相同版本在不同web应用可以共享
- tomcat自身依赖的类库需要与应用依赖的类库隔离 (3)jsp需要支持修改后不用重启tomcat即可生效 为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器
答:回答的非常好
问题
老师您好,我想问一下,我们的应用如果关掉,创建在堆中的对象,还有方法区的数据都还在吗?
答:应用关了,那么系统对应的JVM进程就没了,那JVM内存区域的数据就全没了
问题
请问老师:
- “实例对象都已经从Java堆内存里被回收”和“Class对象没有任何引用”是一个概念么?
- “ClassLoader已经被回收”,什么时候会回收?
答:
- 不是,Class对象代表类,如果你有变量引用了类的Class对象,那么就是有引用
- 比如你自定义的ClassLoader,本身就是个对象,一旦他没人再使用了,就会被垃圾回收
问题:引用Class对象的是该类的实例对象?还是其他什么?
答:比如用反射,可以获取一个对象的类的Class对象实例,比如Class clazz = replicaManager.getClass(),就可以通过replicaManager引用的对象,获取到ReplicaManager类的Class对象,那个clazz变量,就可以引用这个Class对象
问题
第二周打卡,跟上节奏。回答今日思考题:项目中托管给Spring管理的对象,带@Configration的配置对象,都是长期存在老年代。
自己定义那些pojo对象,如果不被定义为类对象就是朝生夕灭的,所以分配在年轻代里面。
答:非常好
问题
public void load(){ A a = new A(); a这个保存地址的变量是存在虚拟机栈的,这个方法执行完成后就销毁了,
那new A()这个对象是需要等待垃圾回收线程扫描后才回收吗?
还是和a这个变量同时回收?
答:对象要等待垃圾回收才销毁
问题
gc回收的是软引用,弱应用和虚引用,关于软引用和弱引用傻傻分不清,这两者有何异同,请指教
答:内存不够才会回收软引用对象,内存空间足够的话,不会回收软引用对象。弱引用不管内存空间够不够,只能撑到下次垃圾回收之前,就被会回收。
问题
思考题回答:目前的系统,大部分是spring容器的对象,spring默认单实例方式加载,这些对象可能会存在老年代中。
但是方法内部new出来的对象不会存活太长时间,方法结束,引用消息,对象也会在下一次gc被回收。
答:非常好
问题
类初始化时,变量引用的是new出来的对象,此时变量引用的对象会被实例化到堆内存吗?
答:会实例化放到堆内存
问题
老师好。我今天使用Java VisualVm看了一下,发现了一个问题,我配置的是-Xms4M -Xmx4M -Xmn2M。应该是年轻代2M 老年代2M。
写了一个while循环不断的在方法里创建临时变量对象,但是我发现当内存堆达到3m左右的时候,就发生了Minor GC,堆内存回到了2M,而不是4M的时候
理论上不应该是堆内存满了再Minor GC吗?
答:这个很正常的,因为后续第三周会讲新生代的内存结构,其实不是新生代全部占满才minor gc,是里面一块主要的内存区域满了,就minor gc
问题
打卡。做项目时候没有关注系统压力,主要是考虑功能怎么现实,然后按时交付测试。以后可以按老师今天这个思路去估算一下系统压力了。
答:是的,如果没合理估算内存压力,没合理设置jvm内存大小,那么上线之后,可能会发现频繁gc问题,导致系统卡顿,这是jvm优化的第一步,合理估算业务压力,合理设置内存大小
问题
案例总结:
1. 分析系统压力点在哪里?
2. 压力点的每秒请求数?
3. 每个请求耗时?
4. 每个请求消耗的内存?
5. 整个系统的所有请求重复1-4。
6. 算出部署多少台机器?每个机器多少内存?
思考题作答: 平时工作中很少这样预估系统压力,一般我的做法都是部署上去后分配一个堆内存,然后测试时再去监控GC的频率做适当调整。
这样做确实很被动,很多时候上线后发现和测试的GC频率差太多,以后试试老师这种估算方法。
答:非常好
问题
上次发生内存溢出,我们搞到凌晨5点多,最后我们老大调大了堆内存解决的,说是由于使用过多的静态内部类,有地方引用到无法释放导致的,不过我现在还没有明白为啥?
答:学完这个专栏,你也能掌握这种能力
问题
这篇文字最重要的收获是分析处理问题的思路,分解然后一步一步分析处理。赞。
答:是的,思路非常的重要,按照这个思路来,你们自己也能做jvm内存压力预估,系统上线前,合理设置一个内存大小
问题
是不是不应该在高峰期的时候让系统进行垃圾回收,这样会造成STW。老师你们线上系统会考虑在低峰期手动触发垃圾回收么?
答:建议不要手动触发,依托合理的内存设置以及参数优化,让系统自行运转
问题
是不是应该通过老师说的估算方式,尽量设大新生代 ,让系统在高峰期不产生gc?
答:是的,尽量是这样
问题
老师,那不管三七二十一,在内存大的条件下,多分配给新生代就好了,如果不行就加内存?
答:那你就浪费机器资源,要合理评估,不需要大内存,就用小内存就可以了
问题
1、支付系统高分期需要处理的请求是是不是应该这么算:
100万 / (24 * 3600) ≈ 12,根据28法则,大部分请求发生在中午12点到13点以及晚上的18点到19点
所以 80万请求 / (2 * 3600) ≈ 111,即算出如果单台每秒大概是100多个请求
2、还有就是在完整的支付系统内存占用需要进行预估中,你提到“可以把之前的计算结果扩大10倍~20倍。
也就是说,每秒除了内存里创建的支付订单对象还创建数十种对象” 这里如果要计算的话之前的计算结果是 30 * 500字节 * 20倍 = 300000字节=300KB 是这么算吗?
答:没错的,这是大致估算的方法
问题
老师 您这儿的案例中提到,一个支付请求需要1s中,30个请求也是1s钟,那是不是可以理解为开了30个线程同时并发处理支付请求入库?
答:就是这个意思
问题
文章中写的:
“可能你每秒过来的1000笔交易,不再是1秒就可以处理完毕了,因为压力骤增,会导致你的系统性能下降,可能偶尔会出现每个请求处理完毕需要几秒钟”
老师,这里说的压力骤增是磁盘读写压力吗还是内存CPU压力,出行每个请求处理完毕需要几秒这里是写入压力吗?与网络有关吗?谢谢
答:都有可能,主要是CPU负载过高,会导致高并发下每个请求的处理性能直线下降,还有网络问题也会有
问题
我们订单一天二百多万,线上正常每秒产生也应该在1M以上,xmn2048,xmx8192,本来半个多小时一次minor gc
扩大一百倍,不到一分钟一次,应该会出现案例中的问题,老年代会频繁gc,不过我们有6g老年代,达到full gc应该时间会稍微长点
答:自己分析的非常好,掌握这个方法了
问题
老师, 可以说下, 为什么并发上来了, 压力就会剧增嘛? 哪些方面的压力.
答:并发上来之后,内存、网络带宽、磁盘IO、数据库,都是系统的瓶颈,比如网络带宽打满,你的请求就会排队卡住,磁盘IO变满,数据库性能下降,都会导致请求处理慢几十倍
问题
您好,我有一个问题,就是main函数中创建了对象,这个对象在堆中开启空间,那么如果这个对象中有成员变量,这个成员变量是存在哪里?成员变量的引用存在哪里
答:成员变量也是在堆内存里的
问题
老师我上网查了一下资料,把问题弄明白了。Test.class是被加载了,但是并没有 执行初始化步骤。
课程中提到了类加载的时机,但是没有提到类初始化的时机,我把一直理解类的 加载->验证->准备->解析->初始化是一个连续的动作,以为类一旦加载必定 会立即初始化。
补充类初始化的时机如下:
- 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
- 当调用某个类的静态方法时
- 当使用某个类或接口的静态字段时
- 调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
- 当初始化某个子类时
- 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类) 所以System.ou.println(Test.class)不满足上面6种情况,也就没有做初始化
答:对的,就是这样
问题
老师 假如Kafka类里面 声明一个实例变量 private ReplicaFetcher = new ... 这个实例变量放在哪个区
答:实例变量就在堆内存里
问题
老师 根据示例代码, 我做了以下jvm参数配置:-Xms10m -Xmx10m, 然后在visualVM里跟踪堆栈使用情况。
十分诧异的现象是:在while true循环中,也就是执行fetchFromRemote的时候, 新生代大小一直在有规律的增长,然后不停的minor GC, 每次GC(而不是等到15次以后),老年代都会相应的增长一点。
我的问题是,使新生代增长的到底是什么对象?GC时又是什么对象跑到老年代里去了?
按我的理解,fetcher对象应该有且只有一份实例,而且while循环中,不会生成新的对象,
最初,新生代里有一份fetcher,然后第16次minor GC的时候,fetcher被转移到老年代, 无论如何,新生代和老年代都不会不断增长。
所以,是不是有什么我不知道的对象混了进去,导致新生代不断增长?
答:新生代到老年代转移的机制不只是年龄一种,还有别的,下周会详细说明
问题
老师,每个订单处理时间是1秒和10秒,10秒的就要比1秒的要多加内存吗?请问是怎样的逻辑?能否量化?
答:那你得计算一下,你的内存每秒被使用的速度,根据这个来规划内存大小
还有你要是10秒一个请求,可能内存里累计起来会有大量对象没法释放,会导致瞬间新生代被打满
而且大量对象没法回收,然后全部去老年代,然后老年代也很快就满了,最后内存不够,很快就内存溢出了
END
推荐一个专栏:
《从零开始带你成为JVM实战高手》
作者是我多年好友,以前团队的左膀右臂
一起经历过各种大型复杂系统上线的血雨腥风
现任阿里资深技术专家,对JVM有丰富的生产实践经验