JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出

这里写目录标题

  • 使用visualvm查看JVM视图
  • Visual GC插件下载链接
  • 模拟JVM常见错误
    • 模拟堆内存溢出
    • 模拟栈溢出
      • 总结
    • 模拟方法区溢出
  • 思考
  • 汇总

使用visualvm查看JVM视图

Java当中提供了工具,你的电脑中装了Java,比如你想看到Java虚拟机的一个视图,就可以打开dos窗口,输入jvisualvm,对于这个命令可以这么理解,前面的j表示java命令,visual就是视图的意思,vm表示虚拟机。

jvisualvm

会弹出这个工具Java VisualVM。
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第1张图片
可以在工具,插件中区下载一个插件,叫做Visual GC,也就是GC的一个视图。
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第2张图片

Visual GC插件下载链接

visualgc插件下载链接 :https://visualvm.github.io/pluginscenters.html
选择对应JDK版本链接—>Tools—>Visual GC
若上述链接找不到合适的,大家也可以自己在网上下载对应的版本

模拟JVM常见错误

模拟堆内存溢出

新建一个SpringBoot项目,pom如下


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.7.14version>
        <relativePath/> 
    parent>
    <groupId>com.examplegroupId>
    <artifactId>jvm-demoartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>jvm-demoname>
    <description>jvm-demodescription>
    <properties>
        <java.version>1.8java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.openjdk.jolgroupId>
            <artifactId>jol-coreartifactId>
            <version>0.17version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    dependencies>
project>

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class HeapController {
    List<Worker> list = new ArrayList<>();

    @GetMapping("/heap")
    public String heap() {
        while (true) {
            list.add(new Worker());
        }
    }
}
import org.openjdk.jol.info.ClassLayout;

public class Worker {
    private Worker worker;
    private Integer id;
    private String username;
    private String password;

    public Worker getWorker() {
        return worker;
    }

    public void setWorker(Worker worker) {
        this.worker = worker;
    }

    public Integer getId() {
        return id;
    }

    public String getPassword() {
        return password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return super.toString();
    }

    public static void printf(Worker worker) {
        // JOL工具类查看对象的整体结构信息
        System.out.println(ClassLayout.parseInstance(worker).toPrintable());
    }

    public static void main(String[] args) {
        Worker work = new Worker();
        // 输入信息
        System.out.println(work.hashCode());
        System.out.println(work);
        // 输出信息
        Worker.printf(work);

    }
}

设置JVM启动参数,以下是简便写法:
Xms:设置程序启动时占用内存大小
Xmx:设置程序运行期间最大的可占用内存大小

-Xms20M -Xmx20M

JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第3张图片

设置好之后启动,然后打开jvisualvm找到对应的Java进程
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第4张图片

点击Visual GC,可以发现该进程的内存分布已经在缓慢增加了
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第5张图片

运行结果:
访问浏览器http://localhost:8080/heap,可以发现迅速占满Old区,
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第6张图片

并且程序报错java.lang.OutOfMemoryError: Java heap space
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第7张图片

这里会报错的有两个,一个是OOM内存溢出,一个是堆空间不足。实际上内存溢出也会有一个阈值,包括GC执行的次数也会有一个阈值,当GC每秒执行超出阈值之后,会报错GC overhead limit exceeded,即GC过于频繁。

这两个错可能都是由于不停往堆中放对象所导致的,这个时候可以发现每次进行垃圾回收之后,回收所释放的空间会被快速的填满,迫使GC再次执行,又或者说空间一直处于高额的额度,就会报Java heap space,堆空间不足,而服务器会因为频繁的执行所谓GC垃圾回收的操作,使得CPU达到100%的使用率,导致服务器运行会变慢、应用程序会卡死,平时几ms的操作,可能现在需要几分钟、甚至更长的时间才能完成。

模拟栈溢出

栈里面放的是栈帧,栈帧代表的是当前方法的执行,所以可以通过递归方法来模拟栈溢出。

public class Demo {

    public static long count=0;

    /**
     * 栈里面放的是栈帧,栈帧代表的是当前方法的执行,所以可以通过递归方法来模拟栈溢出
     * @param i
     */
    public static void method(long i){
        System.out.println(count++);
        method(i);
    }

    public static void main(String[] args) {
        method(1);
    }
}

运行结果:
执行,程序报错java.lang.StackOverflowError,结果如下:
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第8张图片
这里可以看到栈帧的默认深度是7000+,而实际上一个方法占用多少是未知的,仅仅只知道默认值会跑到7000+。

可以通过设置参数来指定虚拟机栈的大小,如指定大小为128k,k表示KB。

-Xss128k

