二、JVM原理

二、JVM原理

 

    1、执行过程

        首先,当一个程序启动之前,它的class会被类装载器装入方法区,执行引擎读取方法区的字节码自适应解析,然后PC寄存器(程序计数器)指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统API等等。

    2、生命周期

        JVM是Java虚拟机规范,现在默认它的实现是HotSpot。

        (1)、JVM实例的诞生

            JVM实例对应了一个独立运行的Java程序(进程级别),当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

        (2)、JVM实例的运行

            main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

        (3)、JVM实例的消亡

            当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。

    3、类加载机制(ClassLoad)

        Java文件通过Java源码编译器将Java文件编译成.class文件,然后类加载器又将这些.class文件加载到JVM中。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

        (1)、加载(重点)

            ①、加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象。

            ②、加载过程:

                a、通过类的全限定名(类全名)来获取定义此类的二进制字节流。

                b、将这个类字节流代表的静态存储结构转为方法区的运行时数据结构。

                c、在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。

            ③、以上类的加载整个过程由类加载器完成,类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。通过使用不同的系统类加载器,可以从不同来源加载类的二进制数据:

                a、从本地文件系统加载class文件

                b、从Jar包加载class文件

                c、通过网络加载class文件

        (2)、验证

            主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。 

            ①、文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主、次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。

            ②、元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

            ③、字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证,保证类方法在运行时不会有危害出现。

            ④、符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

        (3)、准备

            为类变量分配内存,并设置类变量初始值,即在方法区中分配这些变量所使用的内存空间。这里是类变量(static修饰的变量),不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。

        (4)、解析

            解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

            ①、符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要不会出现冲突能够定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

            ②、直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

        (5)、初始化

            初始化是为类的静态变量赋予正确的初始值。

        (6)、类加载的时间点

            ①、创建类的实例,也就是new一个对象

            ②、访问某个类或接口的静态变量,或者对该静态变量赋值

            ③、调用类的静态方法

            ④、反射(Class.forName("com.lyj.load"))

            ⑤、初始化一个类的子类(会首先初始化子类的父类)

            ⑥、JVM启动时标明的启动类,即文件名和类名相同的那个类

        (7)、类加载器

            类加载器负责加载所有的类,并为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次加载。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

            ①、启动类加载器(Bootstrap ClassLoader):它用来加载Java的核心类,负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现(并不继承自java.lang.ClassLoader,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

            ②、扩展类加载器(Extension ClassLoader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,用户可以直接使用。

            ③、应用程序类加载器(Application ClassLoader):负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

            ④、自定义类加载器(User ClassLoader):用户自己定义的类加载器。

        (8)、双亲委派机制(类加载默认机制)

            ①、首先会到自定义加载器中查找(其实是看运行时数据区的方法区有没有加载),看是否已经加载过,如果已经加载过,则返回字节码。

            ②、如果自定义加载器没有加载过,则委托上一层加载器(AppClassLoader)查看是否已经加载过Test.class。

            ③、如果没有加载过,则继续委托上一层加载器(Extension ClassLoader)查看是否已经加载过。

            ④、如果没有加载过,则继续委托上一层加载(BoopStrap ClassLoader)是否已经加载过。

            ⑤、如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下("sun.boot.class.path")查看是否有Test.class字节码,有则返回,没有通知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看。

            ⑥、依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。

            优点:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,从而也避免Java核心API中定义类型不会被随意替换。

//类路径
java.lang.ClassLoader;
public abstract class ClassLoader {
    /**
      * 类加载方法
      */
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先,先查验当前类加载器是否加载了该类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 递归调用,委托其父类查找是否加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 递归边界,如果没有parent就停止,进入else
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 调用当前类的findClass方法查找加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

        (9)、示例

            ①、定义String类,跟java.lang.String包名类名都相同

package java.lang;
public class String {

    public String toString(){ return "String Hello" ; }

    public static void main(String[] args) {
        String s = new String();
        s.toString();
    }
}

            ②、运行String类的main方法,会报错提示找不到main方法,因为实际代码运行的时候运行的是rt.jar下面的String类,里面没有main方法。

Connected to the target VM, address: '127.0.0.1:57311', transport: 'socket'
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
    public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
Disconnected from the target VM, address: '127.0.0.1:57311', transport: 'socket'

    4、内存模型

        Java的虚拟机种有两种线程,一种是守护线程,一种是非守护线程(也叫普通线程),main函数就是个非守护线程,虚拟机的gc(垃圾回收机制)就是一个典型的守护线程。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误,虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。堆和方法区空间不足则会引发OutOfMemoryError。

        (1)、堆(Heap):所有线程间共享

            a、在虚拟机启动的时候创建。

            b、堆是Java虚拟机所管理的内存中最大的一块。

            c、几乎所有的对象实例以及数组都要在这里分配内存。

            d、堆是垃圾收集器管理的主要区域。

            e、Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden空间、From Survivor空间、To Survivor空间。Eden区放新创建对象,From survivor和To survivor保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

            f、Java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。

            g、如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

        (2)、方法区(Method Area)/ 元空间:所有线程间共享

            a、在虚拟机启动的时候创建。

            b、跟堆一样计算机物理存储上是不连续的、逻辑上是连续的,也是大小可调节的,不同的是还可以选择不实现垃圾回收。

            c、用于存放已被虚拟机加载的类信息(Class 构造方法、接口定义)、常量(final)、静态变量(static)、以及编译后的方法实现的二进制形式的机器指令集等数据。指令是Java代码经过javac编译后得到的JVM指令,PC寄存器指向下一条该执行的指令地址,局部变量区存储函数运行中产生的局部变量,栈存储计算的中间结果和最后结果。

            d、被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。

            e、运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

        (3)、Java虚拟机栈(Java Virtual Machine Stacks):线程私有

            a、每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的是当前线程中局部基本类型的变量(Java中八种基本类型和对象引用(reference)、部分返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址;

            b、每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

            c、栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,也是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈。依次执行完毕后,先弹出后进栈的F3栈帧,再弹出F2栈帧,再弹出F1栈帧。

            d、Java虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令。

        (4)、本地方法栈(Native Method Stacks):线程私有

            a、本地方法:是指方法的修饰符是带有native的但是方法体不是用Java代码写的一类方法,这类方法存在的意义是填补Java代码不方便实现的缺陷而提出的。

            b、作用同Java虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

            c、本地方法栈调用本地方法接口(JNI:Java Native Interface),本地方法接口调用本地方法库。

        (5)、程序计数器/PC寄存器(Program Counter Register):线程私有

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

        (6)、Java1.8的变化

            ①、在JDK1.7以前HotSpot虚拟机使用永久代来实现方法区,永久代的大小在启动JVM时可以设置一个固定值(-XX:MaxPermSize),不可变。

            ②、JDK1.8中进行了较大改动

                a、移除了永久代(PermGen),替换为元空间(Metaspace);

                b、永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);

                c、永久代中的 interned Strings 和 class static variables 转移到了 Java heap;

                d、永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

    5、GC机制

        (1)、垃圾回收的内存区域

            方法区、堆

        (2)、垃圾回收的对象

            a、引用计数(Reference Counting):每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。但无法解决对象之间相互循环引用的问题。

            b、可达性分析(Reachability Analysis):这是Java虚拟机采用的判定对象是否存活的算法。从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,为不可达对象。

                GC Roots:静态变量、常量池、Native方法、线程栈变量等

        (3)、垃圾回收的触发

            a、程序调用System.gc()时可以触发

            b、系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)

        (4)、GC算法

            a、标记-清除算法

                为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,遍历所有对象为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

                优点:

                    标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。更重要的是,这个算法并不移动对象的位置。

                缺点:

                    效率比较低(递归与全堆对象遍历),每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。不移动对象位置,导致可能出现很多碎片空间无法利用的情况。

            b、标记-整理算法

                是标记-清除法的改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并不直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

                优点:

                    不会像标记-清除算法那样产生大量的碎片空间。

                缺点:

                    如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

            c、复制算法

                将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将该内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,以此循环。

                优点:

                    实现简单,并不产生内存碎片。

                缺点:

                    每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。

            d、分代收集算法(主流JVM使用)

                堆分为新生代(Young)和老年代(Tenure),新生代(Young)分为Eden区、From Survivor区、To Survivor区,默认情况下新生代各自占比 8:1:1

。新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理或者标记-清除算法。

                ①、当系统创建一个对象的时候,总是在Eden区操作,当这个区内存满了,利用复制算法将Eden区存活的对象复制到From Survivor区,这样就会触发一次YoungGC,进行新生代的垃圾回收。

                ②、当Eden区再次被用完,就再触发一次YoungGC,会将Eden区与From Survivor区存活的对象复制到To Survivor区。

                ③、再下一次YoungGC的时候,Eden区被用完,则将Eden区与To Survivor区中的还在被使用的对象复制到From Survivor区。

                ④、经过若干次YoungGC后,有些对象在From与To之间来回复制,当达到了From Survivor区与To Survivor区的阈值,这些对象会被复制到老年代。

                ⑥、当老年代的内存被用完后会进行全量回收(Full GC)。

                注:要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。因为Full GC使用太频繁的话,会对系统性能产生很大的影响。

    6、JVM参数

        例:-Xms128m -Xmx4096m -Xss1024k -XX:MetaSpaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

        标准:- 开头,所有的HotSpot都支持

        非标准:-X 开头,特定版本HotSpot支持特定命令

        不稳定:-XX 开头,下个版本可能取消

        (1)、常用命令

            ①、jsp -l :查看应用进程ID

            ②、jinfo - flag :查看某一个参数具体值

            ③、jinfo -flags :查看所有配置参数

            ④、java -XX:+PrintFlagsInitial :查看所有参数

                = 表示没有修改过的默认值; := 表示修改过的值。

        (2)、-Xms 等价于 -XX:InitialHeapSize 

            初始内存大小,默认为物理内存1/64(<1GB)。默认空余堆内存小于40%时,JVM就会增大堆知道Xmx的最大限制。

        (3)、-Xmx 等价于 -XX:MaxHeapSize

            堆最大分配内存,默认为物理内存1/4(<1GB)。默认空余堆内存大于70%时,JVM会减少堆直到Xms的最小限制。

        (4)、-Xss 等价于 -XX:ThreadStackSize

            设置单个线程栈的大小,一般默认为512~1024K;0代表默认出厂值。在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

        (5)、-Xmn

            设置年轻代大小。此处的大小是Eden + 2 Survivor,堆 = 年轻代 + 年老代 + 持久代。增大年轻代后将会减小年老代大小,此值对系统性能影响较大,官方推荐配置为整个堆的3/8。

        (6)、-XX:MetaSpaceSize

            设置元空间大小,元空间本质和永久代(JDK1.7之前使用永久代实现方法区)类似,都是对JVM中方法区的实现。不过元空间与永久代之间最大的区别是:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

        (7)、-XX:+PrintGCDetails

            输出详细GC收集日志信息。

        (8)、-XX:SurvivorRatio

            设置新生代中Eden区和Survivor区的空间比例,-XX:SurvivorRatio=8, Eden:S0:S1=8:1:1。

        (9)、-XX:NewRatio

            配置年轻代与老年代在堆结构的占比(除去持久代),-XX:NewRatop=4表示新生代:老年代=1:4。

        (10)、-XX:MaxTenuringThreshold

            设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制。该参数只有在串行GC时才有效。

        (11)、-XX:+HeapDumpOnOutOfMemoryError

            当JVM发生OOM时,自动生成DUMP文件。

        (12)、-XX:HeapDumpPath=${目录}

            生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=${目录}/java_heapdump.hprof。如果不指定文件名,默认为:java_<pid>_<date>_<time>_heapDump.hprof。

    7、垃圾回收器

        (1)、垃圾回收器种类

            ①、串行垃圾回收器(Serial) 

                配置参数:-XX:UseSerialGC

                它为单线程环境设计,且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境。

            ②、并行垃圾回收器(Parallel)

                配置参数:-XX:UseParallelGC

                多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。

            ③、并发垃圾回收器(CMS:Concurrent Mark Sweep)

                用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行)不需要停顿用户线程。互联网公司多用,适用对响应时间有要求的场景,强交互场景。

            ④、G1垃圾回收器

                G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。

            通过命令查看默认垃圾回收器:java -XX:+PrintCommandLineFlags -version

            JDK1.8 默认垃圾回收器为Parallel Scavenge + Parallel Old

        (2)、Serial(串行)收集器

            ①、串行收集器是一个单线程的收集器,在进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。串行收集器是最古老,最稳定以及效率最高的收集器,只使用一个线程去回收但其在垃圾收集过程中可能会产生较长的停顿(Stop-The-Word)。虽然收集垃圾过程中需要暂停所有其他工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器。

                STW(Stop-The-Word)会暂停整个应用服务

            ②、配置参数:-XX:+UseSerialGC 

                开启后会使用:Serial(新生代)+ Serial Old(老年代)的收集器组合。新生代使用复制算法,老年代使用标记-整理算法,表示新生代、老年代都会使用串行回收收集器。

        (3)、Serial Old收集器

            ①、Serial Old是Serial垃圾收集器的老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器主要是运行在Client默认的Java虚拟机默认的老年代垃圾收集器。

            ②、配置参数:-XX:+UseSerialGC 

                开启后会使用:Serial(新生代)+ Serial Old(老年代)的收集器组合。新生代使用复制算法,老年代使用标记-整理算法。

        (4)、ParNew(并行)收集器

            ①、并行收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

            ②、配置参数:-XX:+UseParNewGC

                开启上述参数后会使用:ParNew(年轻代)+ Serial Old(老年代)的收集器组合,新生代使用复制算法,老年代使用标记-整理算法。但是此种搭配Java 8 已经不再推荐使用。因为是并行收集器,可以通过参数:-XX:ParallelGCThreads 来限制线程数量,默认开启和CPU数目相同的线程数。

        (5)、Parallel Scavenge收集器

            ①、Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。

                可控制的吞吐量Thoughput = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

                自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。自适应调节策略就是虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。

            ②、配置参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC

                开启参数后会使用:Parallel Scavenge(年轻代) + Parallel Old(老年代)的收集器组合。新生代使用复制算法,老年代使用标记-整理算法。以上两个配置参数无论配置哪一个都可以使用此收集器组合,因为这两个收集器可以互相激活。另外配置-XX:ParallelGCThreads = 数字N,表示启动多少个GC线程。

        (6)、Parallel Old收集器

            ①、Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法。Parallel Old收集器在JDK1.6才开始提供,在此之前新生代使用Parallel Scavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代吞吐量优先,无法保证整体的吞吐量。

                Parallel Old正式为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配。

            ②、配置参数:-XX:+UseParallelOldGC

                开启参数后会使用:Parallel Scavenge(年轻代) + Parallel Old(老年代)的收集器组合。新生代使用复制算法,老年代使用标记-整理算法。

        (7)、CMS(Concurrent Mark Sweep:并发标记清除)收集器

            ①、并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。

                并发标记清除,并发收集低停顿,并发指的是GC线程和用户线程一起运行。

            ②、配置参数:-XX:+UseConcMarkSweepGC   开启该参数后自动将-XX:+UseParNewGC打开  

                开启上述参数后会使用:ParNew(年轻代)+ CMS(老年代)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器,年轻代使用复制算法,年老代使用CMS的标记-清除加Serial Old的标记-整理算法。

            ③、CMS过程

                a、初始标记(CMS initial mark)(STW):只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

                b、并发标记(CMS concurrent mark):进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。

                c、重新标记(CMS remark)(STW):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。

                d、并发清除(CMS concurrent sweep):清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发执行的。

        (8)、G1收集器(重点)

            ①、G1是一种服务端的垃圾收集器,应用在多CPU和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器暂停时间短的优点,Java 7 发布了一个新的垃圾收集器G1垃圾收集器。主要改变是Eden、Survivor和Tenured等内存区域不再是连续的了,而是变成一个个大小相同的region(区域),每个region从1M到32M不等。一个region可能属于Eden、Survivor或者Tenured的内存区域。当大对象分配失败、晋升失败、疏散失败等都有可能触发Full GC。

            ②、特点

                a、G1能充分利用多CPU、多核硬件优势,尽量缩短STW。

                b、G1整体上采用标记-整理算法,局部通过复制算法,不会产生内存碎片。

                c、整体看G1不再区分年轻代和老年代,把内存划分成多个独立的子区域(Region)。通过参数-XX:G1HeapRegionSize=n(1~32M,必须为2^n),默认将整堆划分为2048个分区。

                d、每个Region仍要进行年轻代、老年代的区分,但它们物理上不一定再连续。

                e、G1也是分代收集器,但是整个内存的划分不存在物理上的年轻代和老年代的区别,只有逻辑上的分代概念。每个分区也不会固定地为某个代服务,根据需要会在年轻代和老年代之间切换。

            ③、原理

                a、G1算法将堆划分为若干个Region,仍然属于分代收集器,有逻辑上的新生代、老年代的划分。

                b、一部分Region为新生代,某个Region为新生代,当其进行垃圾收集时依然采用暂停所有应用线程的方式,将存活的对象拷贝到老年代或者Survivor空间。

                c、一部分Region为老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成清理工作,这样就不会有CMS内存碎片问题了。

                d、G1有一种特殊的区域Humongous(巨大的)区域。如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,这些对象将默认直接分配到老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。Humongous专门用来存放巨型对象,如果一个H区放不下,那么G1寻找连续的H区进行存储,但是为了能找到连续的H区,有时候不得不启动Full GC。

            ④、过程

                a、初始标记:只标记GC Roots能直接关联到的对象

                b、并发标记:进行GC Roots Tracing的过程

                c、最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象

                d、筛选回收:根据时间来进行价值最大化的回收

            ⑤、G1与CMS比较

                a、G1不会产生内存碎片,CMS会。

                b、可以精确控制停顿。该收集器把整个堆(新生代、老生代)划分为多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

            ⑥、配置参数

                a、-XX:UseG1GC 

                    启用G1收集器

                b、-XX:G1HeapRegionSize=n

                    设置区域的大小,值是2的幂,范围是1~32M,共2048块。

                c、-XX:MaxGCPausemillis=n

                    最大GC停顿时间,软目标,JVM尽可能但不保证,停顿小于这个时间。

        (9)、组合选择

            ①、单CPU或小内存,单机程序:-XX:+UseSerialGC

            ②、多CPU,需要最大吞吐量,如后台计算型应用:-XX:+UseParallelGC或者-XX:+UesParallelOldGC

            ③、多CPU,追求低停顿,需要快速响应:-XX:+UseConcMarkSweepGC、-XX:+UseParNewGC

    8、OOM

        (1)、java.lang.StackOverflowError

            每当启动一个新线程的时候,JVM都会为它分配一个java栈。JVM只会直接对java栈执行两种操作,以帧为单位的压栈和出栈。如果一个线程所需要的栈空间大于配置允许最大的栈空间,那么JVM就会抛出StackOverflow。一般出现这个问题是因为程序里有死循环或递归调用所产生的。

        (2)、java.lang.OutOfMemoryError: Java heap space

            在JVM中如果98%的时间是用于GC且可用的堆大小不足2%的时候将抛出异常信息java.lang.OutOfMemoryError: Java heap space。产生这个异样的原因通常有两种,程序中出现了死循环,或者程序占用内存太多,超过了JVM堆设置的最大值。

        (3)、java.lang.OutOfMemoryError: GC overhead limit exceeded

            Java进程花费98%以上的时间执行GC,并且每次回收不到2%的堆内存。连续多次GC都只回收了不到2%的极端情况下才会抛出。如果不抛出异常,那么GC清理出的内存会很快被填满,迫使再次执行GC,形成恶性循环,CPU使用率100%,但是GC却没有任何效果。

        (4)、java.lang.OutOfMemoryError: Direct buffer memory

            写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用native函数直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用来进行操作。这样能在一些场景中显著提供性能,因为避免了在Java堆和Native堆中来回复制数据。

            ByteBuffer.allocate(capability) :该方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。

            ByteBuffer.allocteDirect(capability):该方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。但是如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收。这时候堆内存充足,但是本地内存可能已经使用光了,再次尝试分配本地内存就会出现OOM。

        (5)、java.lang.OutOfMemoryError: unable to create new native thread(案例)

            高并发请求服务器时,经常会出现unable to create new native thread错误,准确的讲该错误与对应的运行平台有关。

            ①、原因:

                a、应用进程创建的线程数已经超过运行平台的限制,超过系统承载极限。

                b、运行的具体服务器不允许你的应用程序创建这么多线程,例如Linux系统默认单个进程可以创建的最大线程数是1024个,超过这个数量就会报此错误。

            ②、解决:

                a、降低应用程序创建线程的数量,分析系统代码是否真需要创建这么多线程。

                b、对于部分应用确实需要创建很多线程的情况,可以扩大服务器的默认线程数的限制,比如扩大Linux的默认限制数。

        (6)、java.lang.OutOfMemoryError: Metaspace

            Java 8及之后的版本使用Metaspace来替代永久代。Metaspace是方法区在HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存。Java 8之后永久代被Metaspace取代,主要用于存放以下信息:虚拟机加载的类信息、常量池、静态变量、即时编译后的代码。

    8、JProfiler

        (1)、连接JVM实例

            ①、Select from all local JVMs模式

                将扫描本地所有正在运行的JVM实例。

            ②、Attach to profiled JVM模式

                选择本地或远程正在运行的JVM实例,远程被监控的机器一定要预先安装JProfiler。

        (2)、实行方案

            ①、通过配置JVM参数 -XX:+HeapDumpOnOutOfMemoryError 当JVM发生OOM时,自动生成DUMP文件。

                    也可以通过配置 -XX:HeapDumpPath=${目录} 将DUMP文件下载到指定目录。

            ②、使用JProfiler打开dump文件进行分析。

                查看是否有大对象,以及大对象的数据。

            ③、查看threaddump,查看具体哪个线程操作的时候出了问题,可以精确到某一行代码。  

        (3)、文档教程

            官方帮助文档:JProfiler Help - Recording data

            Intellij IDEA集成JProfiler:Intellij IDEA集成JProfiler性能分析神器_苦中乐的博客-CSDN博客_idea jprofile

            JProfiler性能分析工具详解:https://www.jianshu.com/p/784c60d94989

    9、Arthas

        官方文档:快速入门 | arthas

        (1)、安装

            ①、curl -O https://arthas.aliyun.com/arthas-boot.jar

                执行命令下载Arthas

            ②、java -jar arthas-boot.jar

                运行Arthas,粘附到指定Java应用进程

        (2)、基础命令

            help——查看命令帮助信息

            cls——清空当前屏幕区域

            session——查看当前会话的信息

            reset——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类

            version——输出当前目标 Java 进程所加载的 Arthas 版本号

            history——打印命令历史

            quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响

            stop——关闭 Arthas 服务端,所有 Arthas 客户端全部退出

            keymap——Arthas快捷键列表及自定义快捷键

        (3)、常用命令

            命令列表:命令列表 | arthas

            ①、dashboard 查看仪表盘,按Q或Ctrl + C可以中断执行。

            ②、thread 获取该进程的所有线程

                thread 1 查看序号为1线程的内容

            ③、jad java.lang.Object 反编译Object类方便查看

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值