一文了解JVM所有知识点

类的加载过程

在这里插入图片描述
1)加载 “类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2)验证

连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

3)准备

该阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里所说的初始值“通常情况”下是数据类型的零值,下表列出了Java中所有基本数据类型的零值。

4)解析

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

5)初始化

到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

Java 虚拟机中有哪些类加载器?

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

这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

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

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

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

这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

什么是双亲委派模型?

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

为什么使用双亲委派模式?

防止系统中存在相同全限定名的不同类 (安全)

// 如果不是全限定名相同的话 也是可以跑的    
public static void main(String[] args) {
        SpringApplication.run(LinuxDemoApplication.class, args);
        com.shi.linux_demo.base.String string = new com.shi.linux_demo.base.String();
        System.out.println(string.age);
    }

有哪些场景破坏了双亲委派模型

SPI机制

要使用Java SPI,需要遵循如下约定:

  • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  • 2、接口实现类所在的jar包放在主程序的classpath中;
  • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • 4、SPI的实现类必须携带一个不带参数的构造方法;

示例代码:
步骤1、定义一组接口 (假设是org.foo.demo.IShout),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。

public interface IShout {
    void shout();
}
public class Cat implements IShout {
    @Override
    public void shout() {
        System.out.println("miao miao");
    }
}
public class Dog implements IShout {
    @Override
    public void shout() {
        System.out.println("wang wang");
    }
}

步骤2、在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。
文件位置

- src
    -main
        -resources
            - META-INF
                - services
                    - org.foo.demo.IShout

文件内容

org.foo.demo.animal.Dog
org.foo.demo.animal.Cat

步骤3、使用 ServiceLoader 来加载配置文件中指定的实现。

public class SPIMain {
    public static void main(String[] args) {
        ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
        for (IShout s : shouts) {
            s.shout();
        }
    }
}

代码输出:

wang wang
miao miao

自定义类加载器破坏双亲委派机制

java demo

1.创建一个类 并使用javac 来编译它

2.继承ClassLoader 重写findClass方法 在类内添加缓存 ,只要是本类加载器加载的类 都进行一个缓存,下次再碰到相同类的时候 直接从缓存中取出。依次调用loadclassData方法获取class二进制文件,然后再调用defineClass 生成calss.

需要注意的是,自定义类加载器中需要保证不同的类加载器加载同一个类的字节码数据时,得到的字节码数组必须相等,否则会出现类重复定义的问题。因此,可以在 loadClassData 方法中加入缓存机制,避免同一个类被重复加载,保证字节码数据的一致性。

public class MyClass110 {
    public String name = "wang";
}

public class MyClassLoader extends ClassLoader {
    private final String path;
    private final Map<String, Class<?>> loadedClasses;

    public MyClassLoader(String path) {
        this.path = path == null ? "/src/main/java/" : path;
        loadedClasses = new HashMap<>();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (loadedClasses.containsKey(name)) {
            return loadedClasses.get(name);
        }

        if (name.startsWith("org.example")) {
            byte[] data = loadClassData(name);
            Class<?> c = defineClass(name, data, 0, data.length);
            loadedClasses.put(name, c);
            return c;
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] loadClassData(String className) {
        String fileName = System.getProperty("user.dir") + path + className.replace('.', '/') + ".class";
        System.out.println("Loading class from file: " + fileName);
        try (InputStream inputStream = new FileInputStream(fileName);
             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, length);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

public class MyClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader myClassLoader = new MyClassLoader(null);
        Class<?> aClass = myClassLoader.loadClass("org.example.myclassload.path.MyClass110");
        Object o = aClass.newInstance();
        System.out.println(((MyClass110) o).name);
    }
}
CustomClassLoader customClassLoader = new CustomClassLoader("path/to/classes");
Class<?> clazz = customClassLoader.loadClass("com.example.demo.MyClass");
Object instance = clazz.newInstance();

线程上下文类加载器破坏双亲委派机制

在双亲委派模型中,一个类的加载会首先委派给其父类加载器,如果父类加载器无法加载,则由自身的类加载器进行加载。但是在某些情况下,这种双亲委派模型可能会导致一些问题。比如,当两个不同的模块(例如,不同的Web应用程序)中都使用了同一个类的不同版本时,由于这些模块使用了不同的类加载器,双亲委派模型会将请求委派到公共父级,导致两个模块都使用同一个版本的类,而无法加载各自的版本。

如果使用相同的类加载器加载同一个类的不同版本,那么第二个被加载的类会直接使用第一个被加载的类的class,这可能会导致类的版本不一致,出现各种问题。

public class VersionedClassExample {
    public static void main(String[] args) throws Exception {
        // 加载版本1的类
        loadVersion("v1");
        
        // 使用上下文类加载器加载版本2的类
        Thread.currentThread().setContextClassLoader(new Version2ClassLoader());
        loadVersion("v2");
    }
    
