JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析

不同平台的解释器不同,但是编译的过程是相同的,这就是所谓的一次编译到处执行跨平台 。一个程序开始运行java -jar xxx.jar虚拟机就会进行实例化,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例被销毁,多个虚拟机实例之间数据不能共享。

2.内存结构

内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 程序在运行过程中的内存申请、分配、管理策略,保证了它的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。

在这里插入图片描述

JVM 运行时管理的内存就是虚拟机内存 它会把这些内存分配成不同的区域, JVM 利用到的没有直接管理的物理内存就是本地内存,这两种内存有一定的区别:

  • 虚拟机内存:受虚拟机内存参数控制 -Xmx 设置; Runtime.getRuntime().maxMemory() 进行查看 ,超过参数设置的大小时就会报OOM。
  • 本地内存:本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制,虽然不受参数的限制,但是如果内存的占用超出物理内存的大小,同样也会报OOM。

在这里插入图片描述

JVM 定义了若干种程序运行期间会使用到的运行时数据区内存区域,其中有一些会随着虚拟机启动而创建,随着虚拟机退出或关闭而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。

  • 线程共享:方法区、堆、堆外内存(永久代或元空间、代码缓存)
  • 线程私有:栈、程序计数器、本地方法栈

下图是 JVM 整体架构,中间部分就是它定义的各种运行时数据区域:

在这里插入图片描述

3.堆内存空间

Heap Area 堆是 JVM 内存中最大的一块,被垃圾收集器管理也被所有线程共享。主要存放对象实例,由于 JVM 的发展,堆中也多了许多东西:

在这里插入图片描述
堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样,既可以是固定大小,也可以是可扩展的(通过参数-Xmx和-Xms设定),堆在无法扩展或者无法分配内存时会报 OOM 也就是堆内存耗尽,堆内存逻辑上被划分成 3️⃣ 个区域(分代的唯一理由就是优化 GC 性能):

  • 新生区(年轻代):新对象和没达到一定年龄的对象都在新生区【占堆的1/3】。
  • 养老区(老年代):被长时间使用的 MinorGC 未回收的对象【占堆的2/3】。
  • 元空间(JDK8之前叫永久区):像一些方法中操作的临时对象等,JDK8之前是占用JVM内存,JDK8使用本地内存这也是为什么在限制了JVM堆内存之后程序处理大量数据时电脑内存还是被大量占用的原因

堆内存及控制参数
Visual VM 的插件 Visual GC 绘制的 Spaces:

在这里插入图片描述

3.1 新生区 (New Space/Young Generation)

新生区是所有新对象存储的地方,被分为 3️⃣ 个区域:伊甸区(Eden Space)和两个幸存区(Survivor Space,被称为 from survivor/to survivor 或 s0/s1),默认比例是8:1:1

刚被new出来的对象都是存放在伊甸区【如果新创建的对象占用内存很大,超过了-XX:PetenureSizeThreshold 则直接分配到养老区】,当前空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸区进行垃圾回收(这种垃圾收集称为MinorGC),销毁伊甸区中不再被其他对象所引用的对象并将剩余对象移动到s0区。若s0区也满了,再对该区进行垃圾回收,然后移动到s1区。那如果s1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生 MajorGC(FullGC),进行养老区的内存清理。若养老区执行了 FullGC 之后发现依然无法进行对象的保存,就会产生OOM。

在这里插入图片描述

MinorGC的过程为(复制->清空->互换):

问题:为什么要将新生区分为三个区域?】由于年轻代的垃圾回收算法,(复制算法)设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入S1区(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生),接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域。

JVM 会给new出来的对象定义一个对象年轻计数器每次的 MinorGC 对象的年龄就会+1达到老年的标准-XX:MaxTenuringThreshold【默认值为15】则复制到养老区:

在这里插入图片描述

3.2 养老区(Tenure Space/Old Generation)

养老区主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以Major GC不会频繁执行。在进行Major GC前一般都先进行了一次Minor GC,使得有新生代的对象晋升入老年代,导致空间不够用时才触发。

大对象直接进入养老区(大对象是指需要大量连续内存空间的对象,超过了-XX:PetenureSizeThreshold)。这样做的目的是避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次Major GC进行垃圾回收腾出空间。

img
Major GC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。因为要扫描再回收所以Major GC的耗时比较长。Major GC会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。当养老区也满了装不下的时候,就会抛出OOM。

3.3 元空间(Meta Space/Permanent Generation)

不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 JVM 规范中方法区的实现。虽然 JVM 规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。

