java基础-jvm学习

 

 

参考文章:https://www.jianshu.com/p/82f27b4dd582

https://www.jianshu.com/p/c441cb07e79a

https://blog.csdn.net/xxxx3/article/details/81009524

https://blog.csdn.net/Lnho2015/article/details/77677869

jvm是每一位java开发人员都应该了解甚至是熟知的知识,最早接触时觉得不实用,没有进行过系统的学习,现在补上。

1.什么是jvm?

JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。

2.JRE、JDK和JVM的关系

  • JRE(Java Runtime Environment, Java运行环境)是Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。
  • JDK(Java Development Kit,Java开发工具包)是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API)
  • JVM(Java Virtual Machine, Java虚拟机)是JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS(操作系统)的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性。

jdk是包含jre的,安装的jdk,其实jre就不必安装了,当然,如果你想装jre的话也不会有什么问题。

3.java 体系结构介绍

jvm体系总体分四大块:

  • 类的加载机制
  • jvm内存结构
  • GC算法 垃圾回收
  • GC分析 命令调优

类加载机制之前的博客中有介绍过,主要流程就是将.class文件内容加载至内存中,存放到运行时方法区(static变量也在这里),然后在堆上生成一个class对象,类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

这里再简单复习一下

加载类的过程。

(1)装载(loading)

 

负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名、ClassLoader完成类的加载。
因此,标识一个被加载了的类:类名 + 包名 + ClassLoader实例ID。

(2)链接(linking)

 

负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口。
链接又包含三块内容:验证、准备、初始化。
    1)验证,文件格式、元数据、字节码、符号引用验证;
    2)准备,为类的静态变量分配内存,并将其初始化为默认值;
    3)解析,把类中的符号引用转换为直接引用

(3)初始化(initializing)

 

负责执行类中的静态初始化代码、构造器代码以及静态属性的初始化,以下六种情况初始化过程会被触发。
    1. 创建类的实例,也就是new的方式
    2.访问某个类或接口的静态变量,或者对该静态变量赋值
    3.调用类的静态方法
    4.反射(如Class.forName(“com.shengsiyuan.Test”))
    5.初始化某个类的子类,则其父类也会被初始化
    6.Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
  • 类加载器
    站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

(1).启动类加载器:Bootstrap ClassLoader

 

负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,
或被-Xbootclasspath参数指定的路径中的
并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。
启动类加载器是无法被Java程序直接引用的。

(2).扩展类加载器:Extension ClassLoader

 

该加载器由sun.misc.Launcher$ExtClassLoader实现,
它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),
开发者可以直接使用扩展类加载器。

(3).应用程序类加载器:Application ClassLoader

 

该类加载器由sun.misc.Launcher$AppClassLoader来实现,
它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1、在执行非置信代码之前,自动验证数字签名。
2、动态地创建符合用户特定需要的定制化构建类。
3、从特定的场所取得java class,例如数据库中和网络中。

  • 双亲委托模式当类加载器收到了类加载的请求
    当JVM加载一个类的时候,下层的加载器会将任务给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查之后,按照相反的顺序进行加载。如果Bootstrap加载器不到这个类,则往下委托,直到找到这个类。一个类可以被不同的类加载器加载。
    双亲委派机制:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,
而是把类加载请求委派给父类加载器`ExtClassLoader`去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,
而是把类加载请求委派给`BootStrapClassLoader`去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),
会使用`ExtClassLoader`来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,
如果 `AppClassLoader`也加载失败,则会报出异常ClassNotFoundException。

启动类加载器负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型意义:

 

系统类防止内存中出现多份同样的字节码
保证Java程序安全稳定运行
可见性限制:下层的加载器能够看到上层加载器中的类,反之则不行,委派只能从下到上。
不允许卸载类:类加载器可以加载一个类,但不能够卸载一个类。但是类加载器可以被创建或者删除。

这么总结可能不太好理解,首先,如果你定义了类名、路径和java中的List接口都一样的接口会怎样?运行main方法就会直接报错,因为启动类加载器会发现两个同样的class,而我们定义的那个,是非法的,这么一说,保证字节码不重复就好理解的多了吧,而且由于启动类加载器是最高的父类加载器,在jvm启动时其类库就会被加载,也就是说启动后,我们就可以直接使用jdk提供的大量工具类库,也保证了jdk类库的优先加载。

自定义类加载器的总共分三步:1、继承ClassLoader对象, 2、覆盖findClass方法 3、发挥defineClass方法生成的class

关于类加载和自定义类加载器,上篇博客有展开说,见:https://blog.csdn.net/future_xiaowu/article/details/105268886

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

方法区和堆是所有线程共享的内存区域;
而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。

详细介绍每个区域的作用

堆:对于大多数应用来说。堆(java heap)是java虚拟机管理的内存中最大的一块,java堆被所有线程共享,在虚拟机启动时堆就完成初始化,它的意义就是为了存放对象实体,注意,不是引用,对象实体内容就存在堆中。java最强大的功能之一就是垃圾回收机制,堆是存放对象的地方,那么垃圾回收主要针对的自然也就是堆,所以堆又叫做"GC堆",如果从内存回收的角度看,现在回收期基本都采用分代收集算法,;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError内存溢出异常。

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码}等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;

