JVM入门--图文并茂 【一篇足矣】

目录

博主自己画的JVM图

黑马的JDK8内存结构图 

Java性能低的而主要原因

字节码文件

字节码文件的组成

字节码文件的常量池 

字段

类加载器ClassLoader

运行时数据区

程序计数器

Java方法栈(虚拟机栈)

栈帧

栈帧组成

本地方法栈 

Heap堆

分区

Full  GC

什么情况下会触发FULL GC ? 

Minor GC

什么情况下会触发Minor GC ? 

Major GC

什么情况下会触发Major GC?

Major GC和Full Gc的区别

方法区

成员变量、局部变量、类变量分别存储在内存的什么地方?

双亲委派

破坏双亲委派

Tomcat破坏

JDBC破坏 

JDBC案例中真的打破了双亲委派机制吗?


博主自己画的JVM图

黑马的JDK8内存结构图 

 JVM有三种:HotSpot、JRockit、J9 VM,而我们用的通常是HotSpot。

JVM虚拟机包括类加载器和运行时数据区,它和其他应用程序并行运作在操作系统上。 

Java性能低的而主要原因

Java语言如果不做任何的优化,性能其实是不如C和C++语言的。主要原因是:

在程序运行过程中,Java虚拟机需要将字节码指令实时地解释成计算机能识别的机器码,这个过程在运行时可能会反复地执行,所以效率较低。

C和C++语言在执行过程中,只需要将源代码编译成可执行文件,就包含了计算机能识别的机器码,无需在运行过程中再实时地解释,所以性能较高。

 Java为什么要选择一条执行效率比较低的方式呢?主要是为了实现跨平台的特性。Java的字节码指令,如果希望在不同平台(操作系统+硬件架构),比如在windows或者linux上运行。可以使用同一份字节码指令,交给windows和linux上的Java虚拟机进行解释,这样就可以获得不同平台上的机器码了。这样就实现了Write Once,Run Anywhere 编写一次,到处运行 的目标。

字节码文件

我们java中说的字节码文件即 java代码编译后的.class文件,class文件可以跨平台运行在不同操作系统的JVM上。

字节码文件的组成

字节码文件总共可以分为以下几个部分:

  • 基础信息:魔数、字节码文件对应的Java版本号、访问标识(public final等等)、父类和接口信息

  • 常量池保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用

  • 字段: 当前类或接口声明的字段信息

  • 方法: 当前类或接口声明的方法信息,核心内容为方法的字节码指令

  • 属性: 类的属性,比如源码的文件名、内部类的列表等

字节码文件的常量池 

字节码文件中常量池的作用:避免相同的内容重复定义。

比如在代码中,编写了两个相同的字符串“我爱北京天安门”,字节码文件甚至将来在内存中使用时其实只需要保存一份,此时就可以将这个字符串以及字符串里边包含的字面量,放入常量池中以达到节省空间的作用。

String str1 = "我爱北京天安门";
String str2 = "我爱北京天安门";

字段

字段中存放的是当前类或接口声明的字段信息。

如下图中,定义了两个字段a1和a2,这两个字段就会出现在字段这部分内容中。同时还包含字段的名字、描述符(字段的类型)、访问标识(public/private static final等)。

类加载器ClassLoader

类加载器会通过二进制流的方式获取到字节码文件的内容,接下来将获取到的数据交给Java虚拟机,虚拟机会在方法区和堆上生成对应的对象保存字节码信息。

主要分为四类:

根加载器(启动类加载器):

  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。

扩展类加载器:

  • 默认加载Java安装目录/jre/lib/ext下的类文件

应用程序类加载器(系统类加载器):

  • 默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。

用户自定义类加载器

输出为null是因为根加载器的具体实现是由C或C++编写,不在java范围内。 

运行时数据区

程序计数器

每个线程都有一个私有的程序计数器,也就是一个指针,指向方法区中的方法字节码(用来存储指向指令的地址),所占空间可以忽略不计。

在加载阶段,虚拟机将字节码文件中的指令读取到内存之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址。

Java方法栈(虚拟机栈)

