JVM通俗易懂一篇搞定!!超详细!!!

目录

JVM组成

1.程序计数器

2.堆

3.虚拟机栈

4.方法区(元空间)

5.本地方法栈

6.直接内存

类加载器

7.类加载器

8.类加载器种类

9.双亲委派模型

10.双亲委派解决什么问题

垃圾回收

11.GC的概念

12.对象被垃圾器回收的时机

引用计数法

可达性分析算法

13.垃圾回收算法

14.JVM中的分代回收(对象的状态如何在新生和与老年代之间转换的)

15.强引用、软引用、弱引用、虚引用

JVM调优

16.JVM调优参数设置

17.JVM调优工具

18.内存溢出排查思路


        JVM是每个人成为大佬必须要经历的过程,虽然难但是也要啃下来,下面我们就一起学习一下Java代码背后发生了什么。

        在开讲之前先简单说一下JVM的作用,我们都知道计算机是二进制的系统,他只认识00110101,那么我们写的代码HelloWord.java我们是认识的,但是计算机不认识,那么就需要将代码转换成计算机认识的二进制,那么JVM来了。

1)首先程序员编写的.java文件通过Javac编译器编译成.class字节码文件之所以编译成.class文件是因为JVM只认识.class文件。

2)然后由类加载器负责将.class文件加载到JVM的内存中,因为只有加载到内存程序才能运行。

3)如果想要运行还要进入一个执行引擎的地方,它的作用是将字节码翻译为底层系统指令,主要分为三部分,第一部分解释器她负责解释字节码的信息,即时编译器他会对你的代码进行优化,第三部分GC垃圾回收指的是运行数据区中的堆空间。

4)最后一部分是本地方法接口也叫本地库,由C或者C++实现,他而存在是因为我们的Java代码有的时候不能完全去实现某些功能,所以需要借助系统提供的接口。

JVM组成

1.程序计数器

        我们都知道多线程编程情况下,CPU会为每一个线程分配时间片,就是说你这个线程可以执行多长长时间,在这个时间段内别的线程不能执行,那么有这样一种情况就是当前被执行的这个线程所分配的时间片时间用完了它就会被挂起,此时就会切换到另外一个线程去执行,并且这个线程的执行时间用完了,接着又会切换到刚刚被挂起的线程,这里就有问题了,我们如何知道这个线程执行到什么位置了呢?所以程序计数器登场了,我们知道Java代码通过Javac编译之后变成.class字节码文件,他就会记录你在字节码中执行到什么位置的行号,然后等你再次执行时根据这个行号接着执行就可以了,并且程序计数器每个线程都有一个是私有的。如图所示

2.堆

        堆的话是一块线程共享区域,就是说我们new出来的对象以及数组等就保存在堆中,如果当堆中没有内存空间可分配就会抛出OutOfMemoryError异常,也就是传说中的OOM内存溢出,它的内部结构分为两部分年轻代、老年代其中年轻代又分为三部分第一个是Eden区、第二是S0、第三是S1其中S0和S1也称之为幸存者区,他的工作流程是这样的,当一个对象创建出来后,先会到Eden区保存,如果这个对象垃圾回收之后还存活,他就会被移动到S0或者S1假如挪动一定次数后这个对象还存活着,他就会被放到老年代中,老年代指的是生命周期比较长的对象。

       Java8与Java7堆的区别

        这里的话多分享点知识,java8和java7中堆的区别,在上面图中我们可以看到在本地内存中有一个叫做元空间的东西,它里面存储的是类信息、静态变量、常量等那么其实在Java8之前元空间是属于堆中的一块区域叫做永久代,那么为啥在java8之后把他挪到本地内存中了呢,其实就是因为随着项目越做越大类信息、静态变量、常量等越来越多太占用内存空间容易发生OOM内存溢出,所以就将他移动到了本地内存上,重新开辟了一块空间,叫做元空间。

3.虚拟机栈

        虚拟机栈是每个线程在运行时所需要的内存空间,存储的就是方法调用时的相关信息,如参数、局部变量和返回地址等。这个栈由多个栈帧组成,对应着线程在执行过程中所调用的方法。每次调用方法时,相应的栈帧就会被压入栈中,当方法执行完毕后,栈帧会从栈中弹出是一个先进后出的顺序。每个线程只有一个活动的栈帧,即当前正在执行的方法。这就是虚拟机栈的工作流程。但是为了更好掌握虚拟机栈在下面在为大家分享一些知识。

垃圾回收是否设计栈内存?

        垃圾回收主要指的是堆内存,而栈里面的栈帧弹栈以后内存就被释放了,这里面并不需要垃圾回收器去回收