    private static void loadVersion(String version) throws Exception {
        // 根据版本创建对应的类加载器
        ClassLoader classLoader = new VersionClassLoader(version);
        
        // 使用类加载器加载类
        Class<?> clazz = classLoader.loadClass("com.example.VersionedClass");
        
        // 打印类信息
        System.out.println("Version: " + version);
        System.out.println("Class: " + clazz.getName());
        System.out.println("Classloader: " + clazz.getClassLoader());
        System.out.println("Object: " + clazz.newInstance());
        System.out.println();
    }
}

// 版本1的类加载器,从 classpath 下的 v1 目录加载类
class Version1ClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        if ("com.example.VersionedClass".equals(name)) {
            String path = "classpath:v1/com/example/VersionedClass.class";
            byte[] bytes = loadBytesFromPath(path);
            return defineClass(name, bytes, 0, bytes.length);
        }
        return super.findClass(name);
    }
}

// 版本2的类加载器,从 classpath 下的 v2 目录加载类
class Version2ClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        if ("com.example.VersionedClass".equals(name)) {
            String path = "classpath:v2/com/example/VersionedClass.class";
            byte[] bytes = loadBytesFromPath(path);
            return defineClass(name, bytes, 0, bytes.length);
        }
        return super.findClass(name);
    }
}

// 版本通用的类加载器,从指定的版本目录下加载类
class VersionClassLoader extends ClassLoader {
    private final String version;
    
    public VersionClassLoader(String version) {
        this.version = version;
    }
    
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "classpath:" + version + "/com/example/VersionedClass.class";
        byte[] bytes = loadBytesFromPath(path);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

// 版本通用的类
class VersionedClass {
    private final String version;
    
    public VersionedClass() {
        this.version = "unknown";
    }
    
    public VersionedClass(String version) {
        this.version = version;
    }
    
    @Override
    public String toString() {
        return "VersionedClass [version=" + version + "]";
    }
}

运行时数据区

在这里插入图片描述
java 虚拟机定义了若干种在程序执行期间会使用到的运行时数据区域。

其中一些数据区域在 Java 虚拟机启动时被创建,随着虚拟机退出而销毁。也就是线程间共享的区域:堆、方法区、运行时常量池。

另外一些数据区域是按线程划分的,这些数据区域在线程创建时创建,在线程退出时销毁。也就是线程间隔离的区域:程序计数器、Java虚拟机栈、本地方法栈。

1)程序计数器(Program Counter Register)

Java 虚拟机可以支持多个线程同时执行,每个线程都有自己的程序计数器。在任何时刻,每个线程都只会执行一个方法的代码,这个方法称为该线程的当前方法(current method)。

如果线程正在执行的是 Java 方法(不是 native 的),则程序计数器记录的是正在执行的 Java 虚拟机字节码指令的地址。如果正在执行的是本地(native)方法,那么计数器的值是空的(undefined)。

2)Java虚拟机栈(Java Virtual Machine Stacks)

每个 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,它与线程同时创建,用于存储栈帧。

Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

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

3)本地方法栈(Native Method Stacks)

本地方法栈与 Java 虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是 Java 虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。

4)(Heap)

堆是被各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

