JVM总结

JVM的内存模型

示意图

在这里插入图片描述
java 虚拟机主要分为以下几个区:
在这里插入图片描述

1. 元空间(方法区)

在 JDK 8 及之前的版本,JVM 方法区(Method Area)被称为“永久代”(Permanent Generation),在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载

b. 方法区主要用来存储被虚拟机加载的类的信息常量(static final)、静态变量(static)和即时编译器编译后的代码等数据。

c. 该区域是被线程共享的。

d. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

从永久代到元空间

在Java 7及之前的版本中,JVM将类信息、静态变量、常量等数据存储在一个叫做PermGen(永久代)的内存区域中。PermGen的大小是有限制的,而且默认情况下不会进行垃圾回收,所以如果应用程序不断加载大量的类,就会导致PermGen空间耗尽,出现OutOfMemoryError异常。

在Java 8中,永久代被Metaspace(元空间)所取代。Metaspace是一块位于堆内存中的空间,用于存储类的元数据信息,如类名、访问修饰符、字段、方法等。相比于永久代,Metaspace没有固定的大小限制,而是根据应用程序的实际需要进行动态分配。同时,Metaspace也支持垃圾回收,所以不用再担心PermGen空间的问题。

变化的原因主要是因为PermGen会带来很多问题,如内存泄漏、性能问题等。Metaspace的引入是为了解决这些问题,并提供更加灵活的内存管理方式,让JVM更加稳定和高效。

2. 虚拟机栈/JAVA栈

a. 虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表操作数栈、动态链接和方法出口等信息。

b. 虚拟机栈是线程私有的,它的生命周期与线程相同

c. 局部变量表里存储的是基本数据类型returnAddress 类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
d. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式

e. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

3. 本地方法栈:

本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。

JVM中的Native方法是一种特殊的Java方法,它并没有在Java虚拟机中实现,而是使用底层的C/C++语言实现。因此,它是一种与平台相关的方法。
在Java中,使用关键字“native”声明的方法就是Native方法。Java中的Native方法可以通过Java本地接口(Java Native Interface,简称JNI)与底层C/C++语言进行交互。Java程序可以调用Native方法,而Native方法可以调用C/C++语言编写的库,从而扩展Java程序的功能。
Native方法通常用于与操作系统、硬件或其他与Java平台不兼容的软件进行交互。例如,Java中的许多标准类库中的方法(如System.arraycopy()、System.currentTimeMillis()等)就是通过Native方法实现的。此外,一些开源软件库,如OpenCV、FFmpeg等,也提供了Java Native接口,以方便Java程序员使用底层C/C++语言编写的库。
需要注意的是,使用Native方法需要谨慎。因为Native方法使用的是底层C/C++语言编写的代码,因此容易受到缓冲区溢出、空指针引用等问题的影响,从而导致系统崩溃或者安全漏洞。因此,在使用Native方法时,需要仔细考虑安全性和可靠性,并对底层代码进行充分的测试和验证。

4. 堆:

java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,当程序创建对象时,Java虚拟机会在堆区中分配一块内存来存放对象实例。因此该区域经常发生垃圾回收操作

5. 程序计数器:

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。
每个线程都有一个独立的程序计数器,它们互不干扰,线程切换时,程序计数器也会切换到相应的线程中。

JVM的OOM(Out Of Memory)是指Java虚拟机中的内存用尽错误,即在程序运行时申请不到足够的内存空间导致的异常情况。当JVM中的内存空间无法满足程序的内存需求时,就会抛出OOM错误。

JVM里面内存寻址怎么对应到操作系统管理的内存

在计算机中,JVM使用虚拟内存来管理其内存。虚拟内存是一种抽象层,它将计算机的物理内存和磁盘存储设备组合在一起,形成一个连续的地址空间。JVM将其需要的内存映射到虚拟地址空间,而不是直接访问物理内存地址。

当JVM创建一个新对象时,它会向操作系统请求一块内存。操作系统会为JVM分配一块虚拟内存地址,并将其映射到物理内存或磁盘存储器中的某个位置。JVM使用这个虚拟内存地址来访问对象,而不需要知道其在物理内存中的确切位置。

JVM使用了一些技术来优化虚拟内存的使用,包括内存分页、缓存和垃圾回收。这些技术可以帮助JVM更有效地管理内存,并减少内存碎片化和垃圾回收的开销。

总的来说,JVM的内存在计算机上进行寻址的过程是通过虚拟内存地址实现的。JVM将需要的内存映射到虚拟地址空间,而不是直接访问物理内存地址。

对象的创建过程

