一、JVM内存模型
1.方法区(Method Area)
类的所有字段和方法的字节码都在这里。所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池 都存放在方法区中。虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该就是为了和java的堆区分开。
2.堆(Heap)
JVM启动时自动分配创建,用于存放类的实例(也就是对象),几乎所有对象都在堆上,当创建对象时却无法在堆中申请到空间时那么将抛出OutOfMemoryError。同时堆也是垃圾收集器管理的主要区域。
2.1新生代(Young Generation)
新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),几乎所有的对象new出来后都是在伊甸区,而一些比较大的对象new出来后会直接放进老年代。幸存者区又分为From Survivor 区和To Survivor 区。当伊甸区的空间满了,而程序又需要空间来存储新new出的对象,这时候JVM的垃圾回收器会对伊甸区进行垃圾回收(对伊甸区的垃圾回收称为Minor GC或者称为Young GC),将伊甸区中没有被引用到的对象进行销毁,然后将伊甸区中剩余的对象移到From Survivor区。若From Survivor区也满了,那么会对From Survivor区进行垃圾回收且同样是Minor GC,然后将From Survivor区未被回收的对象全部转移到To Survivor 区,当转移完成后,To Survivor 区会变成From Survivor区,而From Survivor区会变成To Survivor 区,也就是说在转移完成后To Survivor 区会和From Survivor区进行身份互换,在转移完成后且身份互换前,From Survivor区是空的,而To Survivor 区不是空的,之所以进行这样的身份互换是为了保证To Survivor 区是空的。JVM默认如果一个对象经过了15次的从From Survivor区到To Survivor 区的转移,那证明这个对象的年龄已经到了15了,这时候这个对象就会被放进老年代。
2.2老年代(Old Generation)
新生代经过多次GC仍然存活的对象会进入老年代。若老年代也满了,这时候将发生Major GC(也称为Full GC),对老年代里的对象进行垃圾回收。若老年代执行了Major GC后堆依然无法为新new出的对象提供空间,就会抛出OOM(OutOfMemoryError)。
3.元空间(Meta Space)
在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,元空间与永久代的区别在于元空间不在JVM进程内存中,而是用的本地内存,永久代在JVM进程内存中,永久代逻辑结构上属于堆,但是物理上不属于。
3.1为什么移除永久代
参考官方解释:http://openjdk.java.net/jeps/122
大概意思是移除永久代是为了融合HotSpot与JRockit而作出的努力,因为JRockit没有永久代,不需要配置永久代。
HotSpo与JRockitt这两个虚拟机都被oracle公司收购,oracle公司想结合这两个虚拟机的优点,之后就推出了JDK1.8。从JDK1.8起元空间替代了永久代。
3.2方法区、永久代、元空间三者的关系
《Java虚拟机规范》中规定了方法区这么个概念和它的作用,但是并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。通常我们用的JVM都是HotSpot,在jdk1.8之前HotSpot是使用永久代来实现方法区,从jdk1.8开始元空间替代了永久代,即使用元空间实现方法区。
总的来说这三者的关系就是:方法区是规范标准,而永久代和元空间都是方法区这个标准的具体实现方式。
4.虚拟机栈(Stack)
每个线程都有一个属于自己的虚拟机栈。线程中在执行每个方法时都会创建一个栈帧,栈帧就是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。只要线程结束,那么属于这个线程的虚拟机栈也被释放,也就是说虚拟机栈与其所属线程的生命周期一致。
4.1
4.2 符号引用和直接引用(静态链接、动态链接)
JVM的静态链接和动态链接:静态链接发生在类加载的连接阶段中的解析阶段,动态链接发生在程序运行期间。静态链接和动态链接的作用都是将符号引用转换为直接引用。那么符号引用和直接引用是什么呢?符号引用就是该引用指向的只是一个符号,直接引用就是该引用指向的是一个实际内存地址。以下是自己的理解
java中一个类经编译后形成一个class文件,一个类在编译时并不知道这个类中所引用到的类的实际内存地址,因此在该类的class文件中只能使用引用的类的符号(这个符号就是被引用到的类的完整类名)来代替其实际内存地址。比如在com.hjj.Person类中引用了com.hjj.Student类,那么对Person类进行编译时并不知道Student类的实际内存地址,因此在Person类的class文件中只能使用com.hjj.Student这个符号来表示Student类的实际内存地址。当程序中对Person类进行类加载时,那么就会在类加载的连接阶段中的解析阶段通过虚拟机获取Student类的实际内存地址,然后将符号com.hjj.Student替换为Student类的实际内存地址。这就是符号引用转换为直接引用。
类加载时会对类中静态方法或者私有方法中的引用类型的变量进行静态链接,
参考:https://blog.csdn.net/luzhensmart/article/details/81349587。
在程序运行期间会对执行到方法的内部的引用类型的变量进行动态链接。
5.本地方法栈(Native Method Stack)
和虚拟机栈作用很相似,区别不过是虚拟机栈为JVM执行java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库。
6.程序计数器(Program Counter Register)
程序计数器就是其所属线程所执行的字节码的行号指示器。程序计数器总是存储下一条指令的地址值**(这里的”地址”可以是一个native指针,也可以是方法字节码中相对于该方法起始指令的偏移量即字节码行号。如果该线程正在执行一个native方法,那么程序计数器中的值是”Undefined”,这是因为native方法大多是通过C实现并未编译成需要执行的字节码指令,所以在程序计数器中当然是空即”Undefined”)**,由执行引擎读取下一条指令。随着程序的运行,程序计数器中存储的指令地址值会不断改变,而不会随着程序的运行需要更大的空间,所以程序计数器是个非常小的内存空间,且不会发生溢出情况。
二、JVM性能调优
略
三、JVM垃圾回收
1.垃圾判断算法
- 引用计数算法(Reference Counting)
- 根搜索算法(Root Tracing)
1.1 引用计数算法(Reference Counting)
- 给对象添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的
- 引用计数算法无法解决对象循环引用的问题
1.2 根搜索算法( Roots Tracing )
- 在实际的生产语言中(Java、 C#等),都是使用根搜索算法判定对象是否存活。算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是垃圾。
- Java语言中,GC Roots包括:1.在虚拟机栈中每个栈帧中的局部变量表中的引用;2.方法区中静态引用;3.JNI中的引用。
2.JVM常见GC算法
- 标记-清除算法(Mark-Sweep)
- 标记-整理算法(Mark-Compact)
- 复制算法(Copying)
- 分代收集算法(Generational Collecting)
2.1 标记-清除算法(Mark-Sweep)
- 算法分为”标记“和”清除“两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象
- 缺点:效率不高,需要扫描所有的对象。堆越大,GC越慢。且标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾收集动作。GC次数越多,碎片越严重。
如图所示,图中被标记为绿色的就是用被引用到的对象,标记为红色的没有被引用到,标红的这些对象将被清除。
2.2标记-整理算法(Mark-Compact)
- 算法分为”标记“和”整理“两个阶段。标记阶段同标记-清除算法一样,就是标记出所有需要回收的对象。整理阶段是令所有存活的对象移动到一端,然后直接清理掉这端边界以外的内存。
- 特点:没有空间碎片,但是会耗费较多的时间用于整理。
2.3复制算法(Copying)
- 将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价高昂。
- 现在的商业虚拟机中都是用复制算法来回收新生代。新生代中将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。
- 复制收集算法在对象存活率高的时候,效率有所下降。如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
- 复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小。根据IBM的专门研究,98%的java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间。
2.4分代收集算法(Generational Collecting)
- 当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting) 算法,根据对象不同的存活周期将内存划分为几块。
- 一般是把Java堆分作新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。对于老年代则采用标记-清除算法或者标记-整理算法。
2.5垃圾收集算法总结
参考:https://blog.csdn.net/Searchin_R/article/details/84996683
3.垃圾收集器(Garbage Collector)
每种垃圾收集器可以看作是一个GC算法的具体实现。可以根据具体应用的需求(比如追求吞吐量、追求最短响应时间等)来选用合适的垃圾收集器。没有万能的垃圾回收器,每种垃圾回收器都有自己的适用场景。
3.1 垃圾收集器的“并行”和“并发”
- 并行( Parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态
- 并发(Concurrent)::指收集器在工作的同时,可以允许用户线程工作。但是注意并发不代表解决了GC停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候。但在清除垃圾的时候,用户线程可以和GC线程并发执行。
3.2Serial收集器
- 最早的收集器,单线程进行GC
- 新生代和老年代都可以使用
- 该收集器在新生代采用复制算法;在老年代采用2标记-整理算法
- 因为单线程GC,没有多线程切换的额外开销,简单使用
- Hotsport运行在 Client模式缺省的收集器
3.3 ParNew收集器
- ParNew收集器就是Serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一模一样
- ParNew收集器是Hotsport 运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果,只有在多CPU的环境下,效率才会比Serial收集器高
- 可以通过-XX:ParallelGCThreads来控制GC线程数的多少。需要结合具体CPU的个数。
3.4Parallel Scavenge收集器
Parallel Scavenge收集器也是一个多线程收集器, 也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化
3.5Serial Old收集器
Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器
3.6Parallel Old收集器
该收集器是Parallel Scavenge在老年代的实现,使用多线程和标记-整理算法,JVM 1.6开始提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。Parallel Scavenge+Parallel Old=高吞吐量,但GC停顿可能不理想
3.7CMS(Concurrent Mark Sweep)收集器
CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,CMS收集器使用的是标记-清除算法。CMS收集器只针对老年代,一般结合ParNew收集器使用。
4.GC日志