深入理解JVM

概述:

一、JVM类加载机制

1.1 Java运行时一个类是什么时候被加载的?

一个类在什么时候开始被加载,《Java虚拟机规范》中并没有进行强制约束,交给了虚拟机自己去自由实现,HotSpot虚拟机是按需加载,在需要用到该类的时候加载这个类;

1、Sun公司最早的 Classic虚拟机;

2、Sun/Oracle公司的HotSpot虚拟机;

3、BEA公司的JRockit虚拟机;

4、IBM公司的IBM J9虚拟机;

官方:https://docs.oracle.com/javase/8/

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:+TraceClassLoading

测试类的详细载入:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2 JVM一个类的加载过程?

一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:

1、加载(Loading)

2、验证(Verification)

3、准备(Preparation)

4、解析(Resolution)

5、初始化(Initialization)

6、使用(Using)

7、卸载(Unloading)

其中验证、准备、解析三个阶段统称为连接(Linking);
在这里插入图片描述
加载: classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;

验证: 验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全;

准备: 类变量赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值;

解析: 把符号引用翻译为直接引用;

初始化: 当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化… 那么这些都会触发类的初始化;

使用: 使用这个类;

卸载:

1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

2.加载该类的ClassLoader已经被GC; (ClassLoader:类加载器,用于加载class)

3.该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;

准备阶段:

​ 对于final常量来说,准备阶段直接赋值a为123,对于类变量,是在类初始化(new)的时候才会进行赋值,所以先赋0,而对于实例变量abc来说,也是先赋0

public class Test01 { // java.lang.Class

    //常量
    public static final int a = 123;

    //类变量
    public static int b = 222;//0

    public static String str = "sdfsdf"; //null

    //实例变量
    public int abc;

    // -XX:+TraceClassLoading 监控类的加载
    public static void main(String[] args) {
        User user = new User();
        user.working();
        System.out.println(b);

        Order order = new Order();
    }
}

解析阶段:

把一个类的class文件读进来之后,变成java.lang.class对象,在元空间里面

1.3一个类被初始化的过程?☆☆☆

在这里插入图片描述
类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码

进行准备阶段时,变量已经赋过一次系统要求的初始零值,

而在初始化阶段,才真正初始化类变量和其他资源;

public class Test02 {

    // 静态常量  ==准备
    public static final String staticConstantField = "静态常量";

    // 静态变量 ==准备阶段赋值为 null,初始化阶段赋值为 静态变量
    public static String staticField = "静态变量";

    // 变量 == 创建对象的时候赋值
    public String field = "变量";

    // 静态初始化块 ==初始化阶段执行
    static {
        System.out.println(staticConstantField);
        System.out.println(staticField);
        System.out.println("静态初始化块");
    }

    // 初始化块 == 创建对象的时候执行
    {
        System.out.println(field);
        System.out.println("初始化块");
    }

    // 构造器 == 创建对象的时候执行
    public Test02() {
        System.out.println("构造器");
    }

    // java Test02
    public static void main(String[] args) {
        new Test02();

        // 1、rt.jar charset.jar
        // 2、InitialOrderTest
    }
}

验证初始化:

  1. 当main方法中new对象时,执行空:
    在这里插入图片描述
    此时会执行静态代码块中的语句,同时静态常量、静态变量都得到了初始化,所以main方法执行后,会初始化Test02类中的静态常量、静态变量、静态代码块

  2. main中new Test02对象时:
    在这里插入图片描述
    同样也会先初始化Test02中的静态常量、静态变量、静态代码块,和第一个验证一致,其次再去初始化类的成员变量、代码块、构造方法

1.4 继承时父子类的初始化顺序是怎样的?

父类–静态变量

父类–静态初始化块

子类–静态变量

子类–静态初始化块

父类–变量

父类–初始化块

父类–构造器

子类–变量

子类–初始化块

子类–构造器

父类:

public class ParentClass {

    // 静态变量
    public static String p_StaticField = "父类--静态变量";

    // 变量
    public String p_Field = "父类--变量";

    protected int i = 0;
    protected int j = 0;

    // 静态初始化块
    static {
        System.out.println(p_StaticField);
        System.out.println("父类--静态初始化块");
    }

    // 初始化块
    {
        System.out.println(p_Field);
        System.out.println("父类--初始化块");
    }

    // 构造器
    public ParentClass() {
        System.out.print("父类--构造器");
        System.out.println("i=" + i + ", j=" + j);
        i = 1; j = 1;
    }
}

子类:

public class ChildClass extends ParentClass {

    // 静态变量
    public static String s_StaticField = "子类--静态变量";

    // 变量
    public String s_Field = "子类--变量";

    // 静态初始化块
    static {
        System.out.println(s_StaticField);
        System.out.println("子类--静态初始化块");
    }

    // 初始化块
    {
        System.out.println(s_Field);
        System.out.println("子类--初始化块");
    }

    // 构造器
    public ChildClass() {
        System.out.print("子类--构造器");
        System.out.println("i=" + i + ",j=" + j);
    }

    // 程序入口 java ChildClass
    public static void main(String[] args) {
        new ChildClass();
    }
}

