JVM类加载机制、内存模型以及内存分配机制详解

一、类运行全过程

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

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

    public int complate(){ // 一个方法对应一块栈帧方法区
        int a = 1;
        int b = 2;
        int c = ( a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.complate();
    }
}

图上的左边都是C语言实现的,作为java程序员需要重点关注ClassLoad类加载过程。
类加载过程流程图
ClassLoad类加载过程大致分为以下几步:
1.加载:把类丢到内存里去加载;
2.验证:验证文件的格式(.class里面的格式);
3.准备:给静态变量赋默认值,分配内存空间;
4.解析:符号引用到直接引用(这个有点难理解,后面会继续补充),这里稍微提一下静态连接与动态连接。静态连接:将一些静态方法替换为所存内存的指针;动态连接:程序在运行期间将符号引用替换为直接引用。
5.初始化:对类的静态变量初始化为指定的值,执行静态代码块。
注意:主类在运行过程中如果使用到其他类,会逐步加载这些类,jar包或者war包里的类不是一次性全部加载的,是使用的时候才加载的。

二、类加载器

类加载器主要分为以下4种:
1.引导类加载器:在JRE下的lib目录下的核心类库,如rt.jar,charsets.jar等,如下图所示:
引导类加载器
2.扩展类加载器:在JRE下的lib目录下ext扩展目录中的JAR类包,如下图所示:
扩展类加载器
3.应用程序类加载器:加载ClassPath路径下的类包,主要是你自己写的那些类;
4.自定义加载器:加载用户自定义路径下的类包;

类加载器代码演示:

package com.tulingxueyuan.jvm;

import sun.misc.Launcher;

import java.net.URL;

/**
 * 类加载器
 */
public class TestJDKClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());// c++生成对象 不是java生成对象 结果为null 引导类加载器
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());// 扩展类加载器
        System.out.println(TestJDKClassLoader.class.getClassLoader());// 应用程序类加载器
        System.out.println();

        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the boorstrapLoader:"+ bootstrapLoader);
        System.out.println("the extClassloader:"+ extClassloader);
        System.out.println("the appClassLoader" + appClassLoader);

        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i <urLs.length ; i++) {
            System.out.println(urLs[i]);
        }

        System.out.println();
        System.out.println("extClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
}
-----------------------------------------------------------------
结果为:
"C:\Program Files\Java\jdk1.8.0_91\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=52366:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;E:\tulingWS\JDK\out\production\JDK" com.tulingxueyuan.jvm.TestJDKClassLoader
null
sun.misc.Launcher$ExtClassLoader@4b67cf4d
sun.misc.Launcher$AppClassLoader@18b4aac2

the boorstrapLoader:null
the extClassloader:sun.misc.Launcher$ExtClassLoader@4b67cf4d
the appClassLoadersun.misc.Launcher$AppClassLoader@18b4aac2

bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/classes

extClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;E:\tulingWS\JDK\out\production\JDK;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar

Process finished with exit code 0

自定义类加载器:
如何自定义一个类加载器呢?第一步继承java.lang.ClassLoader类,第二步重写findclass()方法;
代码演示:

package com.tulingxueyuan.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;
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        
        public static void main(String[] args) throws Exception {
            // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("E:/test");
            // E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
            Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
}
----------------------------------------------------------------------
    结果:
*************load User**********
========自己的加载器加载类方法=======
    com.tulingxueyuan.jvm.MyClassLoaderTest$MyClassLoader
没有User1.class文件时:$MyClassLoader
package com.tulingxueyuan.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;
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        
        public static void main(String[] args) throws Exception {
            // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("E:/test");
            // E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
            Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
}
----------------------------------------------------------------------
    *************load User**********
    ========自己的加载器加载类方法=======
    sun.misc.Launcher$AppClassLoaderUser1.class文件时:$AppClassLoader

三、双亲委派机制

3.1 什么是双亲委派机制

加载某个类时会先委托父加载器加载,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载路径下都无法加载,则在自己的类加载路径中查找并加载。
比如写的Math类,最先会找到应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器加载,顶层引导类加载器在自己的类加载路径里找了半天没有找到Math类,则向下退回加载的Math类的加载请求,扩展类加载器收到回复就自己加载,在自己类加载了半天也没有找到Math类,又向下退回Math类的加载请求给应用程序类加载器加载,应用程序类加载器于是在自己的类加载器找到了Math类,结果就自己加载了。
双亲委派机制简单来说就是,先找到父亲加载,不行再由儿子自己加载。
双亲委派机制

3.2 为什么要设计双亲委派机制

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

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("*******************My String Class*******************");
        // 不会加载成功 跟JDK自带的String一样  沙箱安全机制
    }
}
-----------------------------------------------------
    结果
    错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