设置成128k之后,再次执行,
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第9张图片JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第10张图片

可以发现只有800,这个参数代表当前线程(虚拟机栈是线程私有的)的虚拟机栈的内存空间的大小,同理设置成256k,那么理论上就会达到1600左右。
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第11张图片
单个线程的虚拟机栈设置的空间越大,表示能够存储的方法就会越多,256k对应1600左右,1600×4=6400,所以可以猜到单个线程的虚拟机栈默认值大小是1MB。

可以根据业务场景的业务链去设计所需要的栈帧深度。在相同的物理条件下,减小这个值,占用的空间就会越小,也就意味着可以生成更多的线程。

设置时首先需要确定机器能够分配多少空间给栈,比如500M,不设置的话,默认单个线程的虚拟机栈大小为1MB,也就是说支持500条线程,500条线程也可以满足业务正常使用,那么当然是没有任何问题的;而一般根据经验值来说,所需要支持的线程数在3000~5000之间,取个中间值4000条,因此在硬件层面资源有限的情况下,要在满足业务调用链的情况下适当调小这个值,来提升线程数量,500×1024÷400=128,如设置成128k。

设置太小会很容易触发栈溢出,设置太大,整个操作系统的内存空间是有限的,Java进程的空间是有限的,而每个线程的平均大小设置的太大,它回去储存调用很多的方法,会影响其它线程的创建,所以一般情况下,能够做到支持的线程数在3000~5000之间的程度就可以了。

总结

Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出java.lang.StackOverflowError的错误。
-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

模拟方法区溢出

需要用到ASM。ASM是一个通用的Java字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。ASM提供与其他Java字节码框架类似的功能,但专注于性能。因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

pom添加ASM依赖

 
 <dependency>
     <groupId>asmgroupId>
     <artifactId>asmartifactId>
     <version>3.3.1version>
 dependency>

定义一个类,不断的往方法区中添加class信息,来模拟方法区溢出

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.ArrayList;
import java.util.List;

public class MyMetaspace extends ClassLoader {

    public static List<Class<?>> createClasses() {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for (int i = 0; i < 10000000; ++i) {
            ClassWriter cw = new ClassWriter(0);
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null);
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V");
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();
            MyMetaspace= test = new MyMetaspace();
            byte[] code = cw.toByteArray();
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);// 往方法区取添加class信息
            classes.add(exampleClass);
        }
        return classes;
    }
}
@RestController
public class NonHeapController {
    List<Class<?>> list = new ArrayList<Class<?>>();

    @GetMapping("/nonHeap")
    public String nonHeap() {
        while (true) {
            list.addAll(MyMetaspace.createClasses());
        }
    }
}

设置Metaspace的初始化大小和最大大小,比如:

-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M

JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第12张图片

运行结果:
访问浏览器http://localhost:8080/nonHeap,程序报错java.lang.OutOfMemoryError: Metaspace
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第13张图片

思考

JVM内存模型认识的差不多了,就应该思考,什么样的内存模型适合什么样的GC策略,包括垃圾回收为什么会出现。实际上,很多东西都是相对应版本的JVM强加上去的。
JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出_第14张图片
那么垃圾回收是什么?从运行时数据区看垃圾回收到底回收哪块区域?垃圾回收如何去回收?

汇总

JVM1:官网了解JVM;Java源文件运行过程、javac编译Java源文件、如何阅读.class文件、class文件结构格式说明、 javap反编译字节码文件;类加载机制、class文件加载方式

JVM2:类加载机制、class文件加载方式;类加载的过程:装载、链接、初始化、使用、卸载;类加载器、为什么类加载器要分层?JVM类加载机制的三种方式:全盘负责、父类委托、缓存机制;自定义类加载器

JVM3:图解类装载与运行时数据区,方法区,堆,运行时常量池,常量池分哪些?String s1 = new String创建了几个对象?初识栈帧,栈的特点,Java虚拟机栈,本地方法发栈,对象指向问题

JVM4:Java对象内存布局:对象头、实例数据、对齐填充;JOL查看Java对象信息;小端存储和大端存储,hashcode为什么用大端存储;句柄池访问对象、直接指针访问对象、指针压缩、对齐填充及排序

JVM5:JVM内存模型与运行时数据区的关系,堆为什么分区,分代年龄,Young区划分,Survivor区为什么分为S0和S1,如何理解各种GC:Partial GC、Full GC、Young GC

JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出

JVM7:垃圾回收是什么?从运行时数据区看垃圾回收到底回收哪块区域?垃圾回收如何去回收?垃圾回收策略,引用计数算法及循环引用问题,可达性分析算法

你可能感兴趣的:(jvm,jvm,jvm内存模型)