JVM类加载器及其详解

1.JVM加载过程分析

1.1加载流程图

JVM类加载器及其详解_第1张图片

1.2双亲委派机制

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包
    JVM类加载器及其详解_第2张图片
    这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
    比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。。

1.为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("*******my String class*****");
    }
}

JVM类加载器及其详解_第3张图片
JVM类加载器及其详解_第4张图片
说明:在引导类加载这个路径这个方法的时候,已经找到这个这个,会直接调取引导类加载器的方法,不会加载自己定义的方法。

2.双亲委派机制示例

package com.tuling.jvm;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    }

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

1.当在程序中有user1这个实体类的时候
JVM类加载器及其详解_第5张图片
运行结果
JVM类加载器及其详解_第6张图片
2.当在程序中有user1这个实体类的时候
JVM类加载器及其详解_第7张图片
运行结果
JVM类加载器及其详解_第8张图片

3.打破双亲委派机制

package com.tuling.jvm;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest2 {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                       long t0 = System.nanoTime();
                        long t1 = System.nanoTime();

                        if(!name.startsWith("com.tuling.jvm")){
                            c=this.getParent().loadClass(name);
                        }else{
                            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;
            }
        }


        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    }

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

1.相关文件截图
JVM类加载器及其详解_第9张图片
2.运行结果
JVM类加载器及其详解_第10张图片

2.JVM运行原理及其详解

JVM类加载器及其详解_第11张图片

2.1以实际为例,讲解JVM的运行原理

package com.tuling.jvm;
public class Math {
    public static final int initData=666;
    public static User user=new User();

    public int compute(){
        int a=1;
        int b=2;
        int c=(a+b)*10;
        return c;
    }

    public static void main(String[] args) {
        Math math=new Math();
        math.compute();
        System.out.println("123");
    }
}

  • 1.系统初始化的时候,会把public,static,void,main进初始化。

  • 2.main方法加载的时候会开辟一个栈(线程),储放相关的栈帧和程序计数器。(程序计数器的作用表示多线程程序在运行中,此程序被其他更高级的程序抢占后,能够在下一次回到已经执行的代码的位置)
    JVM类加载器及其详解_第12张图片

  • 3.以compute方法为例,在栈帧中分别局部变量表、操作数栈、动态连接、方法出口。
    图一
    JVM类加载器及其详解_第13张图片
    图二
    JVM类加载器及其详解_第14张图片
    图三
    JVM类加载器及其详解_第15张图片
    图四
    JVM类加载器及其详解_第16张图片
    图五
    JVM类加载器及其详解_第17张图片
    图六
    JVM类加载器及其详解_第18张图片
    图七
    JVM类加载器及其详解_第19张图片
    图八
    JVM类加载器及其详解_第20张图片
    图九
    JVM类加载器及其详解_第21张图片
    图十
    JVM类加载器及其详解_第22张图片
    说明:当执行到compute方法时,此时会开辟一个compute的栈帧空间,同时在compute的栈帧空间里面开辟一个‘局部变量表’的空间存放a变量,接着会把1压入到操作数栈中,然后从操作数栈中把1存入局部变量表中(图1,图2);接着在‘局部变量表’存放b变量,接着会把2压入到操作数栈中,然后从操作数栈中把2存入局部变量表中(图3,图4);然后取出1和2,在cpu做加加法得3,把cpu计算得的值放入操作数栈,接着把10压入操作数栈,然后把值取出3和10,在CPU做乘法,最后得到30,把30压入操作数栈,最后返回。(图五,图六,图七,图八,图九,图十)

  • 3.通过方法出口返回到main方法中

  • 4.main方法局部变量表存math局部变量,此时会有对应堆里面Math对象地址。

  • 5.本地方法栈存放的是c++写的本非方法,方法为native的方法。

  • 6.方法区存放的是常量(initData),静态变量(user),类型信息

  • 7.堆里面存放的是年轻代和老年代,年轻代占1/3,老年代占2/3,年轻代分为Eden区,S0区和S1区,比例为8:1:1,当Eden区会不断存取对象数据,当数据满了,此次会进行一次minor gc,会把Eden进行一次垃圾回收,不能回收的垃圾放入S0中;接着在Eden区不断放入对象,当Eden区满了,再进行一次minor gc,会对Eden和S0进行一次垃圾回收,然后把存在的对象放入S1,此时当代年龄加1;接着在Eden区不断放入对象,当Eden区满了,再进行一次minor gc,会对Eden和S1进行一次垃圾回收,然后把存在的对象放入S0;当当代年龄为15时,此次会把存活15代的放入老年代,同时minor gc的时候,当S0或者S1内存满时,也会把对象直接放入老年代,当老年代满了时,系统将会进行一次full gc,回收整个堆里面的对象,当回收之后还是满的,将会进行OOM。