堆在虚拟机启动时创建,堆存储的对象不会被显示释放,而是由垃圾收集器进行统一管理和回收。

5)方法区(Method Area)

方法区是被各个线程共享的运行时内存区域。方法区类似于传统语言的编译代码的存储区。它存储了每一个类的结构信息,例如:运行时常量池、字段和方法数据,构造函数和普通方法的字节码内容,还包括一些用于类、实例、接口初始化用到的特殊方法。

6)运行时常量池(Run-Time Constant Pool)

运行时常量池是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式。

它包含了若干种常量,从编译时已知的数值字面量到必须在运行时解析后才能获得的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表(symbol table),不过它包含的数据范围比通常意义上的符号表要更为广泛。

java中常用的常量池

现在我们经常提到的常量池主要有三种:class 文件常量池、运行时常量池、字符串常量池。

在Java6和6之前,常量池一般是存放在方法区中的,到了Java7,常量池就被存放到了堆中,Java8之后,就取消了整个永久代区域,取而代之的是元空间。在运行时常量池与静态常量池都会存放在元空间中,但[字符串]常量池依然存放在堆中。

class模板类 存放在哪里?

在Java虚拟机中,Class模板类(类的元数据,即类的定义信息)存放在方法区(Method Area)。方法区是Java虚拟机的一部分,用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。对于Java 8及之前的版本,方法区通常被称为永久代(PermGen),而在Java 8中,永久代被移除,取而代之的是元空间(Metaspace)。尽管实现方式有所不同,但它们都用于存储Class模板类。

元空间

元空间在 Java 8 移除永久代后被引入,用来代替永久代,本质和永久代类似,都是对方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存(native memory)。

元空间主要用于存储 Class metadata(类元数据),根据其命名其实也看得出来。
可以通过 -XX:MaxMetaspaceSize 参数来限制元空间的大小,如果没有设置该参数,则元空间默认限制为机器内存。

为什么引入元空间?

在 Java 8 之前,Java 虚拟机使用永久代来存放类元信息,通过-XX:PermSize、-XX:MaxPermSize 来控制这块内存的大小,随着动态类加载的情况越来越多,这块内存变得不太可控,到底设置多大合适是每个开发者要考虑的问题。

如果设置小了,容易出现内存溢出;如果设置大了,又有点浪费,尽管不会实质分配这么大的物理内存。

而元空间可以较好的解决内存设置多大的问题:当我们没有指定 -XX:MaxMetaspaceSize 时,元空间可以动态的调整使用的内存大小,以容纳不断增加的类。

元空间能彻底解决内存溢出(Out Of Memory)问题吗?

很遗憾,答案是不行的。

元空间无法彻底解决内存溢出的问题,只能说是有所缓解。当内存使用完毕后,元空间一样会出现内存溢出的情况,最典型的场景就是出现了内存泄漏时。

怎么判定对象已经“死去”?

常见的判定方法有两种:引用计数法和可达性分析算法,HotSpot中采用的是可达性分析算法。

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象

GC Root有哪些?

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

垃圾收集有哪些算法,各自的特点?

标记 - 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

标记 - 整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

问题排查

先top 一下 看看java 占用内存大小

在 top -Hp PID 查看线程 的子线程 那个占用内存高 记录一下子线程的PID 转成 16进制备用

在 jstack PID >out.log 输出主进程当前的堆栈线程在做什么 使用子PID 进行搜索

这个时候 还是找不到原因的话 dump出文件来查看 命令:jmap -dump:format=b,file=heap.dump PID

垃圾回收器

新生代:serial、ParNew、Parallel
老年代:Serial Old、Parallel Old、CMS
全堆:G1

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程可以分为6个步骤,包括:初始标记、并发标记、预处理、重新标记、并发清除、重置。
CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
一般搭配parNew 复制算法的并行收集器

Parallel java8默认的垃圾回收算法

parallel scavenge 复制算法的并行收集器
parallel old 标记-整理算法

G1