栈中是没有垃圾回收的,线程结束后内存会自动释放。栈主管程序运行、生命周期、线程同步。

Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存

栈帧

stack1的方法结束后要弹出栈,此时需要通过stack1返回下面的stack2的方法。 

栈帧组成

  • 局部变量表,局部变量表的作用是在运行过程中存放所有的局部变量

  • 操作数栈,操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域

  • 帧数据,帧数据主要包含动态链接、方法出口、异常表的引用

本地方法栈 

Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。

在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。

Heap堆

public class Test {    
    public static void main(String[] args) {        
        Student s1 = new Student();        
        s1.name = "张三";       
        s1.age = 18;       
        s1.id = 1;
        s1.printTotalScore();        
        s1.printAverageScore();        
        
        Student s2 = new Student();       
        s2.name = "李四";        
        s2.age = 19;        
        s2.id= 2;        
        s2.printTotalScore();        
        s2.printAverageScore();    
    }
}

这段代码中通过new关键字创建了两个Student类的对象,这两个对象会被存放在堆上。在栈上通过s1s2两个局部变量保存堆上两个对象的地址,从而实现了引用关系的建立。

分区

一个JVM实例只有一个堆内存,堆内存大小可以调节,类加载器读取类文件后要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,堆内存在逻辑上分为三部分:

  • 新生代
    • 伊甸区
    • 幸存0区 from
    • 幸存1区 to
  • 养老代
  • 永久代(JDK8以后叫元空间

 垃圾指JVM中没有任何引用指向它的对象

Full  GC

当新生代空间不足时,会触发Minor GC,只清理新生代内存。而当老年代空间不足或者为了整理碎片化的内存,会触发Full GC,对整个堆内存进行回收。

Full GC 可能会导致较长的停顿时间,因为它需要扫描整个堆内存,标记可回收对象,并进行内存整理。这意味着在 Full GC 过程中,应用程序的执行会被暂停。

Full GC 的频率会受多种因素影响,如堆内存的大小、JVM配置参数、对象分配速度等。如果 Full GC 发生过于频繁或耗时过长,可能会导致应用程序的性能下降。

为了减少 Full GC 的频率和时间,可以采取以下策略:

  • 调整堆内存大小:适当设置堆内存大小,避免过小或过大的情况。
  • 优化对象分配:减少临时对象的创建和使用,避免过多的对象进入老年代。
  • 设置合适的垃圾回收器:根据应用程序的需求和性能特点,选择合适的垃圾回收器和相应的配置参数。
  • 进行代码优化:减少内存泄漏和不必要的对象引用,使垃圾回收更高效。

什么情况下会触发FULL GC ? 

  • Minor GC后老年代空间不足:Minor GC时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
  • 显式调用System.gc():尽管调用System.gc()方法不能保证立即进行Full GC,但它可以向JVM建议执行垃圾回收操作,包括Full GC。不过,频繁调用System.gc()是不推荐的。
  • 永久区空间不足(仅适用于JVM 8及之前版本):在传统的JVM版本中,永久区用于存储类和方法相关信息。如果永久区空间不足就触发Full GC来清理永久区。
  • CMS初始化标记阶段出现Promotion Failed:CMS回收器(Concurrent Mark Sweep)是一种用于减少停顿时间的垃圾回收器。在CMS的初始化标记(InitialMark)阶段,如果发现无法为所有存活对象标记,可能会触发Full GC。

Minor GC

Minor GC主要负责回收年轻代的垃圾对象。年轻代通常分为三个区域:一个伊甸区和两个幸存区(一般称为From区和To区)。当对象被创建时,它们会被分配到伊甸区。在年轻代的垃圾回收过程中,首先会对伊甸区进行垃圾回收,将存活的对象复制到一个空闲的幸存区中(通常是To区),同时清空伊甸区。Minor GC会一直重复这样的过程,直到To区被填满,To区被填满之后,会将所有对象移动到老年代中。 

在多次Minor GC后,存活时间较长的对象会逐渐被移到幸存区,并经过多次复制和清理的过程。当对象经历了一定次数的复制后,会被认为是长时间存活的对象,最终会被晋升到老年代。

Minor GC通常是并行或并发执行的,意味着在垃圾回收期间,应用程序的执行可能会暂停或降低速度。为了减少这种停顿时间,一些垃圾回收器,如并行垃圾回收器(Parallel GC)和G1垃圾回收器(Garbage-First GC),采用了并发标记和清理的方式。

什么情况下会触发Minor GC ? 

Minor GC的触发条件是由JVM自动管理的,具体条件可能因不同的JVM实现和垃圾回收器而有所不同,Minor GC会在以下情况下触发:

  • 对象分配:当应用程序创建新对象时,首先将其分配到年轻代的伊甸区。如果伊甸区没有足够的空间来容纳新对象,则会触发Minor GC。
  • 存活对象晋升:当年轻代经历了多次垃圾回收后,仍然存活的对象会被移到幸存区。当Survivor区无法容纳所有存活的对象时,一部分对象将被晋升到老年代。在晋升对象时,也可能触发MinorGC。
  • 动态年龄判定:在年轻代进行垃圾回收时,会根据对象的年龄来决定是否晋升到老年代。具体地,当某个对象经过一次Minor GC后仍然存活,并且达到一定的年龄阈值(通常是15岁),则会直接晋升到老年代。这个过程也会触发Minor GC。

Minor GC通常会频繁发生,但每次垃圾回收的停顿时间较短。在应用程序设计和调优中,可以通过适当配置堆大小和调整垃圾回收相关的参数来平衡Minor GC的频率和停顿时间,以达到更好的性能表现。

Major GC

Major GC是指对老年代进行的垃圾回收操作。在Java堆内存中,老年代用于存放生命周期较长的对象或者经过多次Minor GC后仍然存活的对象。Major GC的执行时间一般比Minor GC更长,因为它需要处理较多的对象和进行更复杂的内存整理操作。在Major GC期间,应用程序的执行将会暂停,直到垃圾回收操作完成。

什么情况下会触发Major GC?

具体的Major GC触发条件可能因不同的JVM实现和垃圾回收器而有所不同,Major GC在Java虚拟机中会在以下情况下被触发:

  • 老年代空间不足:当老年代无法容纳新对象或晋升对象时,会触发Major GC来回收老年代的垃圾对象。这种情况通常发生在频繁创建大对象或者持久对象导致老年代空间快要满了的情况下。
  • 晋升失败:在年轻代中的对象经过多次Minor GC后仍然存活并且达到了晋升的条件,但是老年代空间不足以容纳它们时,也会触发Major GC。这种情况可能是因为年轻代中的对象生命周期较长,导致垃圾对象聚集在老年代中。
  • 空间分配担保失败:在进行Minor GC时,如果老年代的连续内存空间不足以容纳晋升对象,JVM会尝试进行一次Minor GC并且通过移动对象来释放更多的连续空间。如果这个过程之后仍然无法满足空间需求,那么会触发Major GC。
  • 显式调用:通过System.gc()或Runtime.getRuntime().gc()等方式显式调用垃圾回收,也有可能触发Major GC。不过请注意,Java虚拟机对于显式调用垃圾回收的处理是可选的,因此并不保证一定会触发Major GC。

Major GC和Full Gc的区别

执行对象:

  • Major GC:主要对老年代(Tenured Generation)进行垃圾回收操作,清理长生命周期的对象或经过多次Minor GC后仍然存活的对象。
  • Full GC:涵盖了整个堆内存,包括年轻代和老年代,在进行垃圾回收时会同时处理这两个区域的对象。

目的:

  • Major GC:专注于回收老年代中的垃圾对象,以释放老年代的内存空间。
  • Full GC:除了回收老年代中的垃圾对象外,还会执行其他与垃圾回收相关的任务,如处理永久代中的无效类及常量,并进行堆内存的整理和碎片整理等工作。

触发条件:

  • Major GC:由JVM自动触发,通常在老年代空间不足、晋升对象或永久代垃圾回收等情况下触发。
  • Full GC:触发条件相对复杂,可能在年轻代无法容纳对象、永久代满了、显式调用System.gc()等情况下触发。

停顿时间:

  • Major GC:执行时间相对较短,因为它只关注回收老年代的垃圾对象。
  • Full GC:执行时间较长,因为它需要同时回收整个堆内存,并执行一些更为耗时的操作,如处理永久代中的无效类、堆内存的整理等。Full GC期间,应用程序的执行将会暂停。
  • OOM说明JVM堆内存不够,原因如下:
    1)jvm堆内存设置不够,可以通过参数-Xms(初始值大小)、-Xmx(最大大小)来调整
    2)代码中创建了大量对象,并且长时间无法回收或者出现死循环
  • 永久区是常驻内存区域,用于存放JDK自身携带的class、interface的元数据,也就是说它存储的是运行环境必须的类信息,永久区的数据不会被回收,只有jvm关闭才会释放占用的内存。OOM一般就是因为程序启动需要加载大量第三方jar包,例如tomcat部署太多应用或者大量动态反射生成的类被加载,最终导致永久区被占满。