2.2堆和栈内存配置

JVM类加载器及其详解_第23张图片
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):

java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar microservice‐eurek
a‐server.jar

关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N

  1. -XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
  2. -XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发
    full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超
    过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-
  3. XX:PermSize代表永久代的初始容量。
    由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

3.JVM对象创建与内存分配机制深度剖析

3.1对象加载的过程

JVM类加载器及其详解_第24张图片

  • 1.类加载检查

  • 2.分配内存

  • 3.初始化

  • 4.设置对象头(HotSpot虚拟机中内存分配)
    JVM类加载器及其详解_第25张图片

  • 5.执行方法

3.2 对象大小与指针压缩

对象大小可以用jol-core包查看,引入依赖

 <dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
 </dependency>

运行时代码

package com.tuling.jvm;

import org.openjdk.jol.info.ClassLayout;

public class JOLSample {

    public static void main(String[] args) {
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());

        System.out.println();
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
    }

    // -XX:+UseCompressedOops           默认开启的压缩所有指针
    // -XX:+UseCompressedClassPointers  默认开启的压缩对象头里的类型指针Klass Pointer
    // Oops : Ordinary Object Pointers
    public static class A {
        //8B mark word
        //4B Klass Pointer   如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
        int id;        //4B
        String name;   //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
        byte b;        //1B
        Object o;      //4B  如果关闭压缩-XX:-UseCompressedOops,则占用8B
    }

}


##运行结果
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
//mark word
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1) 
//mark word      
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
//Klass Pointer      
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
 //mark word
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
//mark word 
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
//Klass Pointer           
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.tuling.jvm.JOLSample$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           63 cc 00 f8 (01100011 11001100 00000000 11111000) (-134165405)
     12     4                int A.id                                      0
     16     1               byte A.b                                       0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String A.name                                    null
     24     4   java.lang.Object A.o                                       null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total


Process finished with exit code 0

禁止使用压缩指针运行结果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           00 1c 6a 1c (00000000 00011100 01101010 00011100) (476716032)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           68 0b 6a 1c (01101000 00001011 01101010 00011100) (476711784)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     20     4        (alignment/padding gap)                  
     24     0    int [I.<elements>                             N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total


com.tuling.jvm.JOLSample$A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           f8 cd d3 1c (11111000 11001101 11010011 00011100) (483642872)
     12     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4                int A.id                                      0
     20     1               byte A.b                                       0
     21     3                    (alignment/padding gap)                  
     24     8   java.lang.String A.name                                    null
     32     8   java.lang.Object A.o                                       null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total


Process finished with exit code 0

什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed–压缩、oop(ordinary object pointer)–对象指针
3.启用指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops

为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好

关于对齐填充:对于大部分处理器,对象以8字节整数倍来对齐填充都是最高效的存取方式。

3.3对象内存分配

对象内存分配流程图
JVM类加载器及其详解_第26张图片
我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

**对象逃逸分析:**就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

public User test1() {
   User user = new User();
   user.setId(1);
   user.setName("zhuge");
   //TODO 保存到数据库
   return user;
}

public void test2() {
   User user = new User();
   user.setId(1);
   user.setName("zhuge");
   //TODO 保存到数据库
}

很显然test1方法中的user对象被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内存一起被回收掉。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
栈上分配示例:

/**
 * 栈上分配,标量替换
 * 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
 * 
 * 使用如下参数不会发生GC
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * 使用如下参数都会发生大量GC
 * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 */
public class AllotOnStack {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void alloc() {
        User user = new User();
        user.setId(1);
        user.setName("zhuge");
    }
}

结论:栈上分配依赖于逃逸分析和标量替换

3.4对象在Eden区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。
在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  • Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
    Eden与Survivor区默认8:1:1
    大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可,
    JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
//添加运行JVM参数: -XX:+PrintGCDetails
public class GCTest {
   public static void main(String[] args) throws InterruptedException {
      byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
      allocation1 = new byte[60000*1024];

      //allocation2 = new byte[8000*1024];

      /*allocation3 = new byte[1000*1024];
     allocation4 = new byte[1000*1024];
     allocation5 = new byte[1000*1024];
     allocation6 = new byte[1000*1024];*/
   }
}

##运行结果
Connected to the target VM, address: '127.0.0.1:51385', transport: 'socket'
Heap
 PSYoungGen      total 75776K, used 65024K [0x000000076be00000, 0x0000000771280000, 0x00000007c0000000)
  eden space 65024K, 100% used [0x000000076be00000,0x000000076fd80000,0x000000076fd80000)
  from space 10752K, 0% used [0x0000000770800000,0x0000000770800000,0x0000000771280000)
  to   space 10752K, 0% used [0x000000076fd80000,0x000000076fd80000,0x0000000770800000)
 ParOldGen       total 173568K, used 0K [0x00000006c3a00000, 0x00000006ce380000, 0x000000076be00000)
  object space 173568K, 0% used [0x00000006c3a00000,0x00000006c3a00000,0x00000006ce380000)
 Metaspace       used 3026K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 322K, capacity 392K, committed 512K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:51385', transport: 'socket'

Process finished with exit code 0

我们可以看出eden区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用至少几M内存)。假如我们再为allocation2分配内存会出现什么情况呢?

