JVM-深入理解JVM内存模型、类加载机制、内存分配机制

一、类加载机制的过程

1.类加载过程

当我们使用java命令运行某个类的main函数启动程序时,首先需要类加载器把主类加载JVM中。

package com.sonny.classexercise.jvm;

/**
 * 类加载:将用户定义的类通过类加载器加载到JVM中
 *
 * @author Xionghaijun
 * @date 2022/9/25 20:21
 */
public class LoadUserClass {

    public static final int INIT_DATA = 2;

    static {
        System.out.println("加载 LoadUserClass 类静态方法");
        System.out.println("静态常量:" + INIT_DATA);
    }

    public static User user = User.builder().userId(1L).name("sonny").build();

    public static void main(String[] args) {
        System.out.println("类加载器测试:");
        LoadUserClass loadUserClass = new LoadUserClass();
        System.out.println(LoadUserClass.class.getClassLoader());
        int c = loadUserClass.calculate();
        System.out.println("计算结果:" + c);

        System.out.println(LoadUserClass.user.toString());

        System.out.println("类加载顺序测试:");
        new A();
    }

    private int calculate() {
        int a = 5;
        int b = 6;
        return (a + b) * INIT_DATA;
    }

    static class A {
        static {
            System.out.println("load static class A");
        }

        public A() {
            System.out.println("load noArgsConstructor A");
            new B();
        }
    }

    static class B {
        static {
            System.out.println("load static class B");
        }

        public B() {
            System.out.println("load noArgsConstructor B");
            C c = null;
        }
    }

    static class C {
        static {
            System.out.println("load static class C");
        }

        public C() {
            System.out.println("load noArgsConstructor C");
        }
    }
}

运行该段代码,查看大体流程,如图
JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第1张图片

其中loadClass的类加载过程分为:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

加载: 1.通过类型的完全限定名,产生一个代表该类型的二进制数据流(没有指明从哪里获取、怎么获取,是一个非常开放的平台),加载源包括:文件(Class文件,jar文件)、网络、计算生成(代理$Proxy)、由其它文件生成(jsp)、数据库中;2.将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构;3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证: 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成4个阶段的校验工作:文件格式验证、元数据验证、字节码验证、符号引用验证。若果运行的代码被反复使用和验证过,可以通过设置-Xverify:none参数关闭大部分的验证措施。

准备: 准备阶段正式为类变量分配内存并设置变量的初始值,这些变量使用的内存都应在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化是随着对象一起分配在java堆中。初始值通常是数据类型的零值;对于:public static int value=123,那么变量value在准备阶段过后的初始值为0而不是123,这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。对于:public static final int value =123;编译时javac将会为value生成ConstantValue(常量)属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

解析: 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化: 初始化阶段是执行类构造器()方法的过程,它是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

类被加载到方法区中后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。

类加载器的引用: 这个类到类加载器实例的引用 对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

tips:主类在运行过程中如果使用到其它类,会逐步加载这些类。 jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

2.类加载器

代码中的Jvm类加载过程使用到了类加载器,分为:

  • 启动类加载器(BootStrap ClassLoader): 这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。getClassLoader()方法返回null。
  • 扩展类加载器(Extension ClassLoader): 这个加载器有sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader): 这个类加载器由sun,misc.Laucher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以有些场合也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器(User ClassLoader): 继承ClassLoader,有两个核心方法:loadClass(实现双亲委派,加载父类的classLoader即appClassLoader),findclass(空方法,用户自己重写)。

类加载器示例:

package com.sonny.classexercise.jvm;

import com.sun.crypto.provider.DESKeyFactory;

/**
 * 查看JDK中类加载器
 *
 * @author Xionghaijun
 * @date 2022/9/25 20:46
 */
public class TestJDKClassLoader {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());

        System.out.println();
        System.out.println("查看系统类加载器");
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassLoader = appClassLoader.getParent();
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("classLoader is :" + appClassLoader);
        System.out.println("classLoader is :" + extClassLoader);
        System.out.println("classLoader is :" + bootstrapClassLoader);

    }
}

类加载器初始化过程: 参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应 用类加载器)。 JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们 的应用程序。

3.双亲委派机制

JVM类加载器是有亲子层级结构的,如下图:
JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第2张图片

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试这个加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

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();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } 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();
                    c = findClass(name);

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

双亲委派机制的好处:

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

4.自定类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。

package com.sonny.classexercise.jvm;

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

/**
 * 自定义类加载器
 *
 * @author Xionghaijun
 * @date 2022/9/25 21:32
 */
public class MyClassLoaderTest {

    static class MyClassLoader extends ClassLoader {
        private String classPath;

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

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

        @Override
        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 myClassLoader = new MyClassLoader("/Users/xionghaijun/TulingStudy");
            Class<?> clazz = myClassLoader.loadClass("com.jvm.User1");
            Object o = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(o, null);
            System.out.println(clazz.getClassLoader().getClass().getName());

        }
    }
}

//运行结果
打印方法
com.sonny.classexercise.jvm.MyClassLoaderTest$MyClassLoader

5.打破双亲委派

package com.sonny.classexercise.jvm;

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

/**
 * 自定义类加载器
 *
 * @author Xionghaijun
 * @date 2022/9/25 21:32
 */
public class MyClassLoadeBreakrTest {

    static class MyClassLoader extends ClassLoader {
        private String classPath;

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

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

