JKD关系梳理以及内存优化

JDK,JVM,JRE的区别联系

JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心。

JRE :英文名称(Java Runtime Environment),我们叫它:Java 运行时环境。它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库【记几个java重用类库】。它相对于 jvm 来说,多出来的是一部分的 Java 类库。

java.util: 集合,日期类型,集合的工具类

java.lang: 枚举包类型,对象,java反射类,线程

java.lang.concurrent:java(啃卡润特)的并发包具有原子性数据类型,锁,线程池。

java.io: 几个IO流。

JDK :英文名称(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。

显然,这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM。

接着,提出一个问题:为什么我们的电脑在装完 jdk 后会有两个版本的 jre?

因为我们的 jdk 安装成功后,在 C:\Program Files\Java 目录会是这样的(这里,我是装的 jdk1.8 的版本)

而 jdk 的子目录下也存在一个 jre。

这两个不同版本的 jre 相互之间有什么联系吗?

答案是:没有联系。甚至准确的来说,它俩是一样的,无论是用哪一个都是可以的。只是很多人习惯将会单独安装另一个 jre,虽然单独安装的 jre 也并没有被使用,原因可能就是刚开始大家都不清楚 jdk 和 jre 之间的关系,所以就默认的都安装上了。

在 jdk 的 bin 目录下,基本上都是一些可执行文件,并且它们还不大。其实这些可执行文件只是外层的一层封装而已,这样的目的是避免输入的命令过长。例如 javac.exe 内部调用的其实是 JDK 中 lib 目录中的 tools.jar 中 com.sun.tools.javac.Main 类,也就是说这些工具只是入口而已。而实际上它们本身又都是由 Java 编写的,所以在 jdk 目录下的 jre 既提供了这些工具的运行时环境,也提供了我们编写完成的 Java 程序的运行时环境。

所以,很明显,jdk 是我们的开发工具包,它集成了 jre ,因此我们在安装 jdk 的时候可以选择不再安装 jre 而直接使用 jdk 中的 jre 运行我们的 Java 程序。(但是大部分人都默认将两个都装上了)。但是如果你的电脑不是用来开发 Java 程序的,而仅仅是用来部署和运行 Java 程序的,那么完全可以不用安装 jdk,只需要安装 jre 即可。

下一个问题,Java 为什么能跨平台,实现一次编写,多处运行?

Java 能够跨平台运行的核心在于 JVM 。不是 Java 能够跨平台,而是它的 jvm 能够跨平台。我们知道,不同的操作系统向上的 API 肯定是不同的,那么如果我们想要写一段代码调用系统的声音设备,就需要针对不同系统的 API 写出不同的代码来完成动作。

而 Java 引入了字节码的概念,jvm 只能认识字节码,并将它们解释到系统的 API 调用。针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有 Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。引用上面的例子,在 Java API 层面,我们调用系统声音设备的代码是唯一的,和系统无关,编译生成的字节码也是唯一的。但是同一段字节码,在不同的 jvm 实现上会映射到不同系统的 API 调用,从而实现代码的不加修改即可跨平台运行。

java的编译过程?

程序员编写的.java文件

由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件)

在由JVM编译成电脑认识的文件 ,二进制的流文件(对于电脑系统来说 文件代表一切)

jvm虚拟机的运行过程

JVM包含两个子系统和两个组件:

两个子系统为Class loader(类装载)、Execution engine(执行引擎);

两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。[java的反射机制]

Execution engine(执行引擎):执行classes中的指令。

Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。【java运行要调用cpu,java播放视频】

Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能【cas比较并置换】。

jvm 内存模型划分

根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。

程序计数器(线程私有):

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,用于记录当前线程执行到哪里。

下次该线程获得cpu资源后,根据程序计数器去继续执行代码。每个线程都有独立的程序计数器,用来在线程切换后能恢复到正确的执行位置,各条线程之间的计数器互不影响,独立存储。所以它是一个“线程私有”的内存区域。此内存区域是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。

java 虚拟机栈(线程私有):

线程访问过程中产生的局部变量,对象,方法出栈和入栈。

JVM栈是线程私有的内存区域。它描述的是java方法执行的内存模型(栈是一个先进后出的数据结构,在a方法中调用b方法,b方法调用c方法,对于调用顺序而言是abc,但是执行顺序是cba。)每一个方法对应一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息

每个方法从调用直至完成的过程,都对应着一个栈帧从入栈到出栈的过程。(每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法。)方法的调用过程也是由栈帧切换来产生结果。

在JVM规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

