猿创征文 【高级篇】Java 进阶之JVM实战

文章目录

前言

JVM 是 Java 实现 跨平台的基础,所有的Java 程序都基于JVM,那么JVM底层到底是如何实现的呢,Java目前已火了20多年了,下面我们就一起来看看 这个强大的 JVM!!!

一、面试题解析

以下是面试高频题

请你谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?

JVM(Java Virtual Machine) :虚拟机 ,源文件.java在虚拟机中通过编译器编译成字节码文件.class,是整个java实现跨平台的最核心的部分

Java 8 虚拟机 撤销了 永久代,引入了 元空间的概念。

在HotSpot虚拟机中,jkd1.6时,设计团队把方法区设计为永久代,这样GC工作区域就可以扩展至方法区。这种策略可以避免为方法区单独设计垃圾回收机制 ,但是坏处就是,方法区的回收条件十分苛刻,而且回收效果也不好。

到了最新的Java1.8 ,撤销了永久代,改为了元空间。

元空间的规则:

元空间中类及其相关的元数据和类加载器生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。

什么是OOM?什么是栈溢出StackOverflowError?怎么分析?

OOM(OutOfMemoryError):内存溢出,原因是发生了某种原因 导致程序使用了大量的jar 和 class,使Java虚拟机的内存空间不足,与Permanent Generation space有关

解决方案:

  1. 增加Java虚拟机中的 XX:PermSizeXX:MaxPermSize 参数的大小,XX:PermSize 是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
  2. 清理应用程序中web-inf/lib下的 Jar 或者 Maven仓库中的Jar,防止出现大量的Jar文件导致程序崩溃

StackOverflowError: 栈溢出,当栈深度超过虚拟机分配给线程的栈大小时就会出现此Error

注意: 递归的错误,才出现Stack满的情况,无限循环一般不会占用更多的内存或者具体的Stack,只是占cpu而已,所以不会抛此错误。

分析:抓取内存快照,分析Dump文件。

JVM常用的调优参数有哪些?

  • -Xms2g:初始化推大小为2g;
  • -Xms2g:堆最大内存为2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为1:4;
  • -XX:SurvivorRatio=8:设置新生代Eden和Survivor比例为8:2;
  • -XX:+UseParNewGC:指定使用ParNew + Serial Old垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用ParNew + ParNew Old垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用CMS + Serial Old垃圾回收器组合;
  • -XX:+PrintGC:开启打印gc信息;
  • -XX:+PrintGCDetails:打印gc详细信息;

内存快照如何抓取,怎么分析dump文件?

开启内存快照,当出现OOM时,会自动将dump文件放到改路径下

复制代码
-XX:+HeapDumpOnOutOfMemoryError
 # 把内存快照放到指定路径下
 -XX:HeapDumpPath=/usr/local/app/oom

最重要的是要打印出来GC日志GC日志可以配合你用jstat工具分析GC频率和性能 的时候用,jstat可以分析出来GC的频率, 但是对每次具体的GC情况,可以结合GC日志来看

谈谈JVM中类加载器你的认识?

类加载器(ClassLoader) 是Java语言的一项创新,也是Java流行的一个重要原因。在类加载的第一阶段"加载"过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。

类加载器最重要的就是双亲委派模型,在下方会有说明。

二、JVM 理论详解

JVM的位置

JVM在JRE中

JVM的体系结构

类加载器

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器
  3. 扩展类加载器
  4. 应用程序加载器

三、JVM 双亲委派机制

复制代码
/*
    双亲委派机制
    1.类加载器收到类加载的请求    Application(应用加载)
    2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器
    3.根加载器检查是否能够加载当前这个类,能加载就结束,不能加载就抛出异常,通知子加载器进行加载
    4. 重复步骤3. 直至完成加载
    如果都没找到,则会抛出ClassNotFound!

    null:java调用不到的加载器,是由于底层是由C++写的,调用的C++的本地栈方法,所以是null

 */

加载顺序

复制代码
package java.lang;

public class String {

    //双亲委派机制:安全,一层一层网上找,上面有就使用上面的,没有就从根部一层层外外找,直至找到为止
    //1. APP(应用加载)  ---> EXC(扩展加载)  ---> BOOT(根加载,最终执行)
    //BOOT没有 --> EXC 再没有  ---> APP找到!
    
    @Override
    public String toString() {
        return "Hello World!!!";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.toString());
    }
}

我们新建了一个java.lang.String类,当加载时会报错,为什么呢,是因为双亲委派机制,直接去调用了ROOT下的String 类

很好的双亲委派机制讲解

沙箱安全机制

很好的沙箱安全机制讲解

了解即可

四、Native 关键字

native是一个计算机函数,一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++

我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。

如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。

最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。

Thread类就调用了本地方法启动线程

复制代码
//navive:凡是带了native关键字的方法,说明java的作用范围达不到了,会去调用C语言的库
//会进入本地方法栈
//Java诞生的时候 C、C++横扫天下,Java想要立足,就必须要有 调用C、C++的程序
//JNI作用:扩展Java程序的使用,融合不同的编程语言为Java所用,C/C++
//它在内存区域开辟了一块空间为本地方法栈(Native Method Stack) 来登记需要执行的本地方法
//在最终执行的时候,通过JNI加载本地方法库中的方法
private native void start0();

//调用其它语言的接口,http、Socket、WebService

掌握即可,企业级应用中较为少见!

五、PC寄存器 与 方法区

PC寄存器

程序计数器:Program Counter Register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也指向即将要指向的指代代码),在执行引擎读取下一条命令,是一个非常小的内存空间,几乎可以忽略不计

方法区

Methad Area 方法区

方法区就是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在于堆内存中,和方法区无关

六、栈与堆