        @Override
        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();
            }
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * 
         * @param name
         *         The binary name of the class
         *
         * @param resolve
         *         If {@code true} then resolve the class
         *
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        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) {
                    // If still not found, then invoke findClass in order to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }


        public static void main(String[] args) throws Exception {
            MyClassLoader myClassLoader = new MyClassLoader("/Users/xionghaijun/TulingStudy");
            Class<?> clazz = myClassLoader.loadClass("com.jvm.User1");
            Object o = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(o, null);
            System.out.println(clazz.getClassLoader().getClass().getName());

        }
    }
}

//运行结果:
java.lang.SecurityException: Prohibited package name: java.lang
 at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
 at java.lang.ClassLoader.defineClass(ClassLoader.java:758)

6.Tomcat打破双亲委派机制

Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程 序,那么要有10份相同的类库加载进虚拟机。
  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

Tomcat 如果使用默认的双亲委派类加载机制行不行?
不行。
一个Web应用可能需要部署过个应用程序,不同程序可能依赖同一个第三方类库的不同版本,不能要求同一个类库同个服务器中只有一份类库,要保证每个应用程序类库都是独立、隔离的;
部署在同个服务器中相同类库相同版本可共享;Web容器有自己依赖的类库,容器类库要与程序类库隔离;Web容易需支持Jsp修改,修改后不用重启服务。
JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第3张图片

二、JVM整体结构深度解析

1.JVM内存模型

JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第4张图片

2.JVM参数设置

Springboot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件):

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

-XX:MaxMetaspaceSize: 设置元空间最大值,默认是-1,即不限制,或只受限于本地内存大小。
-XX:MetaspaceSize: 指定触发Fullgc的初始阈值(元空间无固定初始大小),以字节为单位,默认21M,达到该值就会触发Fullgc,同时收集器会对该值进行调整:如果释放了大量空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(已设置情况下),适当提高该值。
调整元空间的大小需要FullGC,这是非常昂贵费时的操作,如果启动时发生大量FullGC,通常是由永久代或元空间发生了大小调整,基于此情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置的比初始值大,对于8G物理内存来说,一直设置成256M。

1)StackOverflowError

使用“ -Xss1M”设置栈空间大小默认值1M,值设置越小,一个线程里分配的栈帧就越少,但对JVM整体来说能开启的线程数会更多。

2)JVM参数设置

先看一个百万级订单交易系统图:
JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第5张图片
JVM参数设置:java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar microservice-eureka-server.jar

3)调优案例

JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第6张图片

请对此场景进行调优,让其几乎不发生Full GC
参数设置:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

结论: JVM优化尽可能让对象都在新生代里分配和回收,尽量别让太多的对象进入老年代,避免频繁对老年代进行垃圾回收,同时给系统足够的内存大小,避免新生代频繁进行垃圾回收。

三、JVM对象创建及内存分配机制深度解析

1.对象的创建过程

JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第7张图片
1.类加载检查: 虚拟机遇到一条new指令时,先去检查这个指令参数是否在常量池中定位到一个的类的符号引用,并且检查符号引用代表的类是否已被加载、解析、初始化。如果没有,则执行类加载过程。
2.分配内存: 虚拟机在Java堆中为新生对象分配内存。
划分内存方法:

  • 指针碰撞(默认):Java堆是绝对规整的,使用的内存在一边,空闲的在另一边,中间放着一个指针作为分界点的指示器,分配内存时将指针往空闲空间移动一段与对象大小相等的距离。
  • 空闲列表:Java堆不是规整的,需要维护一个列表,记录哪些内存块可用,分配时从列表中找到一块足够大的空间划分给对象实例,并更新到列表中。
    解决空间分配并发问题方法:
  • CAS(Compare And Swap):虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间操作进行同步处理。
  • 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):把分配内存的操作按照线程划分到不同的空间之中进行,即为每个线程在Java堆中预先分配一小块内存。
    3.初始化: 虚拟机分配的内存空间初始化为零值(不包括对象头),如果使用TLAB,这一过程可以提前。
    4.设置对象头: HotSpot虚拟机在内存中存储布局可分为:对象头,示例数据,对其填充。其中头像头包括两部分信息,第一部分用于存储对象自身运行时数据,如hash码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过该指针确定指向哪个实例。
    5.执行init()方法: 按编程者意图进行初始化。即为属性赋值,执行对象初始化构造方法。

2.内存分配和逃逸分析

通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,使用标量替换将对象分解成若干成员变量,在栈或寄存器上分配空间,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

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

3.对象内存回收

引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。(难以解决对象相互循环引用)
可达性分析法: 将GC Roots对象作为起点,根据引用关系向下搜索,所走过程称为引用链,一个对象到根节点无引用关系,则表示不可达。
JVM-深入理解JVM内存模型、类加载机制、内存分配机制_第8张图片

4.引用类型

1.强引用:类似 Object obj = new Object();只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
2.软引用:使用SoftReference实现,在系统将要发生内存溢出异常钱,会把这些对象列进回收范围中进行二次回收,若回收后还没足够内存,则抛出OOM。
3.弱引用:使用WeakReference实现,生存在下一次垃圾收集发生为止。
4.虚引用:使用PhantomReference实现,为了这个对象在被收集器回收时收到一个系统通知。

finalize()方法最终判断对象是否存活,即使当前对象不可达,回收该对象需要再次标记。

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

5.判断一个类是无用的类

需满足三个条件:

  • 该类的所有实例都被回收。
  • 该类的classLoader都被回收。
  • 该类对应的class对象在任何地方都没被引用,无法在任何地方通过反射类获取该类的方法。

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