StackOverflowError异常: 递归的层级太多了。

OutOfMemoryError异常:

(1)死循环不断创建新对象。

(2)请求数量大,每个方法都要创建对象。

(3)将大量的数据一次查询出来展示或者导出。

本地方法栈(线程私有)

本地方法栈和虚拟机栈所发挥的作用是很相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。(Sun HotSpot 直接就把本地方法栈和虚拟机栈合二为一。)本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆(线程共享)

Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象堆由垃圾收集器自动回收堆区由各子线程共享使用;通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间;堆的内存空间既可以固定大小,也可运行时动态地调整,通过参数-Xms设定初始值、-Xmx设定最大值。

当队中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

方法区(线程共享)

方法区是被所有线程共享的内存区域,用来存储已被虚拟机加载的类信息、常量、静态变量、JIT(just in time,即时编译技术)编译后的代码等数据。运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面常量和符号引用。

通过反射获取到的类型、方法名、字段名称、访问修饰符等信息就是从方法区获取到的。在使用到CGLib对类进行增强时,增强的类越多,就需要越大的方法区类存储动态生成的Class信息,当存放方法区数据的内存溢出时,会报OutOfMemoryError异常。在jdk1.8中也就是Metaspace内存溢出,可以通过参数JVM参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置Metaspace的空间大小。jdk1.8后方法区(Method Area)被元空间(Metaspace)代替。

堆内存模型

本地内存:线程共享区域,Java 8 中,本地内存,也是我们通常说的堆外内存,包含元空间和直接内存

元空间其实就是java8以前的永久代。永久代在堆内存中受到gc管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM。很难确定一个合适的大小,受类数量,常量数量的多少影响很大。

而元空间不在堆内存中,他是在本地内存中,不会受到jvm管控也就不会发生gc,这样也提高了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响),也就不会发生OOM

只有堆内存才会进行垃圾回收GC。

看完这篇垃圾回收,和面试官扯皮没问题了

如何识别垃圾呢??

(1)引用计数法

算法思想:对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收。

缺点:无法解决循环引用的问题

循环引用见下图

(2)可达性算法

算法思想:以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕

如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。这就解决了循环引用问题

现代虚拟机基本都是采用这种算法来判断对象是否存活

(哪些对象可以作为 GC Root )

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

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

方法区中常量引用的对象

本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

(标记垃圾机制)

a, b 对象可回收,就一定会被回收吗?并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会

1.发生GC时,会先判断不可达对象是否执行了 finalize 方法

2.如果未执行,则会先执行 finalize 方法,当前对象与 GC Roots 关联,GC 会再次判断对象是否可达

如果不可达,则会被回收,如果可达,则不回收!

注意:finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达,不会回收。但如果此对象再次被 GC,则会忽略 finalize 方法,此对象会被回收。

垃圾回收主要方法

通过算法对垃圾进行回收

标记清除算法 、复制算法 、标记整理法以及分代收集算法

1.标记清除算法

算法思想:先根据可达性算法标记出相应的可回收对象,然后对可回收的对象进行回收。

虽然实现很简单,但是问题也很明显:回收的空间都是零碎的,也就是内存碎片。比如清除了垃圾释放了3M、4M、5M的内存,虽然总共有12M的空闲空间,但是如果这个时候申请一个6M的空间是不行的,因为最大的连续空间是5M

总结:实现简单,但是存在内存碎片导致内存浪费

在Java中,垃圾收集器的一种常见算法是标记清除算法(Mark and Sweep)。该算法的核心思想是,首先标记出所有仍然在使用中的对象,在标记完成后,再清除掉未被标记的对象。

在执行标记阶段时,垃圾收集器会从根节点开始遍历对象图,将所有可达的对象进行标记。这些根节点可以是程序中的静态变量、本地方法栈中的引用等。

在标记完成后,垃圾收集器会扫描整个堆内存,将未被标记的对象进行回收。由于标记和清除过程需要暂停应用程序的执行,因此在实现中通常使用了并发标记清除算法来减少暂停时间。

尽管标记清除算法相对简单,但它也存在一些缺点。其中一个主要问题是,清除操作之后会产生大量的不连续空间,这可能导致堆碎片严重,影响程序性能。此外,由于该算法无法处理循环引用的情况,因此在处理包含循环引用的数据结构时,需要使用其他垃圾回收算法来处理这种情况。

2.标记整理法

前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列,再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

缺点:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下

3.复制算法

算法思想:

把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配。

对区域 A 使用以上所说的标记法把存活的对象标记出来,然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。