永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class、Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

如果出现java.lang.OutOfMemoryError: PermGen space,说明是 JVM 对永久代 Perm 内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方 jar 包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。

版本永久代常量池
JDK6及之前有永久代在方法区
JDK7有永久代,已逐步“去永久代”在堆
JDK8及之后无永久代在元空间

4.堆内存参数

Java 堆用于存储 Java 对象实例,那么堆的大小在 JVM 启动的时候就确定了,我们可以通过 -Xmx-Xms 来设定

  • -Xms 堆的起始内存,默认情况下为服务器内存的1/64,等价于 -XX:InitialHeapSize
  • -Xmx 堆的最大内存,默认情况下为服务器内存的1/4,等价于 -XX:MaxHeapSize

如果堆的内存大小超过 -Xms 设定的最大内存, 就会抛出OOM。通常会将 -Xmx-Xms 两个参数配置为相同的值,其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小,从而提高性能。

可以通过代码获取到我们的设置值,当然也可以模拟 OOM:

	public static void main(String[] args) {
         // JVM 堆大小
        long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        // JVM 堆的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + totalMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

        // 反向计算计算间内存
        System.out.println("系统内存大小:" + totalMemory \* 64 / 1024 + "G");
        System.out.println("系统内存大小:" + maxMemory \* 4 / 1024 + "G");
	}

5.堆内存分配

在默认不配置 JVM 堆内存大小的情况下,JVM 根据默认值来配置当前内存大小:

  • 默认情况下新生区和养老区的比例是1:2,可以通过 –XX:NewRatio 来配置。
  • 新生代中的 Eden:From Survivor:To Survivor 的比例是8:1:1,可以通过-XX:SurvivorRatio来配置。
  • 若在JDK7中开启了 -XX:+UseAdaptiveSizePolicy,JVM 会动态调整 JVM 堆中各个区域的大小以及进入老年代的年龄,此时 –XX:NewRatio-XX:SurvivorRatio 将会失效;而 JDK8 是默认开启-XX:+UseAdaptiveSizePolicy在 JDK8中,不要随意关闭-XX:+UseAdaptiveSizePolicy,除非对堆内存的划分有明确的规划。

每次 GC 后都会重新计算 Eden、From Survivor、To Survivor 的大小计算依据是 GC 过程中统计的GC时间、吞吐量、内存占用量:

# JDK8
java -XX:+PrintFlagsFinal -version | grep HeapSize
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 29360128                            {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 459276288                           {product}
java version "1.8.0\_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

# 查看进程的堆信息
jmap -heap 进程号

6.堆内存回收(垃圾回收)

JVM 在进行 GC 时,并非每次都对堆内存(新生区、养老区、方法区)区域一起回收的,大部分时候回收的都是指新生代。针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:

(1)整堆收集(FullGC):收集整个 Java 堆和方法区的垃圾。
(2)部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:

新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。
老年代收集(MajorGC/OldGC):只是老年代的垃圾收集。
目前只有 G1 GC 会有这种行为
目前,只有 CMS GC 会有单独收集老年代的行为
很多时候 MajorGC 会和 FullGC 混合使用,需要具体分辨是老年代回收还是整堆回收
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集

7.TLAB(Thread Local Allocation Buffer)

从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略。为什么要有 TLAB:

  • 堆区是线程共享的,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。在程序中,可以通过 -XX:UseTLAB 设置是否开启 TLAB 空间。默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。

8.堆是分配对象存储的唯一选择吗

随着 JIT 编译期的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。 ——《深入理解 Java 虚拟机》

逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术。这是一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中,称为方法逃逸。

例如:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb;
}

StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个 StringBuffer 有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

上述代码如果想要 StringBuffer sb不逃出方法,可以这样写:

public static String createStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb.toString();
}

不直接返回 StringBuffer,那么 StringBuffer 将不会逃逸出方法。

参数设置:

  • 在 JDK 6u23版本之后,HotSpot 中默认就已经开启了逃逸分析
  • 如果使用较早版本,可以通过-XX"+DoEscapeAnalysis显式开启

开发中使用局部变量,就不要在方法外定义。使用逃逸分析,编译器可以对代码做优化:

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

😝朋友们如果有需要的话,可以联系领取~

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

2️⃣视频配套工具&国内外网安书籍、文档
① 工具

② 视频

image1

③ 书籍

image2

资源较为敏感,未展示全面,需要的最下面获取

在这里插入图片描述在这里插入图片描述

② 简历模板

在这里插入图片描述

因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值