//添加运行JVM参数: -XX:+PrintGCDetails
public class GCTest {
   public static void main(String[] args) throws InterruptedException {
      byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
      allocation1 = new byte[60000*1024];

      allocation2 = new byte[8000*1024];

      /*allocation3 = new byte[1000*1024];
      allocation4 = new byte[1000*1024];
      allocation5 = new byte[1000*1024];
      allocation6 = new byte[1000*1024];*/
   }
}

##运行结果
[GC (Allocation Failure) [PSYoungGen: 63913K->991K(75776K)] 63913K->60991K(249344K), 0.0319632 secs] [Times: user=0.11 sys=0.03, real=0.03 secs] 
Heap
 PSYoungGen      total 75776K, used 10942K [0x000000076be00000, 0x0000000775200000, 0x00000007c0000000)
  eden space 65024K, 15% used [0x000000076be00000,0x000000076c7b7c78,0x000000076fd80000)
  from space 10752K, 9% used [0x000000076fd80000,0x000000076fe77cb0,0x0000000770800000)
  to   space 10752K, 0% used [0x0000000774780000,0x0000000774780000,0x0000000775200000)
 ParOldGen       total 173568K, used 60000K [0x00000006c3a00000, 0x00000006ce380000, 0x000000076be00000)
  object space 173568K, 34% used [0x00000006c3a00000,0x00000006c7498010,0x00000006ce380000)
 Metaspace       used 3027K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 322K, capacity 392K, committed 512K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:51552', transport: 'socket'

Process finished with exit code 0

简单解释一下为什么会出现这种情况: 因为给allocation2分配内存的时候eden区内存几乎已经被分配完了,我们刚刚讲了当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,GC期间虚拟机又发现allocation1无法存入Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,所以不会出现Full GC。执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。可以执行如下代码验证:

package com.tuling.jvm;

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;
        allocation1 = new byte[60000*1024];

        allocation2 = new byte[8000*1024];

      allocation3 = new byte[1000*1024];
     allocation4 = new byte[1000*1024];
     allocation5 = new byte[1000*1024];
     allocation6 = new byte[1000*1024];
    }
}

##运行结果
[GC (Allocation Failure) [PSYoungGen: 65024K->872K(75776K)] 65024K->60880K(249344K), 0.0255158 secs] [Times: user=0.30 sys=0.02, real=0.03 secs] 
Heap
 PSYoungGen      total 75776K, used 13785K [0x000000076be00000, 0x0000000775200000, 0x00000007c0000000)
  eden space 65024K, 19% used [0x000000076be00000,0x000000076ca9c770,0x000000076fd80000)
  from space 10752K, 8% used [0x000000076fd80000,0x000000076fe5a020,0x0000000770800000)
  to   space 10752K, 0% used [0x0000000774780000,0x0000000774780000,0x0000000775200000)
 ParOldGen       total 173568K, used 60008K [0x00000006c3a00000, 0x00000006ce380000, 0x000000076be00000)
  object space 173568K, 34% used [0x00000006c3a00000,0x00000006c749a010,0x00000006ce380000)
 Metaspace       used 3215K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

3.4大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代
为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率。

3.5长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置

3.6对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。

3.7老年代空间分配担保机制

年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”
JVM类加载器及其详解_第27张图片

3.8可达性分析算法

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
JVM类加载器及其详解_第28张图片

3.9常见引用类型

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
**强引用:**普通的变量引用

public static User user = new User();

**软引用:**将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

public static SoftReference<User> user = new SoftReference<User>(new User());

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

public static WeakReference<User> user = new WeakReference<User>(new User());

**虚引用:**虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

3.10 finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

    1. 第一次标记并进行一次筛选。
      筛选的条件是此对象是否有必要执行finalize()方法。
      当对象没有覆盖finalize方法,对象将直接被回收。
    1. 第二次标记
      如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
      注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
      示例代码:
public class OOMTest {

   public static void main(String[] args) {
      List<Object> list = new ArrayList<>();
      int i = 0;
      int j = 0;
      while (true) {
         list.add(new User(i++, UUID.randomUUID().toString()));
         new User(j--, UUID.randomUUID().toString());
      }
   }
}