栈是一种数据结构

程序=数据结构+算法

栈:先进后出,桶

队列:先进先出(FIFO),First Input First Output

喝多了吐就是栈,吃多了拉就是队列

一个简单的执行流程

复制代码
public class Test {

    public static void main(String[] args) {
        new Test().test();
    }
    
    public void test(){
        
    }
}

在内存图中的结构如下

复制代码
public class Test {

    public static void main(String[] args) {
        new Test().test();
    }
    
    public void test(){
        a()
    }
    
    public void a(){
        test();
    }
}

这样会无线堆积栈,直至栈内存溢出

这种错误是非常严重的,例如递归时我们经常见这样的错误,一旦发生,很难解决,避免程序OOM

对象在内存中的创建过程

复制代码
public class Person {

    private Integer noid;

    private String name;

    private Integer age;

    public void info() {
        System.out.println("学生的姓名:" + name);
        System.out.println("学生的年龄:" + age);
        System.out.println("学生的noid:" + noid);
    }

    public static void main(String[] args) {
        //实例化对象
        Person person = new Person();
        //给属性赋值
        person.name = "小智";
        person.age = 20;
        person.noid = 1;
        //调用方法
        person.info();
    }
}

这段代码有3个成员变量,一个成员方法,我们为其赋值

内存结构图

在java内存中,创建对象有三个区域,栈(Stack )、堆(Heap )、方法区(Method Area)

将类信息和成员方法加载至方法区,将成员属性加载至堆

main函数进入栈区,并定义一个Person类型的引用指向Person类的实例,在堆区创建Person对象的实例

接下来赋值操作,现在栈区找到对象的引用,然后根据引用去堆区赋值

随后调用info方法,先找到栈区对象的引用,然后根据指向去堆区找到实例,再去方法去调用方法

最后执行方法,方法执行完毕后,方法被弹出,也叫出栈,最后main函数被弹出

至此,创建对象在内存中的过程完毕

Heap,一个JVM只有一个堆内存大小,堆内存的大小是可以调节的,

类加载器读取了类文件后,一般会将什么东西放到堆中?方法,全局变量,保存我们引用的真实对象

堆内存还要细分3个区域

  • 新生区(伊甸园区) Young/New
  • 养老区 old
  • 永久区 Perm

GC垃圾回收主要在伊甸园区和养老区

假设内存满了,就会爆OOM错误,堆内存不够

复制代码
import java.util.Random;

public class Test {

    public static void main(String[] args) {
        String str = "";
        while (true) {
            str += str + new Random().nextInt(666666666)+new Random().nextInt(99999999);
        }
    }

}

在JDK8之后,永久存储区改为元空间

七、三种JVM、新生区、老年区、永久区

以下三种JVM

  • HotSpot
  • BEA JRockit
  • IBM J9VM

我们使用的是HotSpot

新生区

  • 类:诞生和成长的地方,甚至死亡
  • 伊甸园区,所有的对象都是在伊甸园区new出来的
  • 幸存者区(0,1)两个来回切换 from to

老年区

老年区,经历重重GC回收还没死亡的会进入老年区,进入老年区的对象少之又少

真理:经过研究,有99%的对象都是临时对象,都在伊甸园区,用完即失。

永久区

这个区域是常驻内存的,用来存放JDK自身携带的Class对象,Interface存放元数据,存储的是Java运行时的一些环境或类信息,这个区域不被GC垃圾回收,在关闭虚拟机的时候就会释放这个区域的内存。

一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态的生成反射类,不断的被加载,直到内存满,就会出现OOM

  • jdk1.6之前:永久代,常量池是在方法区
  • jdk1.7:永久代,满满的退化了,去永久代 常量池在堆中
  • jdk1.8之后:无永久代,元空间,常量池在元空间内

逻辑上存在,物理上不存在

在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错~

  • 能够看到代码第几行出错:快照分析工具,MAT:Eclipse的工具 JProfiler
  • DeBug,一行行分析

MAT、Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的错误
  • 获得大的对象~

小结

以上就是【Bug 终结者 】对 【高级篇】Java 进阶之JVM实战 的简单介绍,JVM 是Java 进阶必备, 在项目开发中,熟练的掌握了JVM 那就会感受到极其好的感受JVM调优 ,提高程序QPS,吞吐量必备,同时 也是 Java 高级面试高频,可见,掌握并灵活运用JVM就可以说是达到了高级的水平!

如果这篇【文章】有帮助到你,希望可以给【Bug 终结者 】点个赞??,创作不易,如果有对【后端技术 】、【前端领域 】感兴趣的小可爱,也欢迎关注 【Bug 终结者】,我将会给你带来巨大的【收获与惊喜】???!

相关推荐
shykevin27 分钟前
python开发Streamable HTTP MCP应用
开发语言·网络·python·网络协议·http
我不是程序猿儿30 分钟前
【C#】 lock 关键字
java·开发语言·c#
漫路在线1 小时前
JS逆向-某易云音乐下载器
开发语言·javascript·爬虫·python
小辉懂编程2 小时前
C语言:51单片机实现数码管依次循环显示【1~F】课堂练习
c语言·开发语言·51单片机
tmacfrank2 小时前
网络编程中的直接内存与零拷贝
java·linux·网络
醍醐三叶2 小时前
C++类与对象--2 对象的初始化和清理
开发语言·c++
weixin_472339462 小时前
Maven 下载安装与配置教程
java·maven
Magnum Lehar3 小时前
3d游戏引擎EngineTest的系统实现3
java·开发语言·游戏引擎
就叫飞六吧4 小时前
Spring Security 集成指南:避免 CORS 跨域问题
java·后端·spring
Mcworld8574 小时前
java集合
java·开发语言·windows