java7中,存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。

java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

  • 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法(一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。),这个计数器值则为空(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  • JVM栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

  • 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

一张图表示如下

比较常见的问题围绕栈和堆展开,以及jvm内存溢出如何处理的问题。

根据上面的介绍,处理内存溢出要按照具体的场景处理,栈不够会溢出,堆不够会溢出,方法区不够也会溢出,而这些区域对应的都是内存溢出的问题,处理方式不同 ,可参考https://blog.csdn.net/xxxx3/article/details/81009524https://blog.csdn.net/Lnho2015/article/details/77677869

简单来说,需要先定位是哪一种溢出,再对应处理,如果是方法区溢出则一般是方法区空间不够用,而我们创建的class又太多引起的,修改方法区(java7中的永久区)参数 -XX:PermSize=5M -XX:MaxPermSize=7M,可以把这两个参数改大些

第二种是堆引起的溢出,这种事比较常见的,创建了很多对象,而在使用之后这些对象没有得到很好的回收导致堆满溢出,场景也会多一些。这里简单罗列一下

单个对象内容过大,当对象属性多,存储数据大的时候可能会引起这样的问题,对象运行时大小的获取以后介绍,这种情况有限考虑对象瘦身,缩减对象内部存储数据的量和属性字段数,其次修改jvm启动参数,增大堆内存的配置-Xms256m -Xmx1024m

程序内存在死循环不断创建对象也可能引起这种情况,这样的情况下代码需要先优化代码,再修改对应的配置参数,这里需要注意,可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收,JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。,并不是强制执行回收,所以指望这种方法来解决内存溢出的问题是不可靠的。

第三种,栈内存溢出,这种一般与线程挂钩,创建线程过多、线程超时均可能引起上述问题,可以修改栈配置,也需要进行代码优化

 

上面简单说了一下GC,下面,具体先介绍java的垃圾回收机制,以及该如何具体定位内存溢出的问题

JVM三大核心区域

JVMHeap区域(年轻代、老年代)和方法区(永久代)结构图:

第一张图片内容为各个部分主要存储内容和特性,图二简略说明了方法区和堆中的新生代和老年代

从Java GC的角度解读代码:new的对象会首先会进入年轻代的Eden中(如果对象太大可能直接进入年老代)。在GC之前对象是存在Eden和from中的,进行GC的时候Eden中的对象被拷贝到To这样一个survive空间(survive幸存)空间:包括from和to,他们的空间大小是一样的,又叫s1和s2)中(有一个拷贝算法),From中的对象(算法会考虑经过GC幸存的次数)到一定次数(阈值(如果说每次GC之后这个对象依旧在Survive中存在,GC一次他的Age就会加1,默认15就会放到OldGeneration。但是实际情况比较复杂,有可能没有到阈值就从Survive区域直接到Old Generation区域。在进行GC的时候会对Survive中的对象进行判断,Survive空间中有一些对象Age是一样的,也就是经过的GC次数一样,年龄相同的这样一批对象的总和大于等于Survive空间一半的话,这组对象就会进入old Generation中,(是一种动态的调整))),会被复制到OldGeneration,如果没到次数From中的对象会被复制到To中,复制完成后To中保存的是有效的对象,Eden和From中剩下的都是无效的对象,这个时候就把Eden和From中所有的对象清空。在复制的时候Eden中的对象进入To中,To可能已经满了,这个时候Eden中的对象就会被直接复制到Old Generation中,From中的对象也会直接进入Old Generation中。就是存在这样一种情况,To比较小,第一次复制的时候空间就满了,直接进入old Generation中。复制完成后,To和From的名字会对调一下,因为Eden和From都是空的,对调后Eden和To都是空的,下次分配就会分配到Eden。一直循环这个流程。好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC)

 

虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC。

刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。

比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝)

如果OldGeneration满了就会产生FullGC

满原因:
1,from survive中对象的生命周期到一定阈值

2,分配的对象直接是大对象

3、由于To 空间不够,进行GC直接把对象拷贝到年老代(年老代GC时候采用不同的算法)

如果Young Generation大小分配不合理或空间比较小,这个时候导致对象很容易进入Old Generation中,而Old Generation中回收具体对象的时候速度是远远低于Young Generation回收速度。

因此实际分配要考虑年老代和新生代的比例,考虑Eden和survives的比例

Permanent Generation中发生GC的时候也对性能影响非常大,也是Full GC

-XX:SurvivorRatio新生代里面Eden和一个Servive的比例,如果SurvivorRatio是5的话,也就是Eden区域是SurviveTo区域的5倍。Survive由From和To构成。结果就是整个Eden占用了新生代5/7,From和To分别占用了1/7,如果分配不合理,Eden太大,这样产生对象很顺利,但是进行GC有一部分对象幸存下来,拷贝到To,空间小,就没有足够的空间,对象会被放在old Generation中。如果Survive空间大,会有足够的空间容纳GC后存活的对象,但是Eden区域小,会被很快消耗完,这就增加了GC的次数。