以下内容基于HotSpot VM 分代模型

这张图其实就能完整的说明一个对象的创建过程到底发生了什么,很多朋友可能一下看不懂,那么我们就跟着左上角的一步一步来:

  1. 一个对象new出来先判断线程栈是否能分配下 如果能分配下,直接分配在栈中。如果分配不下则进行第二步。
  2. 判断该对象是否足够大 如果足够大,则直接进入老年代。如果不够大,则进行第三步。判断创建对象的线程的TLAB(本地线程缓冲区)空间是否足够 如果足够,直接分配在TLAB中。
  3. 如果不够,则进入Eden区中其他空间。然后进行第四步。
  4. GC清除 如果清除掉了该对象,则直接结束。如果没有清除掉对象,进行第5步。
  5. 此刻对象进入Survivor 1 区,判断年龄是否足够大 如果年龄足够大,则直接进入old区域。
  6. 如果年龄不够大,则进入Survivor 2 区,然后进入第4步,循环往复。
    通过这张流程图和步骤解析大家应该对一个对象的创建过程有一个很清晰的概念了,但是其中还是有很多小细节会被忽略,为什么jvm会在对象的创建过程中大作文章,会分这么多种情况?为了让大家更深入的能够理解它,我们就再来看看下面这几个问题:
    为什么对象会选择先分配在栈中?
    首先栈是线程私有的,将对象优先分配在栈中,可以通过pop直接将对象的所有信息,空间直接清除,当线程消亡的时候也可以直接清理这一块儿TLAB区域。
    *为什么会选择先进入TLAB?
    TLAB是线程本地缓冲区,TLAB的好处就是防止不同线程创建对象选择同一块儿内存区域而产生竞争,会使其概率大大减少。

对象的内存布局

我们回到文章的标题,Object o = new Object();到底占用多少个字节?这道题的目的其实就是考验看你对对象的内存布局了解的是否清晰,先上图:

在java中对象的内存布局分为两种情况,非数组对象和数组对象数组对象和非数组对象的区别就是需要额外的空间存储数组的长度length

对象头

对象头又分为MarkWordClass Pointer两部分。
MarkWord:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等,在32位系统占4字节,在64位系统中占8字节
ClassPointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节
Length:只在数组对象中存在,用来记录数组的长度,**占用4字节 **
Instance dataInstance data:对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)PaddingPadding:Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是8个字节,所以为了完整,padding的作用就是补充字节,保证对象是8字节的整数倍。 moon在上文特意标注了32位系统和64位系统不同区域占用空间大小的区别,这是因为对象指针在64位JVM下的寻址更长,所以想比32位会多出来更多占用空间。 但是现在假设一个场景,公司现在项目部署的机器是32位的,你们老板要让你将项目迁移到64位的系统上,但是又因为64位系统比32位系统要多出更多占用空间,怎么办,因为正常来说我们是不需要这一部分多余空间的,所以jvm已经帮你考虑好了,那就是指针压缩。

指针压缩

-XX:+UseCompressedOops 这个参数就是JVM提供给你的解决方案,可以压缩指针,将占用的空间压缩为原来的一半,起到节约空间的作用,classpointer参数大小就受到其影响

Object o = new Object()到底占用多少个字节?

通过刚才内存布局的学习后,这个问题就很好回答了,面试官其实就是想问你对象的内存布局是怎样的,我们这里就针对这个问题的结果分析下。
这里分两种情况:在开启指针压缩的情况下markword占用8字节classpoint占用4字节,Instance data无数据,总共是12字节,由于对象需要为8的整数倍Padding会补充4个字节,总共占用16字节的存储空间。
没有指针压缩的情况下markword占用8字节classpoint占用8字节,Instance data无数据,总共是16字节

为什么要内存对齐?

防止伪共享

什么是伪共享?

随着CPU和内存的发展速度差异的问题,导致CPU的速度远远快于内存,所以一般现在的CPU都加入了高速缓存,就是常说的解决不同硬件之间的性能差异问题。
这样的话,很简单的道理,加入了缓存,就必然会导致缓存一致性的问题,由此,又引入了缓存一致性协议。(如果你不知道,建议去百度一下,这里不做展开)
CPU缓存,顾名思义,越贴近CPU的缓存速度越快,容量越小,造价成本也越高,而高速缓存一般可以分为L1、L2、L3三级缓存,按照性能的划分:L1>L2>L3。
在这里插入图片描述
而事实上,数据在缓存内部都是按照来存储的,这就叫做缓存行。缓存行一般都是2的整数幂个字节,一般来说范围在32-256个字节之间,现在最为常见的缓存行的大小在64个字节。
所以,按照这个存储方式,缓存中的数据并不是一个个单独的变量的存储方式,而是多个变量会放到一行中。
我们常说的一个例子就是数组和链表,数组的内存地址是连续的,当我们去读取数组中的元素时,CPU会把数组中后续的若干个元素也加载到缓存中,以此提高效率,但是链表则不会,也就是说,内存地址连续的变量才有可能被放到一个缓存行中。
在多个线程并发修改一个缓存行中的多个变量时,由于只能同时有一个线程去操作缓存行,将会导致性能的下降,这个问题就称之为伪共享。为什么只有一个线程能去操作?我们举个实际的栗