测试验证:

  1. main方法中不执行任何语句:
    在这里插入图片描述
    当在子类的main中执行时,会先初始化父类的静态变量+静态代码块,然后再初始化子类的静态变量+静态代码块

  2. main方法中new子类对象时:
    在这里插入图片描述
    前面的依旧,先初始化父类子类的静态变量、静态代码块,然后由于new了子类,所以还会先执行父类的成员变量、代码块、构造器,再执行子类的成员变量、代码块、构造器。

  3. 如果在子类main中new父类对象
    在这里插入图片描述
    结果可想而知,由于是子类的main,会先初始化父类子类静态变量、静态代码块,然后由于new了父类,所以还会初始化父类的成员变量、代码块、构造器

1.5 究竟什么是类加载器?

在这里插入图片描述
在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”被称为“类加载器”(Class Loader),这个 动作是可以自定义实现 的;

类加载器可以由C++、Java语言实现

简单来说,就是把.class文件以二进制形式读进来

1.6 JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:

1、启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分;

2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;

站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;
在这里插入图片描述

1.7 JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的,在java中通过getClassLoader方法是获取不到的

<JAVA_HOME>\jre\lib\rt.jar,resources.jar、charsets.jar

被-Xbootclasspath参数所指定的路径中存放的类库;

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


sun.misc.Launcher$ExtClassLoader,

<JAVA_HOME>\jre\lib\ext,

被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器

sun.misc.Launcher$AppClassLoader

加载用户类路径(ClassPath)上所有的类库;

测试验证:

比如在

  • <JAVA_HOME>\jre\lib\rt.jar下找到一个类BufferReader类,查看其classLoader,为空,由于是c++语言编写的,所以Java中获取不到,是正常的;

    输出为null则表明为根的类加载器

在这里插入图片描述

  • AppClassLoader,包括自己写的代码以及项目中涉及到的第三方jar包(如Springboot)
    在这里插入图片描述
    继承结构:
    在这里插入图片描述

1.8 JVM三层类加载器之间的关系是继承吗?

在这里插入图片描述
不是继承关系。

解释:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
换个角度思考这个问题,前面提到过BootstrapClassLoader是C++语言实现的,ExtClassLoader是Java实现的,怎么可能继承呢?

如果我们自定义一个类加载器,那么则是继承共同的父类ClassLoader(抽象类)

1.9 JVM类加载的双亲委派模型 ☆☆☆

在这里插入图片描述
官方描述:

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

过程分析:

如:我要加载String这个类,来自rt.jar,首先让App ClassLoader去加载,但它自己不加载,委派给上一层加载,ExtClassLoader也不去加载,再继续委派给Bootstrap ClassLoader,再往上就没有了,那么此时BootstrapClassLoader就要去尝试加载,它主要是加载jre下内部库的jar包,而String这个类正好在rt.jar中,所以就return了,从而把String这个类加载到JVM内存中了。

当BootstrapClassLoader加载内库后,找不到所要加载的类,那么就让第二层ExtClassLoader去尝试加载,找得到就return,找不到就继续让AppClassLoader去加载后找。

当这个类几个类加载器都找不到时,报ClassNotFoundException

解释一下为啥叫双亲:因为AppClassLoader上面有两层,所以叫双亲

总结:先自底向上委派,再自顶向下去尝试加载

1.10 JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;

2、避免重复加载;

3、保证类的唯一性;

如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

解释:

比如即使自己写了个String类,也不会被加载,因为最先会加载rt.jar下的String,能够避免核心内库被修改,要保证安全。
在这里插入图片描述
在这里插入图片描述
自定义的包理论上是最终被APPClassLoader去加载的,但是这里为啥会报错,是因为包和核心内库重名了,所以报安全异常

1.11 可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?

可以;

想要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

1.12 如何自定义自己的类加载器?

1、继承ClassLoader
2、覆盖findClass(String name)方法 或者 loadClass() 方法;

findClass(String name)方法 不会打破双亲委派;

loadClass() 方法 可以打破双亲委派(如何去加载可以自己去实现);

import java.io.IOException;
import java.io.InputStream;

/**
 * 自定义ClassLoader
 *
 */
public class MyClassLoader extends ClassLoader {

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
    //public Class<?> loadClass(String name) throws ClassNotFoundException {
        //name = 包名 + 类名
        try {
            // User.class
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                throw new ClassNotFoundException(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);

            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}

测试:

public class Test04 {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader();

        Class clazz = myClassLoader.findClass("com.bjpowernode.loader.User");
        System.out.println(clazz.getClassLoader());
        Object instance = clazz.newInstance();
        System.out.println(instance);

        System.out.println(User.class.getClassLoader());
        Class claz2 = User.class;
        Object o2 = claz2.newInstance();
        System.out.println(o2);
    }
}

本来com.bjpowernode.loader.User应该去AppClassLoader中加载的,但由于自定义了myClassLoader,并调用了findClass方法,所以会使用自定义的方法去加载。

在这里插入图片描述

1.13 ClassLoader中的loadClass()、findClass()、defineClass()区别?

loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;

findClass() 根据名称或位置加载.class字节码;

definclass() 把字节码转化为java.lang.Class;

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;

因为覆盖即打破双亲委派,所以建议改findClass

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

经典案例:Tomcat就打破了双亲委派机制