二、 JVM的GC日志Full GC日志每个字段彻底详解

[Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]

[Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]

[Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC会导致新生代Minor GC产生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC后,并没有降低内存占用,因为写的程序不断循环一直有引用)(7168K) (老年代总容量)] 8114K(GC前占用整个Heap空间大小)->7555K (GC后占用整个Heap空间大小) (9216K) (整个Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,类相关的一些信息) 2613K->2613K(1056768K) (GC前后基本没变,空间很大)], 0.1022588 secs(GC的耗时,秒为单位)] [Times: user=0.56 sys=0.02, real=0.10 secs](用户空间耗时,内核空间耗时,真正的耗时时间)

三、 Java8中的JVM的MetaSpace

Metaspace的使用C语言实现的,使用的是OS的空间,Native Memory Space可动态的伸缩,可以根据类加载的信息的情况,在进行GC的时候进行调整自身的大小,来延缓下一次GC的到来。

可以设置Metaspace的大小,如果超过最大大小就会OOM,不设置如果把整个操作系统的内存耗尽了出现OOM,一般会设置一个足够大的初始值,安全其间会设置最大值。

永久代发生GC有两种情况,类的所有的实例被GC掉,且class load不存。

对于元数据空间 简化了GC, class load不存在了就需要进行GC。

三种基本的GC算法基石

一、 标记/清除算法

内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。

缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;

 

 

二、 复制算法

把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。

速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。

三、 标记整理算法

平衡点

标记谁是活跃对象,整理,会把内存对象整理成一课树一个连续的空间,

综合了上述算法优略

1, 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。

2, 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。

JVM垃圾回收器串行、并行、并发垃圾回收器概述

1, JVM中不同的垃圾回收器

2, 串行,并行,并发垃圾回收器(和JVM历史有关系,刚开始串行)

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

1、Serial 收集器

      Serial 收集器是新生代的单线程收集器,它“单线程”的意义体现在:       1.它只会使用一个CPU或一条收集线程去完成垃圾收集工作;       2.它在进行垃圾收集时,必须暂停其他所有的工作线程(既Stop The World),直到它收集结束。       Stop The World是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把正常的工作线程全部停掉,这对很多应用来说都是难以接收的,应该尽量避免Stop The World。

2、Serial Old 收集器

      Serial Old 收集器是老年代的单线程收集器,使用“标记-整理”算法,除了与新生代的Serial收集器配合之外,Serial Old 收集器的另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

3、Serial + Serial Old

      Serial 和 Serial Old虽然是单线程收集器,但它有着优于其他收集器的地方,简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,它们由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 

4、ParNew 收集器

      ParNew收集器是新生代收集器,它是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。        ParNew 收集器除了多线程收集之外,其他与Serial收集器相比没有太多创新之处,但它却是许多运行在Server模式下的虚拟机钟首选的新生代收集器,其中一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。        ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对与GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个)的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

5、并行(Parallel)和并发(Concurrent)

      **并行(Parallel)收集器:**指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;       **并发(Concurrent)收集器:**指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

JVM中CMS收集器解密

低延迟进行垃圾回收,在线服务和处理速度要求高的情况下很重要

配置:XX:UseConcMarkSweepGC

concurrence(并发) Mark(标记)Sweep(清理)

低延时

把垃圾回收分成四个阶段

CMS-initial-mark初始标记阶段会stop the world,短暂的暂停程序根据跟对象标记的对象所连接的对象是否可达来标记出哪些可到达

CMS-concurrent-mark并发标记,根据上一次标记的结果确定哪些不可到达,线程并发或交替之行,基本不会出现程序暂停。

CMS-remark再次标记,会出现程序暂停,所有内存那一时刻静止,确保被全部标记,有可能第二阶段之前有可能被标记为垃圾的对象有可能被引用,在此标记确认。

CMS-concurrent-sweep并发清理垃圾,把标记的垃圾清理掉了,没有压缩,有可能产生内存碎片,不连续的内存块,这时候就不能更好的使用内存,可以通过一个参数配置,根据内存的情况执行压缩。

JVM中G1收集器

可以像CMS收集器一样,GC操作与应用的现场一起并发执行

紧凑的空闲内存区域且没有很长的GC停顿时间

需要可预测的GC暂停耗时

不想牺牲太多吞吐量性能

启动后不需要请求更大的Java堆

通过案例瞬间理解JVM中PSYoungGen、ParOldGen、MetaSpace

 

Heap PSYoungGen      total 2560K, used 321K[0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)  eden space 2048K, 15% used[0x00000007bfd00000,0x00000007bfd50568,0x00000007bff00000)  from space 512K, 0% used[0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)  to   space 512K, 0% used[0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) ParOldGen       total 7168K, used 7097K[0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000) object space 7168K, 99%used [0x00000007bf600000,0x00000007bfcee7b8,0x00000007bfd00000) Metaspace       used 2647K, capacity 4486K, committed4864K, reserved 1056768K class space    used 289K, capacity 386K, committed 512K,reserved 1048576K

PSYoungGen是eden + from

使用MAT对Dump文件进行分析实战

导出Dump文件

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章