垃圾回收算法是一种用于 Java 虚拟机(JVM)的高性能、低延迟的垃圾回收器。G1 垃圾回收器旨在替代 CMS(Concurrent Mark Sweep)回收器,以满足大内存、多核心 CPU 环境下的应用需求。G1 垃圾回收器的主要特点是可以设置停顿时间目标,从而实现可预测的停顿时间。G1 垃圾回收算法的工作原理如下:

  1. 初始标记(Initial Marking):此阶段是 STW(Stop-The-World)事件,即在执行初始标记时,所有的应用线程都会暂停。初始标记的目的是标记所有的 GC 根对象,以及与这些根对象直接关联的对象。
  2. 并发标记(Concurrent Marking):在此阶段,G1 垃圾回收器将与应用线程并发运行,不会导致应用线程暂停。并发标记阶段的目的是遍历整个对象图,找到所有存活的对象。在此过程中,G1 会跟踪并发标记过程中新创建的对象,并确保这些对象被正确标记。
  3. 最终标记(Final Marking):此阶段也是一个 STW 事件。最终标记阶段的目的是处理在并发标记阶段由于应用线程运行而产生的剩余工作,例如处理 WeakReferences、Finalizers 等。此阶段还会完成计算存活对象的工作。
  4. 复制/清除(Evacuation):此阶段也是一个 STW 事件。在此阶段,G1 垃圾回收器将执行以下操作:
    - 年轻代收集(Young GC):清除 Eden 区和 Survivor 区,将存活的对象复制到另一个 Survivor 区或 Old 区。
    - 混合收集(Mixed GC):在并发标记阶段完成后,G1 垃圾回收器将执
    行混合收集。混合收集会清除 Eden 区、Survivor 区以及部分 Old 区。G1 会根据回收价值选择 Old 区域进行回收,以在有限的停顿时间内最大化回收效果。
  5. 并发清除(Concurrent Cleanup):在混合收集完成后,G1 垃圾回收器将执行并发清除阶段。在此阶段,G1 会回收已清空的区域,并将它们添加到可用区域的列表中。这个阶段与应用线程并发执行,不会导致应用线程暂停。

G1 垃圾回收器通过这些阶段实现高效的内存回收,同时尽量减少应用线程的暂停时间。在调优 G1 垃圾回收器时,可以通过设置停顿时间目标、堆大小等参数来平衡吞吐量和停顿时间。

新生代到老年代的转换

1.担保机制

新生代内存不足的时候 通过担保分配的方式让大对象直接分配到老年代

2.大对象直接进入老年代

-XX:PretenureSizeThreshold=对象大小(单位:byte)

通过配置 让大对象直接进入老年代

3.新生代长期存活的方式进入老年代

4.动态对象年龄判断:s0 或者 s1空间中 相同年龄的所有对象的和大于s区空间的一半 则大于此年龄的所有对象可以直接进入老年代

相关推荐

  1. Python教程:了解Python的异常处理知识

    2024-04-04 22:52:01       40 阅读
  2. JVM知识

    2024-04-04 22:52:01       58 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-04 22:52:01       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-04 22:52:01       106 阅读
  3. 在Django里面运行非项目文件

    2024-04-04 22:52:01       87 阅读
  4. Python语言-面向对象

    2024-04-04 22:52:01       96 阅读

热门阅读

  1. 每天定时杀spark进程

    2024-04-04 22:52:01       33 阅读
  2. 算法——验证二叉树的前序序列化

    2024-04-04 22:52:01       39 阅读
  3. API 接口类型有哪些:入门指南

    2024-04-04 22:52:01       33 阅读
  4. Vue3 自定义指令Custom Directives

    2024-04-04 22:52:01       41 阅读
  5. php身份证实名认证接口、社交平台实名制

    2024-04-04 22:52:01       39 阅读
  6. C++内存池

    2024-04-04 22:52:01       37 阅读
  7. Vue3 & Vite 整合组件脚手架笔记

    2024-04-04 22:52:01       39 阅读
  8. 速盾:怎么通过cdn防御ddos

    2024-04-04 22:52:01       32 阅读
  9. 为什么资讯网站选择高防IP防护攻击

    2024-04-04 22:52:01       39 阅读