1.14 加载一个类采用Class.forName()和ClassLoader有什么区别

在这里插入图片描述

public class Test05 {

	public static void main(String[] args) throws IOException {
		 Class.forName("com.jvm.demo.loader.ChildClass")
       
     Class clazz = Test05.class.getClassLoader().loadClass("com.jvm.demo.loader.ChildClass")
	}

}

在这里插入图片描述
在这里插入图片描述

分析区别:
由此可见,ClassLoader这种方式,只经过了加载->链接,但是没有初始化,但是forName方式会初始化
如果继续对类实例化,则会初始化,并会创建对象

public class Test05 {

	public static void main(String[] args) throws IOException {
		  
     Class clazz = Test05.class.getClassLoader().loadClass("com.jvm.demo.loader.ChildClass");
    clazz.newInstance();
	}

}

在这里插入图片描述
看看两个方法底层实现:

forName底层会初始化类:

调用了一个本地C++方法
在这里插入图片描述
ClassLoader底层:

双亲委派类加载,没有对类进行初始化
在这里插入图片描述

1.15 了解Tomcat 的类加载机制

在这里插入图片描述
可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:

common.loader=" c a t a l i n a . b a s e / l i b " , " {catalina.base}/lib"," catalina.base/lib","{catalina.base}/lib/.jar"," c a t a l i n a . h o m e / l i b " , " {catalina.home}/lib"," catalina.home/lib","{catalina.home}/lib/.jar"
server.loader=
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

1.16 有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境;

热部署 是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;

1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;

2、热部署是在运行时重新部署整个项目,耗时相对较高;

如何实现热加载呢?

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;

重点步骤

1、实现自己的类加载器;

2、从自己的类加载器中加载要热加载的类;

3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

类加载器

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义一个类加载器
 *
 * bootstrap ClassLoader jdk/jre/目录下的jar包加载
 *
 * ext ClassLoader  jdk/ext/目录下的jar包加载
 *
 * App ClassLoader --我们应用的ClassLoader
 *
 */
public class MyClassLoader extends ClassLoader {

    private File classPathFile;

    private static Map<String, Class> clazzCache = new ConcurrentHashMap<>();//缓存 保存加载的类

    public MyClassLoader() {
        String classPath = MyClassLoader.class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return this.findClass(name, false);
    }