JIT

JIT 是 Just-In-Time 的缩写,即动态编译器,它是 Java 虚拟机(JVM)的一部分。JIT 可以在运行时将 Java 字节码编译为本地机器代码,并将编译后的代码缓存起来以便下次使用,这样可以提高 Java 应用程序的性能。JIT 可以根据程序的实际运行情况,动态地决定哪些代码需要编译,以及采用何种编译策略。这种动态编译技术是 Java 虚拟机与其他解释型语言的主要区别之一

JDK代理与CGLib

Java编译到执行的过程

共分为4个步骤:编译->加载->解释->执行

编译:将源码文件编译成JVM可以解释的class文件。

编译过程会对源代码程序做 「语法分析」「语义分析」「注解处理」等等处理,最后才生成字节码文件。
比如对泛型擦除和我们经常用的Lombok就是在编译阶段干的。

加载:将编译后的class文件加载到JVM中。

在加载阶段又可以细化几个步骤:装载->连接->初始化

候选者:【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)

候选者:【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上)

候选者:【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

候选者:装载这个阶段它做的事情可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中

Java 类加载

类加载机制:java 虚拟机将编译后的 class 文件加载到内存中,进行校验、转换、解析和初始化,到最终的使用。这就是 java 类加载机制。包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)等阶段。

加载流程(Parent-delegate)

先检查类是否已经加载过,如果没有则会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。按照由父级到子集的顺序,类加载器主要包含以下几个:

类加载器

〇、类加载器是干什么的?

初学Java的时候,你应该用命令行编译过Java文件。Java代码通过javac编译成class文件,而类加载器的作用,就是把class文件装进虚拟机。

面试请回答:将"通过类的全限定名获取描述类的二进制字节流"这件事放在虚拟机外部,由应用程序自己决定如何实现。
宏观来看,只有两种类加载器:启动类加载器其他类加载器

启动类加载器属于虚拟机的一部分,它是用C++写的,看不到源码;其他类加载器是用Java写的,说白了就是一些Java类,一会儿就可以看到了,比如扩展类加载器、应用类加载器。

启动类加载器:Bootstrap ClassLoader
扩展类加载器:Extention ClassLoader
应用类加载器:App ClassLoader (也叫做“系统类加载器”)
既然只是把class文件装进虚拟机,为什么要用多种加载器呢?因为Java虚拟机启动的时候,并不会一次性加载所有的class文件(内存会爆),而是根据需要去动态加载
而且每个类加载器都有自己的命名空间,它只能看到其加载的类和其父类加载器所加载的类,而不能看到其他类加载器所加载的类。
1)最顶级-启动类加载器(Bootstrap ClassLoader):负责加载 <JAVA_HOME>\lib 或称为jre和jre/lib目录下的核心库,具体路径要看你的jre安装在哪,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如 rt。jar,名字不符合的类库即使放在lib目录中也不会被加载)。

2)中级-扩展类加载器(Extension ClassLoader):负责加载 <JAVA_HOME>\lib\ext 目录中的,或被 java。ext。dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3)次级-应用程序类加载器(Application ClassLoader):负责加载用户路径(ClassPath)上所指定的类库,也就是我们自己的Java代码编译成的class文件所在开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器,

除了启动类加载器(BootstrapClassLoader),每个类加载器都有一个父加载器。比如刚才的应用类加载器,它的父加载器是扩展类加载器。你可能会说扩展类加载器的parent是null,所以它没有父加载器?

有,它的父加载器就是BootstrapClassLoader任何parent为null的加载器,其父加载器为BootstrapClassLoader,先记住这个结论,很快你会看到原因。

最后一个问题,如果你直接继承ClassLoader自己实现一个类加载器,且不指定父加载器,那么这个自定义类加载器的父加载器是什么?

是应用类加载器AppClassLoader。可以拉回去看看ClassLoader的无参构造器。