栈内存分配越大越好吗?

        默认的栈帧内存为1024k也就是一兆,如果栈帧所占用的内存过大会导致线程数变少,举个例子,现在服务器总内存为512兆,如果每个线程占用内存一兆,也就是能分配512个线程,但是如果把栈帧内存改为2048k也就是两兆,那是不是线程数减少了一半。

方法内的局部变量是否线程安全?

        一个方法内部的局部变量如果没有逃离方法的作用范围他就是线程安全的,如果局部变量逃离了方法的作用范围就是非线程安全,比如一个方法可以传形参,或者有返回值那么就有可能被多线程进行调用,所以是非线程安全的。

栈内存会溢出吗?

我们都知道堆可能造成内存溢出OOM原因是装不下了对象迟迟不被垃圾回收,那么栈也是会内存溢出的,两种情况第一使用递归自己调用自己的方法就会报错Java.lang.StackOverflowError,第二栈帧过大导致栈内存溢出,栈帧就是线程执行方法所需的内存默认是一兆,通常不会溢出

虚拟机栈和堆的区别?

1、栈用来存储方法调用时的相关信息,如参数、局部变量、返回地址等,堆用来存储对象和数组

2、栈内存是线程私有的,而堆内存是线程共有的。

3、异常错误不同,栈和堆内存溢出都会抛出异常

        栈空间不足:java.lang.StackOverFlowError

        堆空间不足:java.lang.OutOfMemoryError

4.方法区(元空间)

        方法区也叫做元空间,他负责存储类的信息、运行时常量池,存储的位置是本地内存也就是操作系统的内存,并且它是各个线程共享的的,他的主要作用就是说当虚拟机想要执行一个方法,那么他就需要知道类名、方法名、参数类型等信息,而运行时常量池就相当于一个字典通过字典的目录就可以找到对应的信息,那么虚拟机就会通过指令找到字典的目录再通过目录找到对应类名、方法名、参数类型等信息然后执行,以上说的这些是类被加载之前也就是.class文件时,等到该类被加载,运行时常量池的地址也就是字典的目录就会直接指向内存的地址,因为只有指向内存的地址才能去执行当前的指令。

5.本地方法栈

JVM本地方法栈是一种特殊的内存区域,用于支持native关键字修饰的方法,本地方法栈是每个线程私有的,本地方法栈上保存着Native方法的信息和状态。

6.直接内存

举例

Java代码完成文件拷贝

常规IO

        我们都知道Java本身并不具备磁盘读写能力,若想使用磁盘读写的能力,就必须调用操作系统提供的native函数也就是内部会调用本地方法,同时CPU的状态会从用户态切换成内核态

同时内存也会作出相应变化,当切换到内核态的时候,会将磁盘文件先读进系统缓冲区(分次读取),Java是无法使用系统缓冲区,Java就会在堆内存中创建一个Java缓冲区(byte[] buf = new byte[_1Mb]),Java要想读取到系统缓冲区,就需要将系统缓冲区的数据间接读入到Java缓冲区,Java就能对Java缓冲区进行操作了。

之所以用传统IO效率比较低,是因为磁盘文件需要先读入系统缓冲区,系统缓冲区再读入Java缓冲区,Java才能对磁盘文件进行处理,这里造成了不必要的数据复制。效率因而较低。

NIO

直接内存是指操作系统分配一块内存,Java也可以直接访问,操作系统也可以访问,也就相当于Java和操作系统共享的一块内存。这时候磁盘文件可以读入进直接内存,接着Java代码可以对直接内存进行操作,也就是比传统IO少了一次复制的操作,因而效率较高。

类加载器

7.类加载器

由于JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

8.类加载器种类

  • 启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库
  • 扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类
  • 应用类加载器(AppClassLoader):用于加载classPath下的类,我们字节编写的类就由应用类加载器加载
  • 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。

9.双亲委派模型

        加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类,比如我们现在写一个Student类属于我们自己定义的,正常由应用类加载器加载,但是双亲委派的意思就是自己先不加载而是向上委托,然后由于启动类加载器和拓展类加载器目录下的类没有Student所以最后由应用类加载器加载。

10.双亲委派解决什么问题

1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。

2)保证类库API不会被修改,打个比方我们定义一个String类,然后由于是双亲委派的机制String类在启动类加载器得到了加载,就是说在启动类加载器下的类库中有其相同名字的类文件,平时我们自己定义一个和API相同的类名,那么启动时就会报错这就是双亲委派的机制,保证核心类库API不会被修改。

垃圾回收

11.GC的概念

为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。

有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。

在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机

换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。