方法区

方法区被所有线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。所有字段和方法字节码及构造函数等特殊方法的信息都保存在该区域。

主要包含三部分内容:

  • 类的元信息,保存了所有类的基本信息

  • 运行时常量池,保存了字节码文件中的常量池内容

  • 字符串常量池,保存了字符串常量

成员变量、局部变量、类变量分别存储在内存的什么地方?

类变量

  • 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁
  • 在java8之前把静态变量存放于方法区,在java8时存放在堆中

成员变量

  • 成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

双亲委派

过程

应用程序类加载器(又叫系统类加载器)收到类的加载请求先检查自己是否加载过该类,如果没有,将请求向上委托给自己的父类加载器(extensionLoader),如果父类加载器也没有加载过该类,该父类加载器继续向上委托给自己的父类加载器(bootstrapLoader,又叫根加载器、启动类加载器)若启动类加载器也没有加载过该类,则会根据要加载的类的全限定名尝试加载该类,若加载成功,则返回引用,若加载失败,则抛出异常,并反向委托给扩展类加载器,若仍加载失败,则继续抛出异常,并反向委托给应用程序类加载器,若仍加载失败,则报异常ClassNotFound。

安全性和沙箱机制

由于java核心库和扩展库由根加载器加载,这些库中的类有更高的安全级别,而应用程序类由应用程序类加载器加载,安全级别低,双亲向上委派可以防止核心API被篡改,提高了程序安全性。