图1. 父加载器关系
#### 四、双亲委派模型 有一个描述类加载器加载类过程的术语:双亲委派模型。然而这是一个很有误导性的术语,它应该叫做单亲委派模型(Parent-Delegation Model)。但是没有办法,大家都已经这样叫了。所谓双亲委派,这个亲就是指ClassLoader里的全局变量parent,也就是父加载器。

双亲委派的具体过程如下:

  1. 当一个类加载器接收到类加载任务时,先查缓存里有没有,如果没有,将任务委托给它的父加载器去执行
  2. 父加载器也做同样的事情,一层一层往上委托,直到最顶层的启动类加载器为止
  3. 如果启动类加载器没有找到所需加载的类,便将此加载任务退回给下一级类加载器去执行,而下一级的类加载器也做同样的事情。
  4. 如果最底层类加载器仍然没有找到所需要的class文件,则抛出异常。
五、为什么要双亲委派?

确保类的全局唯一性。

如果你自己写的一个类与核心类库中的类重名,会发现这个类可以被正常编译,但永远无法被加载运行。因为你写的这个类不会被应用类加载器加载,而是被委托到顶层,被启动类加载器在核心类库中找到了。如果没有双亲委托机制来确保类的全局唯一性,谁都可以编写一个java.lang.Object类放在classpath下,那应用程序就乱套了。

从安全的角度讲,通过双亲委托机制,Java虚拟机总是先从最可信的Java核心API查找类型,可以防止不可信的类假扮被信任的类对系统造成危害。
Java可以保证类的唯一性和安全性,避免不同类加载器之间的类冲突,同时也提高了类加载的效率,避免重复加载类。

六、哪些场景需要打破双亲委派模式

在 Java 中,类加载器采用了双亲委派模式,即当一个类加载器需要加载一个类时,它会先将这个任务委托给它的父类加载器,如果父类加载器无法加载,则由它自己来加载。

但是,有些场景需要打破双亲委派模式,例如:

  1. 应用程序需要加载第三方库时,由于第三方库可能与 JDK 自带的库冲突,因此需要使用自定义的类加载器来加载第三方库,从而隔离不同版本的库。

  2. 应用程序需要动态加载类时,由于这些类在编译时无法确定,因此需要使用自定义的类加载器来动态加载这些类。

  3. 应用程序需要在运行时修改类的字节码时,由于系统类加载器无法加载修改后的字节码,因此需要使用自定义的类加载器来加载修改后的字节码。

JVM的类的验证过程

JVM中的类的验证过程是指在**将字节码文件加载到内存并进行解析后,对字节码文件进行验证的过程。**类的验证是JVM保证字节码的正确性和安全性的重要步骤之一。

类的验证过程可以分为以下几个步骤:

  1. 文件格式验证:验证字节码文件是否符合JVM规范的格式要求,包括魔数、版本号、常量池、访问标志等。

魔数是指Java虚拟机在识别class文件时,用于确定该文件是否为有效的class文件的标志。在Java虚拟机规范中,规定了每个class文件的前四个字节必须是固定的魔数值,即0xCAFEBABE。

  1. 元数据验证:对类的元数据进行验证,包括父类、接口、字段、方法等信息是否正确,以及访问权限等是否符合要求。

  2. 字节码验证:对字节码进行验证,检查字节码的操作数栈、局部变量表、异常表等是否符合规范,以及类型转换是否安全等。

  3. 符号引用验证:检查符号引用是否正确,包括类、方法、字段等是否存在,以及访问权限等是否符合要求。

在Java虚拟机中,符号引用(Symbolic Reference)指的是一组符号来描述目标方法或字段,而不是直接指向目标方法或字段的内存地址。
当Java程序中使用到某个类、方法或字段时,Java编译器并不知道这些实体在内存中的具体位置,而是使用符号引用来描述它们。例如,当调用一个类的方法时,Java编译器会生成一个符号引用,其中包含了该方法所在类的全限定名、方法名、参数列表以及返回值类型等信息。
符号引用的优点是可以在程序执行过程中动态链接目标实体,从而实现更灵活的编程。例如,当程序调用某个方法时,Java虚拟机会通过符号引用来查找该方法的具体位置,并将其动态链接到调用点上,从而实现方法的调用。
需要注意的是,符号引用与直接引用(Direct Reference)不同。直接引用是指直接指向目标方法或字段在内存中的位置的引用,可以直接被程序使用。而符号引用需要经过解析等过程才能转化为直接引用,因此在程序执行过程中需要消耗额外的时间和空间。

  1. 内部一致性验证:对类的内部结构进行验证,包括是否存在不一致的继承、重载、覆盖等情况。

  2. 安全性验证:对字节码进行安全性验证,包括是否存在可能导致类型安全问题的指令,例如类型转换、数组越界、空指针等。