3.3 打破双亲委派机制

package com.tulingxueyuan.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;
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new 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) {
                    long t0 = System.nanoTime();
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    if(!name.startsWith("com.tulingxueyuan.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;
            }
        }
        
        public static void main(String[] args) throws Exception {
            // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义加载器的父加载器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("E:/test");
            // E盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
            Class clazz = classLoader.loadClass("com.tulingxueyuan.jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj, null);
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
}
--------------------------------------------------------
*************load User**********
========自己的加载器加载类方法=======
com.tulingxueyuan.jvm.MyClassLoaderTest$MyClassLoader

四、JVM整体结构及内存模型

jvm整体结构及内存模型
堆:所有new出来的对象;
栈:局部变量 一个方法对应一个栈帧内存空间(方法结束了栈内存释放);
局部变量表:其中的数据可以理解为一个数组下标从零开始 0代表this、1代表1;
操作数据栈:程序在运行过程中需要操作的一块临时内存空间;
程序计数器:为了区分多线程并发情况下的线程号(名,可以代表那个线程);
动态链接:符号引用变成直接引用;
本地方法:有native修饰,大多都是C语言的方法;
gc root:线程栈的本地变量、静态变量、本地方法栈的变量等
对象头:主要由三部分组成,1.Mark Word标记字段(32位图);
2.Klass Pointer类型指针:堆里面的对象通过指针指向了方法区的类信息。如第二节课中jvm整体结构及内存模型中math对象指针指向了Math.class类信息;
3.数组长度(4字节 只有数组对象才有)
Minor GC:在年青代发生的gc(采用复制算法);
Full GC:在老年代发生的gc;
Eden与Survivor默认比例是:8:1:1;
方法区:常量+静态变量+类信息;
补充:
stw机制: 全称stop the world,停止用户线程;
jvm为什么要设置stw机制?
如果不设置stw机制的话,栈线程中的局部变量都被释放了,方法执行完毕,main栈帧的指针指不到堆中了。这样就会导致大量的对象没有了,重新创建太麻烦了。

五、JVM内存参数设置

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

方法区容量自动扩容机制:
ideal中jvm参数优化:
如果是一下配置,大概2万多次
在这里插入图片描述

package com.tulingxueyuan.jvm;

public class StackOverflowTest {
    // JVM设置
    // -Xss128k -Xss默认1M
    static int count = 0;

    public static void redo() {
        count++;
        redo();
    }

    public static void main(String[] args) {
        try {
           redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}
----------------------------------
com.tulingxueyuan.jvm.StackOverflowTest
java.lang.StackOverflowError
	at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
	at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
    24699

如果是以下配置:
在这里插入图片描述

package com.tulingxueyuan.jvm;

public class StackOverflowTest {
    // JVM设置
    // -Xss128k -Xss默认1M
    static int count = 0;

    public static void redo() {
        count++;
        redo();
    }

    public static void main(String[] args) {
        try {
           redo();
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println(count);
        }
    }
}
-----------------------------------------------
java.lang.StackOverflowError
	at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:9)
	at com.tulingxueyuan.jvm.StackOverflowTest.redo(StackOverflowTest.java:10)
    1101

结论:
-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。

六、内存分配机制详解

6.1 对象的创建

6.1.1 类加载检查

在这里插入图片描述

6.1.2 分配内存

1.指针碰撞(Bump the Pointer):有规整的将对象移动到指针(已使用内存与空闲内存分界线)指向的空闲空间(对象大小与空闲空间一样大),默认采用指针碰撞方式分配内存;
2.空闲列表(Free List):将不规整(已使用内存与空闲内存相互交错)的内存空间当做一个列表,如果新来对象的内存小于等于空闲列表,那么空闲列表可以存放新来的对象,否则就不能存放;
解决内存分配过程中的并发问题,CAS(compare and swap CAS底层原理在并发专题)利用CAS配上失败重试的方式保证原子性的操作同时保证内存分配同步处理。(如果第一个线程抢到内存,第二个线程不能抢到跟第一个线程同样的内存,第二个线程通过CAS加失败重试的方式抢到其他的内存)
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):为每个线程分配一个专属的内存空间大小(如果放不下就走CAS)。通过­XX:+/­UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),­XX:TLABSize 指定TLAB大小。

6.1.3 初始化

对成员变量赋零初值,跟静态变量初始化一样。

6.1.4 设置对象头

在这里插入图片描述
由对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头主要由三部分组成:
1.Mark Word标记字段(32位图)
2.Klass Pointer类型指针:堆里面的对象通过指针指向了方法区的类信息。如第二节课中jvm整体结构及内存模型中math对象指针指向了Math.class类信息;
3.数组长度(4字节 只有数组对象才有)
对象对齐是8的整数倍

6.1.5 执行方法

程序员为属性赋值(这里的赋值跟初始化赋零值不一样,这里赋实际的值)与执行构造方法。

6.2 对象大小与压缩指针

6.2.1 什么是java对象的指针压缩?

通过­XX:+UseCompressedOops参数可以把对象的内存压缩成4个字节(JDK1.6以后默认开始指针压缩);

6.2.2 为什么要进行压缩?

如果不进行指针压缩一个对象的内存就变成8个字节,堆内存很快就会占满,会触发gc(减少每个对象的内存大小);如果没有指针压缩一个对象内存是8个字节,进行指针压缩以后一个对象内存是4个字节,节约内存空间。

6.3 对象内存分配

6.3.1 对象栈上分配

对象逃逸分析:在方法中定义一个对象,这个对象被外部某个方法引用(例如作为参数调用到其他方法)。
将不会逃逸分析的对象先放入栈里,让方法结束时跟随栈内存一起被回收掉。JDK7之后默认开启逃逸分析(-XX:+DoEscapeAnalysis),如果要关闭使用参数(-XX:-DoEscapeAnalysis)。
代码演示:

public User test1(){// test1方法中的user对象 会被其他方法引用 User u = test1(); user对象逃逸test1()方法
        User user = new User();
        user.setName("周瑜");
        user.setId(1);
        return user;
}

public void test2(){// test2方法中user对象只能在test2方法里使用 没有逃逸
        User user = new User();
        user.setName("诸葛");
        user.setId(2);
}

标量替换:对于不会发生逃逸现象的对象,JVM会优先考虑将对象放到栈内存空间,JVM也不会创建(new)对象而是将对象的成员变量拆开放入到栈帧中去,也不用担心栈帧中没有连续的内存空间。

6.3.2 对象在Eden区分配

大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够内存空间进行分配时,将会出发一次Minor GC。
Minor GC:在年青代发生的gc(采用复制算法);
Full GC:在老年代发生的gc;
Eden与Survivor默认比例是:8:1:1

6.3.3 大对象直接进入老年代

大对象就是需要连续内存空间的对象,JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,这个参数只在 Serial 和ParNew两个收集器下有效(这两个参数会在垃圾收集器时详细讲),比如设置JVM参数 -XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC;
大对象为什么直接进入老年代:为年轻代节省更多的内存空间

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

虚拟机给每个对象一个对象年龄(Age)计数器,如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

6.3.5 对象动态年龄判断

在触发minor gc以后,如果一个对象内存大小超过Survivor区内存大小的50%,那么这个对象会被挪到老年代中去。例如Survivor区域里现在有一批对象,年龄1+年龄2+…+年龄n的多个对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代了。

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

在这里插入图片描述
在触发minor gc之前,如果老年代剩余可用空间小于年轻代里现有的对象(包括垃圾对象),
会看是否配置了这个参数-XX:HandlePromotionFailure”(jdk1.8默认就设置了)的参数,如果配置了参数老年代剩余可用空间小于历史每一次minor gc后进入老年代的对象的平均大小,就会触发full gc,最后再触发minor gc;如果没有配置-XX:HandlePromotionFailure这个参数,直接触发full gc,最后再触发minor gc;
在触发minor gc之前,如果老年代剩余可用空间大于等于年青代里现有的所有对象(包括垃圾对象),直接触发minor gc。
在触发minor gc之后,如果老年代剩余可用空间小于历史每一次minor gc后进入老年代的对象的平均大小,再次触发full gc(平均做一次minor gc 就会做两次full gc)

6.4 对象内存回收

6.4.1 引用计数器

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题。(一般不用)

6.4.2 可达性分析算法(内存泄漏)

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象,在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。
在这里插入图片描述

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

一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

public class OOMTest {
    public static List<Object> arrayList = new ArrayList<>();
    public static void main(String[] args) {
        List 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());// 此对象会被回收 仅触发一次finalize()
        }
    }
}
-----------------------------------------------------------
    收的对象
关闭资源,userid=-50132即将被回收的对象
关闭资源,userid=-50131即将被回收的对象
关闭资源,userid=-50130即将被回收的对象
......

6.4.5 如何判断一个类是无用的类

这里“无用的类”是指方法区里无用的类,需要满足以下3个条件:
1.该类的所有对象都会被回收,也就是java堆中不存在该类的任何实例;
2.加载该类的classLoader已经被回收(一般不容易会被回收,只有自定义的类加载器才会被回收)
3.该类的java.lang.class对象没有在任何地方被引用,无法在任何地方反射访问该类的方法。
很难同时满足以上3点,所以方法区里也回收不了什么对象。

总结

主要是对JVM运行全过程、类加器、双亲委派机制以及其原因与打破双亲委派机制,JVM内存模型以及内存模型分配详解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值