什么是沙箱?

java安全模型的核心就是java沙箱,沙箱是一个限制程序运行的环境,沙箱机制就是把java代码限定在jvm的特定运行范围内,严格限制代码对本地系统资源的访问(CPU、内存、文件系统、网络等),通过这样来保证代码的有效隔离,防止对本地系统造成破坏。

避免类重复加载

由于父类加载器加载类时会优先尝试加载,若类已经被加载过,就不会再次加载,避免了类重复加载 

破坏双亲委派

打破双亲委派机制历史上有三种方式,但本质上只有第一种算是真正的打破了双亲委派机制:

  • 自定义类加载器并且重写loadClass方法。Tomcat通过这种方式实现应用之间类隔离。

  • 线程上下文类加载器。利用上下文类加载器加载类,比如JDBC和JNDI等。

  • Osgi框架的类加载器。历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载,目前很少使用。

Tomcat破坏

JDBC破坏 

JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。DriverManager类位于rt.jar包中,由启动类加载器加载。依赖中的mysql驱动对应的类,由应用程序类加载器来加载。DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制存疑

JDBC案例中真的打破了双亲委派机制吗?

最早这个论点提出是在周志明《深入理解Java虚拟机》中,他认为打破了双亲委派机制,这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,所以打破了双亲委派机制。

但是如果我们分别从DriverManager以及驱动类的加载流程上分析,JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制。

所以我认为这里没有打破双亲委派机制,只是用一种巧妙的方法让启动类加载器加载的类,去引发的其他类的加载。

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值