如果在验证过程中发现任何问题,JVM将会抛出相应的异常,防止类的执行可能会导致安全问题或者程序运行错误。只有经过完整的验证过程并通过验证的类才能够被JVM加载、解析、初始化和执行。

JVM 使用哪种字符表示

Java 虚拟机使用 Unicode 字符集来表示字符串。Unicode 是一种字符集,它定义了各种字符的编码方式,可以用来表示几乎所有语言中的字符。

应该在哪里调整JVM的参数?

JVM 的参数可以通过多种方式进行配置,常见的方式有:

命令行参数:在启动 Java 应用时,可以通过命令行参数来设置 JVM 的参数。例如,通过在命令行中添加“-Xmx1024m”参数,可以设置 JVM 最大可用内存为 1024MB。

环境变量:可以通过设置环境变量来影响 JVM 的行为。例如,设置“JAVA_TOOL_OPTIONS”环境变量可以指定 JVM 参数,如“-Xmx1024m”。

配置文件:JVM 的参数可以在配置文件中进行配置。例如,可以在“jvm.options”文件中设置参数,这个文件通常位于应用程序的安装目录下。

JVM API:在应用程序代码中,可以通过 JVM API 来设置 JVM 的参数。例如,在 Java 代码中通过“System.setProperty()”方法设置 JVM 参数。

具体选择哪种方式来配置参数,取决于应用程序的部署方式和具体需求。在生产环境中,通常采用配置文件或命令行参数的方式来设置 JVM 参数,以确保参数的准确性和可维护性。

JVM的垃圾回收

JVM的年轻代、老年代、伊甸园区、幸存者区

JVM 的内存分为堆和栈两部分,其中堆内存又可以分为年轻代老年代。年轻代是用于存储新创建的对象的内存区域,而老年代则用于存储长时间存活的对象。为了更加高效地管理内存,年轻代又被分为伊甸园区和幸存者区。

伊甸园区(Eden Space)是年轻代的一部分,用于存放新创建的对象。当伊甸园区内存满时,会触发一次 Minor GC,将伊甸园区中的垃圾对象清理掉,并将存活的对象移动到幸存者区。

幸存者区(Survivor Space)是年轻代的另一部分,每个幸存者区都有一个对应的同龄区,用于存放从上一次 Minor GC 中幸存下来的对象。当一个幸存者区被填满时,会将其中的存活对象移动到对应的同龄区,如果同龄区也被填满了,就会将存活对象移动到老年代。

老年代(Tenured Generation)用于存放长时间存活的对象。当老年代内存满时,会触发一次 Major GC,将老年代中的垃圾对象清理掉。

通过将堆内存分为年轻代和老年代,可以有效地减少垃圾回收的频率和时间,从而提高 JVM 的性能和稳定性。同时,通过将年轻代分为伊甸园区和幸存者区,也可以更加高效地管理内存,避免出现内存溢出等问题。

新⽣代和⽼年代给的⽐例可以调整吗?伊甸园区和幸存者区呢?

可以通过调整 JVM 的参数来调整新生代和老年代的比例,以及伊甸园区和幸存者区的比例。常用的参数包括:

  • -Xmn:设置年轻代的大小;
  • -Xms:设置 JVM 初始分配的内存大小;
  • -Xmx:设置 JVM 最大分配的内存大小;
  • -XX:NewRatio:设置新生代和老年代的比例,默认为2,表示年轻代和老年代的比例为1:2;
  • -XX:SurvivorRatio:设置幸存者区和伊甸园区的比例,默认为8,表示每个幸存者区占年轻代的1/8。
    例如,如果要将年轻代和老年代的比例改为1:3,可以设置参数:-XX:NewRatio=1:3;如果要将幸存者区和伊甸园区的比例改为1:4,可以设置参数:-XX:SurvivorRatio=4。
Java的对象⼀定⼀开始在伊甸园区吗?

Java 对象并不一定一开始就在 Eden 区(伊甸园区)中创建。在 Java 堆中创建对象时,对象的具体位置和存储方式由 JVM 决定,这取决于许多因素,例如对象的大小、当前堆空间的使用情况、垃圾收集算法等等。