//User类需要重写finalize方法


@Override
protected void finalize() throws Throwable {
    OOMTest.list.add(this);
    System.out.println("关闭资源,userid=" + id + "即将被回收");
}

finalize()方法的运行代价高昂, 不确定性大, 无法保证各个对象的调用顺序, 如今已被官方明确声明为不推荐使用的语法。 有些资料描述它适合做“关闭外部资源”之类的清理性工作, 这完全是对finalize()方法用途的一种自我安慰。 finalize()能做的所有工作, 使用try-finally或者其他方式都可以做得更好、更及时, 所以建议大家完全可以忘掉Java语言里面的这个方法。

4.JVM垃圾收集算法和垃圾收集器

4.1图片整理

JVM类加载器及其详解_第29张图片

4.2 案例配置

JVM类加载器及其详解_第30张图片
对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8

JVM类加载器及其详解_第31张图片

-Xms堆的初始容量
-Xmx堆的最大容量
-Xss线程栈的容量
XX:SurvivorRatio Eden占整个年轻代比例为8/10
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8

由于动态对象年龄判断原则导致频繁full gc。
于是我们可以更新下JVM参数设置:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 

-Xmn配置年轻代说占用的内存(Eden,S0,S1)
JVM类加载器及其详解_第32张图片
对于对象年龄应该为多少才移动到老年代比较合适,本例中一次minor gc要间隔二三十秒,大多数对象一般在几秒内就会变为垃圾,完全可以将默认的15岁改小一点,比如改为5,那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了,如果对象这么长时间都没被回收,完全可以认为这些对象是会存活的比较长的对象,可以移动到老年代,而不是继续一直占用survivor区空间。
对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统看下有没有什么大对象生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的对象。

可以适当调整JVM参数如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M 

-XX:MaxTenuringThreshold 表示年轻代经历五代就会进去老年代
-XX:PretenureSizeThreshold表示对象超过1M表示大对象,直接进入老年代
对于JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,只是经验值),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)对于老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会长期存活躲过5次以上minor gc最终进入老年代。
无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其量也就几十MB。
还有就是某次minor gc完了之后还有超过一两百M的对象存活,那么就会直接进入老年代,比如突然某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧增,一个订单要好几秒才能处理完,下一秒可能又有很多订单过来。
我们可以估算下大概每隔五六分钟出现一次这样的情况,那么大概半小时到一小时之间就可能因为老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制,历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了抢购的最高峰期,后续可能几小时才做一次FullGC。
对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理,或者两到三次之后再做一次也行。
综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 
-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3

-XX:+UseParNewGC年轻代使用ParNew垃圾收集
XX:+UseConcMarkSweepGC 老年代使用CMS垃圾收集
-XX:CMSInitiatingOccupancyFraction老年代超过92%做full GC
-XX:+UseCMSCompactAtFullCollection full GC之后做碎片整理
XX:CMSFullGCsBeforeCompaction表示三次full GC做一次碎片整理

4.3 三色标记

JVM类加载器及其详解_第33张图片

4.3.1多标-浮动垃圾

在并发标记的过程当中,程序还未执行完,对象已经标记完成,此时对象引用的对象都是均是非垃圾对象,当程序运行完成后,对象本应该是垃圾对象,但是标记为非垃圾对象,会在下一次标记的时候重新标记为垃圾对象,下次过程root会清理。
JVM类加载器及其详解_第34张图片

4.3.2漏标

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。
JVM类加载器及其详解_第35张图片
增量更新:当黑色对象插入白色对象的引用时,将这个新插入引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以理解为,黑色对象一旦新插入了指向白色引用的对象,它就变为灰色对象了
原始快照:灰色对象要删除指向白色对象的引用关系,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色为根,重新扫描一次,这样就能扫描到白色对象了,将白色对象直接标记为黑色。(目的就是让这种对象在本轮GC清理中存活下来,待下一轮gc的时候重新扫描,这个对象可能是浮动垃圾。)

4.3.3写屏障

1.给某个对象的成员变量赋值时,其底层代码大概长这样:

/**
* @param field 某对象的成员变量,如 a.b.d 
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操作
} 

所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念)

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field);          // 写屏障-写前操作
    *field = new_value; 
    post_write_barrier(field, value);  // 写屏障-写后操作
}


2.写屏障实现SATB

void pre_write_barrier(oop* field) {
    oop old_value = *field;    // 获取旧值
    remark_set.add(old_value); // 记录原来的引用对象
}

3.写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来:

void post_write_barrier(oop* field, oop new_value) {  
    remark_set.add(new_value);  // 记录新引用的对象
}

你可能感兴趣的:(jvm,java,开发语言)