12.对象被垃圾器回收的时机

        因为堆里面存储的是我们new出来的对象及数组,所以垃圾回收主要是指回收堆内存,对象什么时候被垃圾回收总结成一句话就是,如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,就会被垃圾回收器回收。如何定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

引用计数法

        一个对象被引用了一次,就会在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收,比如有这样一行代码String demo = new String("123"); demo在栈中存储指向在堆中的new String("123"),那么对象头上的引用次数就为1,如果此时demo没有指向它那么这个new出来的对象就是垃圾,这种方式好处就是实时性较高,无需等到内存不够的时候,才开始回收,但是也存在问题,假设现在有两个对象ab他们此时互相引用对方也就造成了循环引用,这样a和b永远都不会被回收。

普通引用

循环引用

可达性分析算法

        现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾,可达性分析算法他会去堆中进行扫描,他的扫描过程好比一个树,有根节点根节点下面有子节点子节点下面还有子节点,他会去判断那些子节点与根节点存在关联,如果能关联到无论是直接关联或者是间接关联找到的这些对象都证明是存活的对象就不会被垃圾进行回收,如果在扫描过程中不能沿着根节点找到关联的对象就会被垃圾回收,其中根节点是那些肯定不能当做垃圾回收的对象,就可以当做根对象,比如栈帧的本地变量他就能去关联堆中的数据。

虚拟机栈(栈帧中的本地变量表)中引用的对象

/**
 * demo是栈帧中的本地变量,当 demo = null 时,由于此时 demo 充当了 GC Root 的作用,demo与原来指向的实例 new Demo() 断开了连接,对象被回收。
 */
public class Demo {
    public static  void main(String[] args) {
    	Demo demo = new Demo();
    	demo = null;
    }
}

方法区中类静态属性引用的对象

/**
 * 当栈帧中的本地变量 b = null 时,由于 b 原来指向的对象与 GC Root (变量 b) 断开了连接,所以 b 原来指向的对象会被回收,而由于我们给 a 赋值了变量的引用,a在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!
 */
public class Demo {
    public static Demo a;
    public static  void main(String[] args) {
        Demo b = new Demo();
        b.a = new Demo();
        b = null;
    }
}

方法区中常量引用的对象

/**
 * 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收
 */
public class Demo {
    
    public static final Demo a = new Demo();
    
    public static  void main(String[] args) {
        Demo demo = new Demo();
        demo = null;
    }
}

13.垃圾回收算法

标记清除算法

        标记清除算法将垃圾回收分为两个阶段第一是标记、第二是清除,首先使用可达性分析算法分析出那些是存活的对象然后进行标记,而非存活的对象则不进行标记而是直接进行清除,他的优点是标记和清除速度比较快,但是会出现内存碎片化的问题内存不是连贯的,我们都知道数组也会存储到堆中并且数组存储必须是一个连续的内存空间,如果使用了标记清除算法,由于内存的不连续有可能没有办法存储比较大的数组和对象。所以这个算法用的比较少。

标记整理算法

标记整理算法前半部分和标记清除算法是一样的也是通过可达性算法进行标记存活对象而非标记对象进行回收,不同的是在清除完成之后它会将存活的对象在内存中整理成一块连续的内存空间,也就避免了碎片化的问题,但是也由于是移动了对象他的性能也会受到影响。很多老年代的垃圾回收器都会使用这个标记整理算法。

复制算法

复制算法是将整个内存分为两块大小一致的区域,标记阶段与我们上述两个算法是一样的,也是通过可达性算法进行标记存活对象,然后将这些存活的对象进行一个复制,复制到另外一块内存区域,复制过程中就完成了碎片的整理,他的优点是在垃圾比较多的情况下效率是比较高的,并且清理之后内存是无碎片的,缺点就是需要两块内存空间,内存的使用率是比较低的,一般年轻代垃圾回收器就会使用这个复制算法

14.JVM中的分代回收(对象的状态如何在新生和与老年代之间转换的)

        在Java8中堆分为了两块区域(Java7还存在新生代现在叫做元空间)新生代与老年代,其中新生代又被分为三块区域伊甸园区和两块幸存者区取名为from和to他们的内存比例为8:1:1,当一个对象或数组被创建出来首先会在伊甸园区当中存储,如果伊甸园区内存不足了JVM就会通过复制算法将伊甸园区和幸存者from区中存活的对象通过复制算法复制到幸存者to区当中,此时伊甸园区和from内存都会得到释放,然后经过一段时间之后伊甸园区的内存又出现不足,JVM还是通过复制算法将伊甸园区和幸存者to区的存活对象复制到幸存者from区以此类推,其中当幸存者区的对象经过了多次在幸存者区的复制最多15次,他就会晋升到老年代当中,或者说幸存者区内存不足了或者这个对象可能占用内存空间比较大就会导致提前晋升到老年代。