一般来说,小对象会在 Eden 区中创建,大对象则会直接在老年代中创建,而一些对象可能会在 Survivor 区中创建,这取决于它们的生命周期和垃圾收集算法的策略。同时,JVM 还提供了一些参数来控制 Java 对象的创建和存储,例如 -Xmn 参数可以指定 Eden 区的大小。

总之,Java 对象的创建和存储是由 JVM 决定的,并且受到许多因素的影响,因此不能一概而论所有对象都会在 Eden 区中创建。

Minor GC和Major GC

Minor GC是一种短暂的垃圾回收过程,它通常只清理年轻代内存区域。年轻代是Java虚拟机堆内存中的一部分,用于存放新创建的对象。Minor GC会检查年轻代中哪些对象已经不再使用,然后将它们标记为垃圾并进行清理。通常情况下,Minor GC是频繁执行的,因为大部分对象都是短暂的,而且它们很快就会被回收。

Major GC也被称为Full GC,是一种耗时较长的垃圾回收过程,它会清理整个Java虚拟机堆内存中的对象,包括年轻代和老年代。老年代是Java虚拟机堆内存中的另一部分,用于存放长期存在的对象。Major GC通常发生在当Java虚拟机堆内存已经满了,或者是在老年代中没有足够的连续空间来分配新的对象时。

总的来说,Minor GC和Major GC都是Java虚拟机中的垃圾回收过程,用于清理不再使用的对象。Minor GC通常比Major GC频繁执行,而Major GC则通常会耗费更多的时间。

常用的 GC 算法

引用计数法 应用于:微软的COM/ActionScrip3/Python等

该算法维护每个对象的引用计数,当一个对象被引用时,它的引用计数加1,当引用失效时,它的引用计数减1。当一个对象的引用计数为0时,说明它不再被使用,可以被回收。该算法的优点是实现简单,但它无法处理循环引用的情况。
如果对象没有被引用,就会被回收,缺点:需要维护一个引用计算器

复制算法 年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

该算法将内存分为两个区域,一部分为存活对象的From区域,一部分为空的To区域。当From区域满时,将From区域中的存活对象复制到To区域,然后清空From区域,这样To区域就变成了新的From区域。该算法的优点是回收效率高,但需要两倍的内存空间。
优点:效率高,
缺点:需要内存容量大,比较耗内存
使用在占空间比较小、刷新次数多的新生区

标记清除

该算法分为两个阶段,第一阶段标记出所有存活的对象,第二阶段清除所有未标记的对象。该算法的优点是不需要额外的内存空间,但容易产生内存碎片。
效率比较低,会差生碎片。

标记压缩

算法是标记清除法的改进版本。在清除未标记对象的同时,将存活对象往一端移动,然后清理掉另一端的所有空闲内存,从而避免了内存碎片的产生。
效率低速度慢,需要移动对象,但不会产生碎片。

标记清除压缩:标记清除-标记压缩的集合,多次GC后才Compact

该算法是标记清除法和标记压缩法的综合。它首先使用标记清除法标记并清除所有未使用对象,然后使用标记压缩法对存活对象进行压缩,以消除内存碎片。
使用于占空间大刷新次数少的老年代,是3 4的集合体

Java虚拟机GC机制是什么

Java虚拟机中的垃圾回收机制是指,通过自动化的方式对程序中不再使用的对象进行清理和回收,从而释放内存空间,使程序更加高效地运行。

Java虚拟机中的垃圾回收机制基于“可达性分析”算法。垃圾回收器会从一组被称为“GC Roots”的对象开始遍历整个对象图,所有可达对象都被认为是存活的,而不可达的对象则被认为是垃圾,需要被回收。在遍历对象图的过程中,垃圾回收器会标记可达对象,并清除不可达对象所占用的内存空间,从而释放内存。

Java虚拟机中的垃圾回收机制通过多种垃圾回收器进行实现,例如 Serial、Parallel、CMS、G1 等。每种垃圾回收器都有自己的优点和适用场景,开发人员可以根据应用场景进行选择和配置。

Java虚拟机垃圾回收的收集器中种类有哪些

Serial收集器:使用单线程进行垃圾回收,适用于小型应用程序,且回收过程会造成应用程序停顿。

Parallel收集器:使用多线程进行垃圾回收,适用于中等大小的应用程序,可以通过 -XX:ParallelGCThreads 参数设置并行线程数。

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于并发“标记-清理”实现,在标记清理过程中不会导致用户线程无法定位引用对象。 仅作用于老年代收集。其共有以下4个执行步骤:

  1. 初始标记(CMS initial mark):独占CPU,stop-the-world, 仅标记GCroots能直接关联的对象,速度比较快;
  2. 并发标记(CMS concurrent mark):可以和用户线程并发执行,通过GCRoots Tracing 标记所有可达对象;
  3. 重新标记(CMS remark):独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
  4. 并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。