    protected Class<?> findClass(String name, boolean force) throws ClassNotFoundException {
        //force是否强制加载

        //之前有没有加载过这个class
        Class cls = clazzCache.get(name);
        if (force) {
            //如果强制加载 则缓存值值为空,后面重新加载一遍
            cls = null;
        }
        String className = MyClassLoader.class.getPackage().getName() + "." + name;

        //如果这个cls不为空则直接return了
        if (cls == null && classPathFile != null) {
            File classFile = new File(classPathFile + "\\" + name.replaceAll("\\.", "/") + ".class");
            if (classFile.exists()) {
                FileInputStream fis = null;
                ByteArrayOutputStream bos = null;
                try {
                    fis = new FileInputStream(classFile);
                    byte[] bytes = new byte[4096];
                    bos = new ByteArrayOutputStream();
                    int len;
                    while ((len = fis.read(bytes)) != -1) {
                        bos.write(bytes, 0, len);
                    }
                    cls = defineClass(className, bos.toByteArray(), 0, bos.size());
                    clazzCache.put(name, cls);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if(fis != null) {
                            fis.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    try {
                        if(bos != null) {
                            bos.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return clazzCache.get(name);
    }
}

写一个定时任务去轮询:

观察狗 线程:

思路:遍历文件,去拿到每个文件的时间,判断是否被修改,如果修改则重新触发加载类(自定义类加载器)

import java.io.File;
import java.util.Map;

/**
 * 观察狗
 *
 */
public class WatchDog implements Runnable {

    private Map<String, FileDefine> fileDefineMap;

    public WatchDog(Map<String, FileDefine> fileDefineMap) {
        this.fileDefineMap = fileDefineMap;
    }

    @Override
    public void run() {
        File file = new File(this.getClass().getResource("").getPath());
        File[] files = file.listFiles();
        for (File watchFile : files) {
            long newTime = watchFile.lastModified();
            FileDefine fileDefine = fileDefineMap.get(watchFile.getPath());
            long oldTime = fileDefine.getLastDefine();
            //如果文件被修改了,那么重新生成累加载器加载新文件
            if (newTime != oldTime) {
                System.out.println("文件被修改......");
                fileDefine.setLastDefine(newTime);
                //重新触发类加载
                loadMyClass(watchFile.getName());
            }
        }
    }

    public void loadMyClass(String className) {
        try {
            MyClassLoader myClassLoader = new MyClassLoader();
            myClassLoader.findClass(className.replace(".class", ""), true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 测试运行
 */
public class Test {

    public static void main(String[] args) throws Exception {
        Map<String, FileDefine> fileDefineMap = new ConcurrentHashMap<>();

        File file = new File(Test.class.getResource("").getPath());
        File[] files = file.listFiles();
        for (File watchFile : files) {
            FileDefine fileDefine = new FileDefine();
            fileDefine.setLastDefine(watchFile.lastModified());
            fileDefineMap.put(watchFile.getPath(), fileDefine);
        }

        //定时任务
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleAtFixedRate(new WatchDog(fileDefineMap), 3, 3, TimeUnit.SECONDS);

        MyClassLoader w = new MyClassLoader();
        while (true) {
            Class clazz = w.findClass("MyLog");
            try {
                ILog myLog = (ILog) clazz.newInstance();
                myLog.log();
                Thread.sleep(2000);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试其中一个修改类:

public class MyLog implements ILog {

    @Override
    public void log() {
        System.out.println("log, version 1.0");}
}

此处修改 System.out.println(“log, version 1.0”);为 System.out.println(“log, version 2.0”); 文件发生修改,但是要编译一下,点击build->Recompile '‘MyLog.java’,那么轮询时就会发现文件的修改

在这里插入图片描述

二、深入剖析JVM内存管理

2.1 Java代码到底是如何运行起来的?

1、Mall.java -->javac --> Mall.class --> java Mall (jvm进程,也就是一个jvm虚拟机)

2、Mall.java -->javac–>Mall.class -->Mall.jar --> java -jar Mall.jar

3、Mall.java --> javac --> Mall.class -->Mall.war --> Tomcat --> startup.sh --> org.apache.catalina.startup.Bootstrap (jvm进程,也就是一个jvm虚拟机)

其实运行起来一个Java程序,都是通过D:\dev\Java\jdk1.8.0_251\bin\java 启动一个JVM虚拟机,在虚拟机里面运行Mall.class字节码文件;

在这里插入图片描述

总结:

java源文件通过javac命令转成java字节码文件,再通过java命令运行起来,JVM是用于屏蔽掉底层操作系统之间的差异,这里不同操作系统所装载的jdk是不同的,jdk中包含jvm

这里面的其他语言,如Groovy、Scala、Kotlin也是编译后为字节码,再通过JVM虚拟机处理。

2.1画一下JVM整个运行原理图?☆

在这里插入图片描述

2.1 请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息;hotspot

在这里插入图片描述
在JDK1.8之前,元空间就是原来的方法区(永久代);

比如new User(),那么User这个变量在虚拟机栈中存储为局部变量,但是其值是在堆中存储的

程序计数器:表明代码执行到哪一行

当类加载后,Class文件首先会存储在元空间中,然后运行时才会涉及其他几个空间部分

2.2 JVM哪些区域是线程私有的,哪些区域是线程共享的?☆

1、堆、元空间(方法区)是线程共享的;

2、其他区域是线程私有的;

在这里插入图片描述
线程私有的区域如虚拟机栈、本地方法栈、程序计数器,这些就不存在线程安全的问题,每个线程之间这些区域是隔离的,不共享变量,就不会导致冲突,没有安全问题

举例:两个线程对应就有两个虚拟机栈,一一对应。无论有几个线程,堆和元空间都是公用的一个

2.3 从JVM角度剖析如下程序代码如何执行?

在这里插入图片描述
在这里插入图片描述

Config类在new的时候,对象存在堆中,该类的信息是在元空间里的

2.4 JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;

2、是当前线程所执行的字节码的行号指示器;

3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;

4、该区域是“线程私有”的内存,每个线程独立存储;

5、该区域不存在OutOfMemoryError;

6、无GC回收;

总结:程序计数器是在线程产生的时候存在,在线程结束(销毁时候)消失,不需要垃圾回收(GC)

2.5 JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;

2、方法执行会创建栈帧,存储局部变量表等信息;

3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)

4、==【深度过长】==栈深度大于虚拟机所允许StackOverflowError;

5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;

6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;

7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;

8、随线程而生,随线程而灭;

9、该区域不会有GC回收;

解释:OutOfMemoryError一般少见,比如写个死循环不断的创建线程,当创建到一定程度,无法再继续给空间到新线程时,每个线程都会生成一个线程栈,每个线程栈会占用一定的内存区域,从而导致超出,会把会报此错误。

如下,死循环中不断创建新的线程,而每个线程又不会结束,即不会销毁,从而导致大量内存占用。

在这里插入图片描述

解释:递归调用时,有时会出现StackOverflowError,如下案例:

在这里插入图片描述
输出结果:

解释栈大小设置:

一般1M足够大了,往往会将其设置小一点,在idea中添加JVM配置,默认是1M,也可以改为128k:

在这里插入图片描述
改了栈大小之后,原来的栈深度35710就会减小,再报Stack Overflow:

相当于把栈变小了,压的栈就会相应变少。

在这里插入图片描述

总结:实际项目中,一般也不会递归调用太多次,1M的话,调用个3w多次肯定是够用的!所以改小一点是有好处的,改小一点反而可以启动更多线程,比如原本10M的内存空间可以创建10个1M的线程,那么把栈大小改小后,线程所占用的内存空间就变小了,从而使得线程数量变多

2.8 JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;

2、区别在于本地方法栈为Native方法服务;

3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;

4、有StackOverflowError和OutOfMemoryError(比较少见);

5、随线程而生,随线程而灭;

6、GC不会回收该区域;

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

解释:本地方法栈和虚拟机栈类似,只不过存储的是Native方法,也就是底层由C++写的方法;而HotSpot是把虚拟机栈和本地方法栈合并到一起了!

在这里插入图片描述

2.9 JVM运行时数据区 Java堆的特点及作用?☆

1、线程共享的一块区域;

2、虚拟机启动时创建;

3、虚拟机所管理的内存中最大的一块区域

4、存放所有实例对象或数组

5、GC垃圾收集器的主要管理区域;

6、可分为新生代、老年代;

7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1

8、可通过==-Xmx、-Xms==调节堆大小;

9、无法再扩展java.lang.OutOfMemoryError: Java heap space

10、【TLAB】 如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

解释:堆的年代划分

在这里插入图片描述

解释:堆内存不足了,就会OOM,java.lang.OutOfMemoryError: Java heap space 堆溢出

在这里插入图片描述
输出结果:

在这里插入图片描述
如果改一下堆大小:
在这里插入图片描述
在这里插入图片描述

解释:TLAB:

因为堆是共享区域,所以当多个线程往堆里面放内容时,会产生一个竞争关系、冲突问题,那么最朴素的想法就是用锁去解决这个线程安全问题,但是用锁呢,又会导致效率比较低,所以干脆给每个线程一个默认区域,区域不大,但是供每个线程各自去放内容,也就是给每个线程分配了一个较小的缓冲区,当把缓冲区放满后,再去公共区放数据!

2.10 JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下;

2、空闲列表(Free List):内存不规整的情况下;

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;

因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;

而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;

那么解决方案有两种:

(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;

(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;

-XX:TLABSize=512k 设置大小;

解释:指针碰撞:

当内存排列规整的时候,指针不断向右遍历,依次排列对象

在这里插入图片描述

空闲列表:
和指针碰撞不同,该方式的内存排列不规整,有一个空闲列表去记录哪些区域是空闲的,那么当存储时去找对应的空闲内存进行存储

在这里插入图片描述

解释:具体用哪种方式,这个是根据垃圾回收器的空,间压缩整理能力来决定的,如果GC每次回收完后,将空内存排列的规整,那么就用指针碰撞

一些JVM可以配置的参数:
在这里插入图片描述

2.11 JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:

对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding);

  • 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;
    在这里插入图片描述
    第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;

  • 实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

  • 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;

2.12 JVM什么情况下会发生堆内存溢出?☆

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径

来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

MAT工具分析xxx.hprof文件(相当于一个内存log),排查溢出的原因;

添加JVM参数 输出hprof文件:

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/dev/heapdump.hprof

在这里插入图片描述
在这里插入图片描述
利用Eclipse的MAT工具来分析文件:

官网即可下载,open file,生成怀疑报告Leak suspects

在这里插入图片描述

-Xms3072M

-Xmx3072M

-Xmn1536M

-Xss1M

-XX:-UseCompressedClassPointers

-XX:MetaspaceSize=256M

-XX:MaxMetaspaceSize=256M

-XX:SurvivorRatio=8

-XX:MaxTenuringThreshold=5

-XX:PretenureSizeThreshold=1M

-XX:+PrintGCDetails

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:/dev/heapdump.hprof

在这里插入图片描述

解释:Java堆中用于储存对象,只要不断地创建对象,并且保持==GC Roots==到对象之间有可达路径

比如这里的orderList是一个GC root根,然后引用了new ArrayList()这个List对象地址,然后这个List对象又在循环中,不断的去引用到了order对象,由于对象一直在创建,所以就能保持GC Roots到对象之间有可达路径!

如果这个List对象引用断了,那么就会将剩余的进行垃圾回收,从而不会导致堆内存溢出,也就是说,该List对象不再被引用了,就可以销毁了(被GC回收),从而就不会占用堆内存了

在这里插入图片描述

验证猜想:

如果将order的引用注释掉:

这样一来,虽然orderList还是指向了堆内存中的List对象,但是List对象没有去引用新生成的order对象,导致order对象由于没有被引用,就会被GC回收,这样就不会导致堆内存溢出了.
在这里插入图片描述
可以通过visualVM软件来可视化堆内存中对象的变化过程:

可以看出黄色的是每一个对象新生和销毁的过程,顶峰代表出生,下降到0代表销毁,所以就可以一直跑,不会堆溢出.
在这里插入图片描述
对比溢出的情况:
在这里插入图片描述
New区域不断的增加,到顶后,Old再累积

2.13 JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;

Java通过 ==可达性分析(Reachability Analysis)==算法 来判定对象是否存活的;

该算法的基本思路:通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;
在这里插入图片描述
对象object 5、object 6、object 7虽然有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象;

哪些对象可以作为GC Roots呢?

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;

2、方法区/元空间中的类静态属性引用的对象;

3、方法区/元空间中的常量引用的对象;

4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;

5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如

NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;

6、所有被同步锁(synchronized关键字)持有的对象;

7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;

8、其他可能临时性加入的对象;

总结:一般我们最常见的就是虚拟机栈中的局部变量引用的对象、临时变量,作为GC Root。

2.14 谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;

强引用:Object object = new Object();

软引用:SoftReference 内存充足时不回收,内存不足时则回收;

弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;

虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

解释:

强引用【大多用】,因为有变量指向对象,只要引用没有释放,即便是内存不足、溢出了,也不能回收这个引用

软引用【缓存用】,要看内存足不足,弱引用,一般用于缓存领域(内存充足就缓存起来,内存不足就清空缓存,比如mybatis中就用到过)

弱引用【很少用】:比如类库ThreadLocal用到过

虚引用【很少用】

代码示例:

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

public class References {

    public static void main(String[] args) {
      //强引用
        Object object = new Object();
			
      //软引用
        SoftReference softReference = new SoftReference(object);
			
      //弱引用
        WeakReference weakReference = new WeakReference(object);
    }
}

2.15 JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;

大部分对象朝生夕死,少数对象长期存活;

在这里插入图片描述
From Survivor区也叫S0区,To Survivor 也叫S1区

大小也可以通过参数去调整

2.16 请介绍一下JVM堆中新生代的垃圾回收过程?☆☆

在这里插入图片描述
JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代)

不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收

代码里创建出来的对象,一般就是两种:

1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;

2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;

第一种短期存活的对象,是在Java堆内存的新生代里分配;

第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;

java -XX:+PrintFlagsFinal 打印jvm默认参数值;

总结:垃圾回收针对的是堆和元空间

过程详解:

新创建的对象先进Eden空间,满了后,再就触发Minor GC就会去回收Eden区域里面的对象,这里面有些需要存活、有些需要回收,先把要存活的移到From Survivor区域,把要回收的对象清除掉。
在这里插入图片描述
第二轮:当Eden区域第二次再满的时候,这时又要出发Minor GC,这时候处理回收的就是Eden和S0两个区域的对象了

先将不能回收的对象移到S1区域,把要回收的对象清空掉,此时Eden区域空闲了。
在这里插入图片描述
然后再处理S0区域中的对象,把要回收的清除,要存活的移到S1区域。
在这里插入图片描述
在这里插入图片描述
第三轮:当Eden区域再次满载状态时,再次触发Minor GC,这时处理的就是已满的Eden以及S1区域,先处理Eden:将回收的清除,需要存活的移至S0,针对S1区域做同理操作。
在这里插入图片描述
在这里插入图片描述
然后再回收S1区域的对象,其中不能回收的移到S0区域,要回收的清除掉。
在这里插入图片描述如此,循环往复,已存在于S0、S1的对象经过Minor GC后,要继续存活的就移到另一个区域(S0 or S1),要回收的清除即可,要回收的对象就会在S0、S1两个区域中间来回移动,当移动的次数大于15次后,那之后就放进老年代,表示这个对象是需要长期存活的对象。

在中间每次清理S0、S1后,每次处理后都会有一个是空闲状态的,下一次把需要存活的移到空闲的区域

年龄阈值

关于年龄阈值,如果是并行GC,默认值是15,如果是CMS垃圾处理器的,就是6,详细可以看官方文档:

一般不指定垃圾处理器,那就是并行的,所以默认15次

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

在这里插入图片描述
可以执行命令来看JVM的默认参数:

java -XX:+PrintFlagsFinal 打印jvm默认参数值;

2.17 JVM对象动态年龄判断是怎么回事?☆☆☆

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;

结论
动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;

1、Survivor区分布如下图:

这里是三轮后的的一个分布结果
在这里插入图片描述

解释:也就是还有45%的对象还在存活,只要大于50%,才会触发动态年龄判断

2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
这时从1岁加到4岁时,总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升

解释:动态年龄判断是在大于50%后触发,然后针对的是50%以后的大年龄对象,也就是4岁以后的,5岁 6岁…此处没有

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:
在这里插入图片描述
Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代

总结:并不是严格的到了15岁,就会到达老年代,也有动态年龄判断机制!

2.18 什么是老年代空间分配担保机制

在这里插入图片描述

Eden:800m -->300m

S0:100m

S1:100m

老年代:1000m,剩350m、200m

新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?

1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;

2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;

3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;

所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC

4、如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;

在JDK6的时候有一个参数-XX:+HandlePromotionFailure用于开启是否要进行空间担保;

解释:

简单来说,就是往老年代放对象的时候,这个空间够不够,有一个担保机制。

当Eden新生代区域装满对象后,触发Minor GC,这里的核心在于做一个判断:

判断老年代的可用空间能不能大于新生代的对象大小总和,(这里相当于假设新生代对象全部都不可回收 并且S0 S1也不够,那么就得往老年代里面放,如果老年代可以全覆盖了,说明可以直接Minor GC,是没有OOM风险的);如果老年代空间不足以覆盖全部新生代对象,那么就再作第二个判断:看历史往老年代放对象的平均大小和老年代的可用空间做比较(这里相当于是个评估,用历史平均去预算一下,但是也可能这一次比历史平均的都大,那么最终还得Full GC,Full GC指的是对老年代做回收,如果仍然不够则OOM),这样的目的还是为了避免Full GC

在这里插入图片描述
核心关键:
避免频繁的Full GC,所以每次判断都是先尽量走Minor GC,实在不行再Full GC。

2.19 什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;

2、动态对象年龄判断;

3、老年代空间担保机制;

4、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起**高额的内存复制开销,**为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;

-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;

解释:对于大对象来说,一般大概率是不会被回收的,那么如果在S0和S1区域来回的移动,这样的内存开销是很大的,为了避免这一个巨大开销,就直接将过大的对象直接放入到老年代中存储

2.20 JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;

2、元空间与Java堆类似,是线程共享的内存区域;

3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;

6、元空间内存不足时,将抛出OutOfMemoryError;

解释:

jdk1.8前都叫方法区,目前都叫元空间,这个区域和堆是并列的,也属于线程共享的内存区域,其存储的主要是类的信息,静态变量、常量等等一系列静态的量,比如下图,类加载后,类的信息就会存储在元空间中;

元空间的内存是不需要垃圾回收的,能回收的信息比较少

当元空间内存不足时候也会报OOM

元空间的大小占用的是本地内存,所以本地内存还剩多少,就可以给它扩展到多少,一般来说都是绝对足够的

在这里插入图片描述
测试元空间溢出

这里采用了动态代理方式,来不断的创建类到元空间中

死循环,这里当超出了内存大小后就会溢出,如果不设置参数的话,就是根据机器剩余的内存来扩展,这时候需要等待很久才会溢出,如下图修改元空间大小参数,大小设置为20m,会马上内存溢出:

在这里插入图片描述
在这里插入图片描述

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 元空间溢出测试
 *
 */
public class MetaSpace {

    public static void main(String[] args) throws InterruptedException {
        long counter = 0;

        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(UserService.class);
            enhancer.setUseCache(false);//缓存
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });

            //创建动态代理类class
            UserService userService = (UserService)enhancer.create();
            userService.find();

            Thread.sleep(100);

            System.out.println("创建了" + (++counter) + "个动态代理对象");
        }
    }
}

public class UserService {

    static {
        System.out.println("UserService类加载......");
    }

    public void find() {
        System.out.println("find......");
    }
}

2.21 JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;

2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

3、可能导致OutOfMemoryError异常出现; netty

2.22 JVM本机直接内存溢出问题?

直接内存(Direct Memory) 的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;

由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

解释

  1. 如果想输出原因文件,则要输入JVM参数XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

  2. 测试NIO的直接内存溢出:

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class DirectBufferOOM {

    public static void main(String[] args) {
        final int _1M = 1024 * 1024;
        List<ByteBuffer> buffers = new ArrayList<>();
        int count = 1;
        while (true) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
            buffers.add(byteBuffer);
            System.out.println(count++);
        }
    }
}

报直接内存溢出(物理内存)

在这里插入图片描述
修改直接内存参数
在这里插入图片描述
在这里插入图片描述

如果想输出原因文件,则要输入JVM参数XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/dev/heapdump.hprof

  1. 先配置
  2. 运行程序,报Direct buffer memory错误
  3. 发现在D:/dev/下没有生成heapdump.hprof文件

这个步骤可以用来排查出是直接内存溢出,再进一步考虑是否直接或者间接的使用到了NIO技术

2.23 几个与JVM内存相关的核心参数?

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;

-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;

-XX:MetaspaceSize 元空间大小;

-XX:MaxMetaspaceSize 元空间最大大小;

-Xss 每个线程的栈内存大小;

-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;

-XX:MaxTenuringThreshold=5 年龄阈值;

-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;

-XX:+UseG1GC 指定使用G1垃圾回收器

–查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

2.24 查看一个对象的大小

导入Lucene库

<dependency>
	<groupId>org.apache.lucene</groupId>
	<artifactId>lucene-core</artifactId>
	<version>4.0.0</version>
</dependency>
import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {

    private int id;

    private String name;

    private BigDecimal money;

    private byte[] bytes = new byte[1024 * 1024]; //1024kb = 1m
  
  	public static void main(String[] args) throws IOException{
      Order order = new Order();
      //计算指定对象及其引用树上所有对象的综合大小,单位字节
      long size = RamUsageEstimator.sizeOf(order);
      //计算指定对象及其引用树上所有对象的综合大小,返回可读的结果,如:2KB
      String humanSize = RamUsageEstimator.humanSizeOf(order);
      
      System.out.println(size);
      System.out.println(humansize);
    }

}

在这里插入图片描述

三、JVM垃圾回收机制

3.1 堆为什么要分成年轻代和老年代?

在这里插入图片描述
因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;

年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;

老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法 ;

所以需要分成两个区域来放不同的对象;

1、绝大多数对象都是朝生夕灭的;

如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;

2、熬过越多次垃圾收集的对象就越难以回收;

如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ,同时也有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;

Minor GC/Young GC :新生代收集

Major GC/Old GC:老年代收集

Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;

Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:

(1)复制算法

(2)标记-清除算法

(3)标记-整理算法

3.2 JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区会怎么样?

此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;

解释:

会比较频繁的出发Full GC,开销太大

2、如果只有1个Survivor区会怎么样?【导致内存碎片】

刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;

如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;

如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;

在这里插入图片描述

解释:

如果只有一个Survivor区域,触发Minor GC后会有内存碎片问题的产生(内存不连续),大大降低了效率

所以得有第二个Survivor区域来装要存活的对象,从而使得第一个Survivor空闲、Eden也空闲,保证了内存空间的连续性——需要2个Survivor区域的原因解析

3.3 Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

一个eden区 ,新生代对象出生的地方;

两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;

统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;

解释

8:1:1原因在于每次gc会有90%的对象被回收 ,这是个统计的结果

3.4 请介绍下JVM中的垃圾回收算法?

在这里插入图片描述

3.4.1 标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为‘ 标记 ’和‘ 清除 ’两个阶段;

1、标记

标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;

2、清除

标记后,对所有被标记的对象进行回收;

该算法如下图所示:

会发现回收后,内存空间就不连续了,有内存碎片
在这里插入图片描述
优点:

基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;

缺点:

1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

3.4.2 复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

在这里插入图片描述
优点:

实现简单,效率高,解决了标记-清除算法导致的内存碎片问题

缺点:

1、代价太大,将可分配内存缩小了一半,空间浪费太多了;

2、对象存活率较高时就要进行较多的复制操作,效率将会降低;

3.4.3 标记-整理算法

标记-整理算法是根据老年代的特点而产生的;

1、标记

标记过程与上面的标记-清理算法一致,也是基于可达性分析算法进行标记;

2、整理

和标记-清理不同的是,该算法不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存;

标记-清除算法不移动存活对象,导致有大量不连续空间,即内存碎片,而老年代这种每次回收都有大量存活对象的区域,移动存活对象并更新所有引用这些对象的引用,这是一种比较耗时的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿我们也称为“Stop The World”即STW;

但是即便是移动存活对象是耗时的操作,但是如果不这么做,那么在充满内存碎片的空间中分配对象,又影响了对象的分配和访问的效率,所以JVM权衡两者之后,还是采用了移动存活对象的方式,也就是对内存进行了整理

另外像cms垃圾收集器,平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间,所以像基于标记-清除算法的CMS收集器面临空间碎片过多时就会进行一次整理;

优点:

1、不会像复制算法那样划分两个区域,提高了空间利用率;

2、不会产生不连续的内存碎片;

缺点:

效率问题,除了像标记-清除算法的标记过程外,还多了一步整理过程,效率变低;

如下图所示:
在这里插入图片描述

3.4.4 分代收集算法

现在一般虚拟机的垃圾收集都是采用“ 分代收集 ”算法;

根据对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法;

新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;——只需要复制少于10%的对象,效率很高!

老年代中,因为对象存活率较高,采用标记-清理、标记-整理算法来进行回收;

3.5 请介绍一下JVM垃圾收集器?

在这里插入图片描述
如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;

新生代收集器:Serial、ParNew、Parallel Scavenge [ˈpærəlel] [ˈskævɪndʒ]

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

垃圾收集器的最前沿成果:ZGC(Jdk11中引入了) 和 Shenandoah(Open JDK12,Oracle没引入,很前沿 )

解释:

目前在生产环境中,G1是比较先进的垃圾收集器了

3.5.1 Serial收集器[新生代、单线程]

新生代收集器,最早的收集器,单线程的,收集时需暂停用户线程的工作,所以有卡顿现象,效率不高,致使java语言的开发团队一直在改进垃圾收集器的算法和实现,但Serial收集器简单,不会有线程切换的开销,是Client模式下默认的垃圾收集器,-client, -server;

参数: -XX:+UseSerialGC

java -XX:+PrintFlagsFinal -version 打印jvm默认的参数值;

垃圾收集时间线如下:

所有线程卡住(停下来),进行垃圾收集,收集完毕,然后再继续…
在这里插入图片描述

3.5.2 ParNew收集器[新生代、多线程]

它是新生代收集器,就是Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial;

Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,后者CMS收集老年代,

 "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代垃圾收集器;
 "-XX:+UseParNewGC":强制指定使用ParNew;
 "-XX:ParallelGCThreads=2":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

在这里插入图片描述

3.5.3 Parallel Scavenge收集器【新生代、多线程】

在这里插入图片描述
简称Parallel,它是新生代收集器,基于复制算法,并行的多线程收集器(与ParNew收集器类似),侧重于达到一个可控的吞吐量,虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%,有时候我们也把该垃圾收集器叫吞吐量垃圾收集器或者是吞吐量优先的垃圾收集器;而且这个垃圾收集器是jvm默认的新生代的垃圾收集器;

它提供一个参数设置吞吐量:

-XX:MaxGCPauseMillis 该参数设置大于0的毫秒数,每次GC的时间将尽量保持不超过设置的值,但是这个值也不是设置得越小就越好,GC暂停时间越短,那么GC的次数会变得更频繁;

-XX:+UseAdaptiveSizePolicy 自适应新生代大小策略,默认这个参数是开启的,当这个参数被开启之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics);


如果我们不知道怎么对jvm调优,我们可以使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(最大停顿时间)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成,自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性;

参数:-XX:+UseParallelGC 指定使用Parallel Scavenge垃圾收集器

java -XX:+PrintCommandLineFlags -version 打印jvm默认初始堆和最大堆大小以及垃圾收集器

java -XX:+PrintFlagsFinal -version 打印jvm所有的默认的参数值;

-XX:+ 

-XX:+PrintGCDateStamps

-Xloggc:d:/dev/gc.log

Parallel Scavenge垃圾收集器中的Ergonomics负责自动的调节gc暂停时间和吞吐量之间的平衡,自动优化虚拟机的性能;

GC日志

这里常用一下JVM参数打印JVM的log:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:d:/dev/gc.log

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值