缺点:1.内存利用率仅为50% 2.如果复制的数量大,需要进行大量的数据移动,效率很低

4.分代收集算法

是上面三种算法的结合,集合了他们的优点,避开他们的缺点。是现代虚拟机采用的首选算法

算法思想:根据对象存活周期的不同将堆分成新生代和老生代,其中新生代由出生区和两个大小一样的幸存区组成。根据新老生代的特点选择最合适的垃圾回收算法。我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。

(工作原理)

1、对象在新生代的分配与回收

大部分对象在很短的时间内都会被回收,对象一般分配在出生区,当出生区将满时,触发 Minor GC。

经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区,同时对象年龄加一(对象的年龄即发生 Minor GC 的次数,最后把 Eden 区对象全部清理以释放出空间

当触发下一次 Mino,。r GC 时,会把 Eden 区的存活对象和 S0(或S1) 中的存活对象(S0 或 S1 中的存活对象经过每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活对象年龄+1), 同时清空 Eden 和 S0 的空间。

若再触发下一次 Minor GC,则重复上一步,也就是说在 Eden 区的垃圾回收我们采用的是复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象,S0,S1 区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销。

2、对象何时晋升老年代
  • 当对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代
  • 大对象 当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,因为如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以干脆就直接移到老年代.
  • 还有一种情况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代

可以粗略的理解为晋升老年代的情况只有两种情况:

1.对象年龄达到了设定阈值

2.新生代的内存空间不够用,需要把对象放到老年代中

3、空间分配担保

在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。

如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小

如果大于则进行 Minor GC,否则可能进行一次 Full GC。

jvm的参数调优

优化的目的是不是就是减少垃圾回收的次数,尤其减少full gc。

jvm整体如何给你大小:

-Xmx 指定堆所分配内存的最大值
-Xms ​指定堆所分配内存的初始值
  1. ​ 对于生产环境的部署,-Xms和-Xmx通常设为相同的值

根据我们的项目合理的分配jvm的堆内存。年轻代,老年代,元空间。

4G-1G=3G

堆内存总数-元空间=这个差值给年轻代和年老代分

主要是给年轻代分配大小的,​ 一般建议把年轻代的大小保持在整个堆大小的50%-25%

分配方案:

最高优先级:给年代配置最大和最小空间,剩下的给年老代

 

-XX:NewSize -XX:MaxNewSize

中优先级:直接给年轻代一个固定值,省下的给年老代

-Xmn

中优先级:这个是分配年老代和年轻的比例3:1

XX:NewRatio=3

注意:一般给年老代分配的要大于年轻代,原因避免触发full gc 。

年轻代:1G 年老代:2G

年轻代调优:(10G)

如果n=8

比例:伊甸区:幸存区0:幸存区1=8:1:1

如果n=6

比例:伊甸区:幸存区0:幸存区1=6:2:2

XX:NewRatio=3

如何给元空间分配大小【取代原来的永久代】:

-XX:MetaspaceSize = 128M

​ 指定元空间第一次触发垃圾回收的内存大小的阈值。当元空间内存占用不断增大,直到达到这个阈值时,就会触发一次垃圾回收。所以适当增大这个阈值,会减少垃圾回收的次数,默认值根据平台而定,一般情况下大约20.8MB

-XX:MaxMetaspaceSize=256M

​ 指定元空间所分配内存的最大值,默认是没有限制的,取决于系统的可用内存量,理论上可以占满整个系统的内存。

​ 所以需要适当设置它的大小

内存调优实操

​ 尽可能把堆内存空间设置大一些,减少内存回收的次数。当服务器上的可用内存还有12GB的时候,可以先指定堆所分配内存的最大值和初始值都为8GB。一般情况下,年轻代内存大小需在整个堆大小的50%-25%,那就指定年轻代内存大小为3GB。再把Eden区和一个Survivor区的空间大小比例设为4。元空间第一次触发垃圾回收的内存大小的阈值设为256MB,一般情况下足够使用。元空间所分配内存的最大值设置为512MB,为了避免极端情况下占用大量内存。另外还需要明确指定JVM以server模式启动。

​ 内存调优的参数基本确定后,用它启动一个名为one-more-study-0.0.1-SNAPSHOT.jar的jar文件:

你的springboot项目调优java -jar jar包名称

-server 制定jvm运行的参数的

java -server -Xmx8G -Xms8G -Xmn3G 
-XX:SurvivorRatio=4 
-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=512M -jar one-more-study-0.0.1-SNAPSHOT.jar

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值