G1

G1收集器(Garbage First):采用分代回收算法,将堆内存分为多个大小相等的区域,并对每个区域进行垃圾回收,适用于大型应用程序和需要较短停顿时间的应用程序。
G1 收集器弱化了 CMS 原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个 个Region 1MB~32MB,默认 2048 个分区),这么做的目的是在进行收集时不必在全堆范围内进行。它主要特点在于达到可控的停顿时间,用户可以指定收集操作在多长时间内完成,即 G1提供了接近实时的收集特性。它的步骤如下:

  1. 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,伴随着一次普通的Young GC发生,并修改NTAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,此阶段是stop-the-world操作。
  2. 根区间扫描,标记所有幸存者区间的对象引用,扫描 Survivor到老年代的引用,该阶段必须在下一次Young GC 发生前结束。
  3. 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,该阶段可以被Young GC中断。
  4. 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,此阶段是stop-the-world操作,使用snapshot-at-the-beginning (SATB) 算法。
  5. 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,回收没有存活对象的Region并加入可用Region队列。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
ZGC

ZGC收集器:是一种低延迟的垃圾回收器,它是 JDK 11 中引入的。与其他垃圾回收器相比,ZGC 可以处理几乎任意大小的堆,并且可以在数毫秒的时间内完成 GC 操作,而不会对应用程序产生过多的停顿时间。

以下是 ZGC 的一些特点和优势:

低延迟:ZGC 可以在数毫秒的时间内完成垃圾回收操作,并且最大 GC 暂停时间不超过 10 毫秒。

大堆处理能力:ZGC 可以处理数百 GB 到数 TB 的堆大小,这使得它非常适合处理大型、内存密集型的应用程序。

并发收集:ZGC 使用了一种基于读屏障的算法来实现并发收集,这意味着应用程序可以继续运行,而不需要暂停。

分代收集:ZGC 采用了分代收集策略,它将堆分为年轻代和老年代,对它们采用不同的垃圾回收算法。

内存压缩:ZGC 支持内存压缩,可以在垃圾回收期间对内存进行压缩,以节省空间。

可伸缩性:ZGC 能够利用现代硬件的多核处理器和大量内存,从而具有很好的可伸缩性。

总之,ZGC 是一种适用于大型、内存密集型应用程序的垃圾回收器,它具有低延迟、大堆处理能力、并发收集、分代收集、内存压缩和可伸缩性等特点。在需要高性能和低延迟的情况下,ZGC 可以成为一种非常有用的工具。

serial收集器优势

Serial 垃圾回收器虽然不如其他并发垃圾回收器那样具有高并发性和低停顿时间,但在某些场景下仍然具有一定的优势。

首先,Serial 垃圾回收器是一种单线程的垃圾回收器,其设计的初衷是用于较小的应用或者客户端应用,因为这些应用对于响应时间和吞吐量的要求不是特别高。在这种情况下,Serial 垃圾回收器可以通过暂停应用程序,快速地回收垃圾,从而避免垃圾过多导致应用程序内存溢出的情况。

此外,Serial 垃圾回收器的内存占用比其他垃圾回收器更少,因此可以在内存资源有限的情况下使用。在一些嵌入式设备、移动设备等资源有限的场景下,Serial 垃圾回收器是一种较为理想的选择。

Parallel Scavenge是多线程的,为什么不适于并发垃圾收集器

Parallel Scavenge 垃圾回收器是一种多线程的垃圾回收器,其设计的初衷是为了在具有多核处理器的服务器上,实现高吞吐量的垃圾回收。在 Parallel Scavenge 垃圾回收器中,主要采用了“吞吐量优先”的设计思路,即追求系统整体吞吐量最大化,允许应用程序停顿时间较长,但在垃圾回收过程中可以充分利用多核处理器,以达到高吞吐量的目的。

虽然 Parallel Scavenge 垃圾回收器是一种多线程的垃圾回收器,但它并不适用于并发垃圾收集器的场景。这是因为 Parallel Scavenge 垃圾回收器的设计目标是追求高吞吐量,而不是低停顿时间。在垃圾回收过程中,Parallel Scavenge 垃圾回收器会尽可能地利用多个线程来回收垃圾,以达到高吞吐量的目的,但这也会导致应用程序的停顿时间较长,不适合对响应时间要求比较高的场景。