15.强引用、软引用、弱引用、虚引用

强引用:

        我们都知道现在的虚拟机都是通过可达性分析算法标记那些是可存活对象也就是GC Roots,那么强引用指的就是与GC Roots相关联的对象就不会被回收,即使内存溢出OOM了也不会被回收,只有当对象不被GC Roots引用才会被回收。

User user = new User();

软引用:

        对象被SoftReference包括就是一个软引用对象,就是说在最开始垃圾回收时内存还有空间时软引用对象不会被垃圾回收,而是等内存空间不足时才会被垃圾回收。

User user = new User();
SoftReference softReference = new SoftReference(user);

弱引用:

        弱引用是指当一个对象被WeakReference包裹那么这个对象就是弱引用对象,当垃圾回收时不管内存空间是否足够都会被进行回收。

User user = new User();
WeakReference weakReference = new WeakReference(user);

虚引用:

        对象在被垃圾回收时只是在堆内存中得到了释放,而他可能调用了外部资源有可能不是Java占用的也不是Java的内存有可能是直接内存、网络连接等,那么这些内存该如何释放呢?虚引用登场了当一个对象被垃圾回收之后如果这个对象有虚引用那么这个虚引用对象就会加入到引用队列中,之后会有一个线程Reference Handler会定期检查这个引用队列,检查是否有对象被垃圾回收并且有虚引用存在。如果有,那么它会通过这个虚引用去释放外部资源。这样就能确保一个对象在Java堆内存中被回收,它所持有的外部资源也能被正确地清理,防止内存泄漏或其他资源泄露问题。

JVM调优

16.JVM调优参数设置

方式一:Tomcat中设置修改TOMCAT_HOME/bin/catalina.sh文件这是Linux操作系统,Windos的话时.bat后缀

方式二:springboot项目下修改

nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &

nohup  :  用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行

参数 **&**  :让命令在后台执行,终端退出后命令仍旧执行。

17.JVM调优工具

 jconsole可视化工具:用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具,在java 安装目录 bin目录下 直接启动 jconsole.exe 就行

VisualVM:故障处理工具能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈在java 安装目录 bin目录下 直接启动 jvisualvm.exe就行(只有Java8才有)

18.内存溢出排查思路

        我们知道内存溢出是由于内存装载不下对象造成内存溢出,而内存溢出究其原因是因为内存泄漏也就是因为我们操作不当造成占用内存的对象迟迟不能被回收,久而久之内存装载不下就造成了内存溢出,所以我们的思路就是定位到那行代码造成的原因。我们今天主要说内存堆内存溢出

通过jmap指定打印他的内存快照dump

jmap -dump:format=b,file=heap.hprof pid

使用vm参数获取dump文件

有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式的生成dump文件

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps/

通过工具, VisualVM(Ecplise MAT)去分析 dump文件,文件-->装入--->选择dump文件即可查看堆快照信息

通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题

通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题,找到对应的代码,通过阅读上下文的情况,进行修复即可

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM(Java Virtual Machine)是Java虚拟机的缩写,是Java程序能够运行的关键所在。JVM是一个软件程序,负责将Java字节码翻译成计算机平台上的机器指令,从而使Java程序能够在不同的平台上运行。 JVM包括三个部分:类装载器、执行引擎和内存管理系统。 1. 类装载器:类装载器负责将编写好的Java源代码编译成字节码,并将字节码装载到JVM中。JVM的类装载器有三个层次,分别是启动类装载器、扩展类装载器和应用程序类装载器,不同的类装载器负责不同的类的装载。 2. 执行引擎:执行引擎是JVM的核心部分,它负责将Java字节码翻译成计算机平台上的机器指令。JVM的执行引擎包括解释器和JIT编译器,解释器将字节码一条一条翻译成机器指令,而JIT编译器会将频繁执行的代码编译成机器指令,从而提高程序的执行效率。 3. 内存管理系统:JVM的内存管理系统包括堆、栈、方法区等。JVM的堆是Java程序运行时的内存空间,用于存放Java对象。栈用于存储局部变量和方法调用的信息,方法区用于存储类的信息、常量池等。 总之,JVM是Java程序能够跨平台运行的关键所在。JVM通过将Java字节码翻译成计算机平台上的机器指令来实现Java程序的跨平台运行。JVM的类装载器、执行引擎和内存管理系统是JVM的三个核心部分,它们共同协作,使Java程序能够高效地运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值