相比之下,并发垃圾收集器的设计目标是追求低停顿时间,允许应用程序继续执行,只在必要时才会停顿,以回收垃圾。这种设计思路可以适应对响应时间要求较高的场景,同时也可以充分利用多核处理器,实现高吞吐量的垃圾回收。因此,并发垃圾收集器比 Parallel Scavenge 垃圾回收器更适合需要高并发和低停顿时间的应用场景。

Java内存模型JMM

Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),它描述了Java程序中各种变量的访问规则,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。在多线程编程中,由于线程之间共享数据,因此需要保证线程之间对共享变量的读写操作是原子的、可见的、有序的。JMM提供了这些保证,以保证多线程程序的正确性和稳定性。JMM的实现在JVM中,它规定了Java程序中变量在内存中的存储和访问方式,并定义了多线程下的操作规则和顺序。在多线程编程中,了解JMM的规则和机制非常重要,可以避免由于线程并发访问共享数据而引起的一系列问题,如死锁、数据竞争、线程安全等。

Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。

一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。静态成员变量跟随着类定义一起也存放在堆上。存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

Java内存模型和硬件内存架构之间的桥接

在这里插入图片描述
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

线程之间的共享变量存储在主内存(Main Memory)中
每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

Java内存模型解决的问题

当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)。

1、多线程读同步与可见性

可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改

线程缓存导致的可见性问题:

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不可见的:共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。
解决这个内存可见性问题使用: Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。 Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。 Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)

2、多线程写同步与原子性

多线程竞争(Race Conditions)问题:当读,写和检查共享变量时出现race conditions。如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions。想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增加了两次,每个CPU缓存中一次。如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次:
在这里插入图片描述
解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。 使用原子性保证多线程写同步问题: 原子性:指一个操作是按原子的方式执行的。要么该操作不被执行;要么以原子方式执行,即执行过程中不会被其它线程中断。Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).Reads and writes are atomic for all variables declared volatile (including long and double variables).

JVM的调用与运行

要调用JVM中的javac编译器,首先安装JDK,并且需要将JDK的bin目录添加到系统路径中。然后,打开命令行终端或者PowerShell,并输入以下命令:

javac MyClass.java

其中,MyClass.java是您要编译的Java源代码文件的名称。当您运行此命令时,JVM将启动javac编译器,并使用它来将MyClass.java文件编译成可执行的Java字节码文件MyClass.class。

如果一切顺利,您应该可以在同一目录下找到一个新的MyClass.class文件,该文件可以通过以下命令来运行:

java MyClass

java MyClass
这将启动JVM,并使用它来运行MyClass.class文件中的Java应用程序。注意,这里的MyClass是不带扩展名的,因为JVM会自动查找并加载名为MyClass的.class文件。

在命令行中尝试使用javac命令时遇到问题,请确保已正确设置您的Java环境变量和路径,并且使用的是正确的命令提示符(例如,对于Windows,使用命令提示符而不是PowerShell)。

大对象

在 Java 中,大对象(Large Object,简称 LOB)通常是指占用大量内存的对象。具体来说,如果一个对象的大小超过了 JVM 中设置的阈值,就可以称之为大对象。

在 Java 8 中,JVM 默认将大对象的阈值设置为 16MB,如果一个对象的大小超过了这个阈值,就会被认为是大对象。当然,这个阈值可以通过设置 JVM 的参数来调整,例如通过“-XX:PretenureSizeThreshold”参数来设置大对象的阈值。

大对象在 Java 中通常需要特别处理,因为它们的创建和回收都需要占用大量的内存和 CPU 时间。在创建大对象时,可能需要进行额外的内存分配和拷贝操作,从而导致内存使用效率的降低。在回收大对象时,可能需要进行多次垃圾回收才能释放全部内存,从而影响垃圾回收的效率。

因此,在 Java 应用程序中,应该尽量避免创建过多的大对象,同时采取优化措施,如对象池、缓存等方式来减少大对象的创建和回收次数,从而提高应用程序的性能和稳定性。

JVM调优实战


FulIGC频繁,那么会触发stop the world。此时会导致我们的系统进行停顿,这个可能是导致我们的系统tp9 9耗时上升的主要原因。由于并发很高,我们的YoungGO频繁,那么可能会造成,我们有些本应该在YoungGC就回收的对象,没有回收成功,直接进入了老年代,由于对象的晋升,导致了我们的老年代继续触发FuIIGC。于是峰值变高
目标
I、YoungGC次数减少
z、YoungGC耗时减少
了、FulIGC不超过占次一天
4、FucIIGC耗时减少

JVM的路径

C:\Program Files\Java\jdk-11\bin\server\jvm.dll

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值