admin管理员组

文章数量:1547193

文章目录

    • 一、jvm简介
      • 1.jvm的位置
      • 2.JVM的整体结构
      • 3.java代码执行流程
    • 二、类加载子系统
      • 1.类的加载过程
      • 2.类加载器分类
      • ⭐3.双亲委派机制
    • 三、运行时数据区及线程
    • 四、程序计数器
      • 作用
    • 五、虚拟机栈
      • ⭐**8.栈相关面试题**
    • 六.本地方法接口
    • 七.本地方法栈
    • 八、堆
      • 堆的核心概述
      • 堆内存细分
      • 设置堆内存大小与OOM
      • 年轻代与老年代
      • 对象分配和垃圾回收过程
      • **内存分配策略**
      • 小结堆空间的参数设置
      • 逃逸分析
    • 九、方法区
    • ⭐⭐常见面试题
    • 十、对象的实例化与访问定位
      • ⭐⭐ 面试题
      • **创建对象的步骤**:
      • 对象的访问方式有哪些
    • 十一、直接内存
    • 十二、执行引擎
    • 十三、String Table
      • String的基本特性
      • String的不可变性
    • 十四、垃圾回收概述
    • 十五、垃圾回收相关算法
    • ⭐⭐ 面试题
    • 优点
    • 缺点
    • 十六、垃圾回收相关概念
      • 引用
      • ⭐⭐面试题:
  • 强引用(Strong Reference)
  • 软引用(SoftReference)
  • 弱引用(WeakReference)
  • 虚引用(PhantomReference)
  • 总结:
    • 十七、垃圾回收器
      • G1回收器:区域化分代式
      • 垃圾回收器总结

一、jvm简介

1.jvm的位置

2.JVM的整体结构

  1. 方法区堆区所有线程共享的内存区域

    而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。

  2. Java栈又叫做jvm虚拟机栈

  3. 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT编译器,英文写作Just-In-Time
    Compiler)编译后的代码等数据。

    虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与 Java 堆区分开来。(方法区(永久代)在jdk8中又叫做元空间Metaspace)

​ JVM内存简图

​ jvm内存详细图

3.java代码执行流程

详细:(12条消息) Java程序的运行过程_Lin_Dong_Tian的博客-CSDN博客_简述java程序运行过程

运行一个Java程序的步骤:
1、编辑源代码xxx.java
2、Javac编译xxx.java文件生成字节码文件xxx.class
3、JVM中的类加载器加载字节码文件
4、JVM中的执行引擎找到入口方法main(),执行其中的方法

二、类加载子系统

1.类的加载过程

2.类加载器分类

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类,(如String)
  • 并不继承自ava.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。(JKD1.9之后为PlatformClassLoader)
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

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

  • java语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器

用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏
    用户自定义类加载器实现步骤:
  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

⭐3.双亲委派机制

①说明

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式

②工作原理

③双亲委派机制的优势/作用

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改

④能不能自己写一个java.lang.String

1、代码书写后可以编译不会报错
2、在另一个类中加载java.lang.String,通过反射调用自己写的String类里的方法,得到结果NoSuchMethod,说明加载的还是原来的String,因为通过双亲委派机制,会把java.lang.String一直提交给启动类加载器去加载,通过他加载,加载到的永远是/lib下面的java.lang.String
3、在这个自己写的类中写上main方法
public static void main(String[] args)
执行main方法报错,因为这个String并不是系统的java.lang.String,所以JVM找不到main方法的签名

⑤总结图

三、运行时数据区及线程

  • 每个线程:独立包括程序计数器、栈、本地栈。
  • 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

四、程序计数器

JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

作用

PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。(保存执行引擎将要提取的下一条指令的地址)

  • pc寄存器只保存执行引擎将要提取的下一条指令的地址,不保留当前指令地址
  • cpu一个核只能执行一个线程,不断地切换线程来执行线程
  • 并发交替起来看上去像并行

使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?

  • 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
  • JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

五、虚拟机栈

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。(和第一章的jvm简介相对应)

1.栈和堆

栈是运行时的单位,而堆是存储的单位

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  • 堆解决的是数据存储的问题,即数据怎么放,放哪里

2.Java虚拟机栈是什么

Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。

3.生命周期

虚拟机栈是线程私有的,生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了

4.作用

主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

5.开发中遇到哪些异常?

栈中可能出现的异常

  • Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个
    outofMemoryError 异常。

6.设置栈内存大小

-Xss1m
-Xss1k

参数 -Xss选项来设置线程的最大栈空间
关于idea:(Run -> Edit Configurations… ->VM options: )

7.栈帧的内部结构

  • ① 局部变量表(Local Variables)
  • ② 操作数栈(operand Stack)(或表达式栈)
  • ③ 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
  • ④ 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

(局部变量表操作数栈主要影响栈帧的大小)

具体细节:(13条消息) 尚硅谷2020最新版宋红康JVM教程-5-虚拟机栈_zgcadmin的博客-CSDN博客

8.栈相关面试题

  1. 举例栈溢出的情况?(StackOverflowError)
    有默认值的栈大小,超过该默认值就发生StackOverflowError,也可以手动通过 -Xss设置栈的大小,
  2. 调整栈大小,就能保证不出现溢出么?
    不能。比如死循环时,无论栈大小,都会栈溢出,只能是延长StackOverflowError出现时间。
  3. 分配的栈内存越大越好么?
    不是。(如果jvm设置的内存过大,就会导致其它程序所占用的内存小)
  4. 垃圾回收是否涉及到虚拟机栈?
    不会涉及。
运行时数据区是否存在Error是否存在GC
程序计数器
虚拟机栈
本地方法栈
方法区

记忆:
程序计数器是唯一没有ERROR和GC,栈都没GC,同时具有ERROR和GC都是线程共享

  1. 方法中定义的局部变量是否线程安全?
    具体问题具体分析
    总结:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

六.本地方法接口

一个Native Method是一个Java调用非Java代码的接囗

七.本地方法栈

  1. Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
  2. 本地方法栈,也是线程私有的。

八、堆

堆的核心概述

  1. 一个进程对应一个JVM实列,其中进程包含多个线程,该进程的n个线程是共享同一堆空间的。

  2. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

  3. Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。

    堆内存的大小是可以调节的。
    1
    
  4. 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  5. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

  6. 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。

    我要说的是:**“几乎”所有的对象实例都在这里分配内存**。—从实际使用角度看的。
    因为还有一些对象是在栈上分配的(‘几乎’是因为可能存储在栈上,另见逃逸分析)
    12
    
  7. 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置

  8. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

  9. 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

-Xms10m:堆区的起始内存
-Xmx10m:堆区的最大内存
run -> Edit Configurations.… -> VM options: ->
cmd->jvisualvm或者xxx\jdk\bin\jvisualvm

堆内存细分

  • Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
Young Generation Space新生区Young/New又被划分为Eden区和Survivor区
Tenure Generation Space养老区Old/Tenure
Permanent Space永久区Perm
  • Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
Young Generation Space新生区Young/New又被划分为Eden区和Survivor区
Tenure Generation Space养老区Old/Tenure
Meta Space元空间Meta

约定:新生区 -> 新生代 -> 年轻代 、 养老区 -> 老年区 -> 老年代、 永久区 -> 永久代

设置堆内存大小与OOM

  1. Java堆区用于存储java对象实例,堆的大小在jvm启动时就已经设定好了,可以通过 "-Xmx"和 "-Xms"来进行设置

    “-Xms"用于表示堆区的起始内存,等价于-xx:InitialHeapSize
    “-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
    (-X 是jvm的运行参数,ms 是memory start)
    123
    
  2. 一旦堆区中的内存大小超过“-xmx"所指定的最大内存时,将会抛出outofMemoryError异常。

  3. 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

    默认情况:
    初始内存大小:物理电脑内存大小/64
    最大内存大小:物理电脑内存大小/4
    

年轻代与老年代

  • 存储在JVM中的Java对象可以被划分为两类:

     1.一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速生命周期短的,及时回收即可
     2.另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
    
    
  • Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)

  • 其中年轻代又可以划分为Eden区Survivor0区Survivor1区(有时也叫做from区、to区)

配置新生代与老年代在堆结构的占比

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
  • 在hotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的
  • 绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)
  • 可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)

总结:
Eden:From:to -> 8:1:1 (-XX:SurvivorRatio)
新生代:老年代 - > 1 : 2(-XX:NewRatio)

对象分配和垃圾回收过程

概念

为新对象分配内存是一件非常严谨和复杂的任务,JM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(YGC/MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者0区。
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
  6. 啥时候能去养老区呢?可以设置次数。默认是15次。
  7. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
  8. 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。

总结

  • 在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作。
  • 如果Survivor区满了/Eden内存不够等特殊情况,(先触发YGC)将会触发一些特殊的规则,可能直接晋升老年代。
  • 针对幸存者s0,s1区:复制之后有交换,谁空谁是to。
  • 关于垃圾回收:多数在新生区收集,少数在养老区收集,几乎不再永久区/元空间收集。
  • -Xx:MaxTenuringThreshold= N进行设置年龄阈值

Minor GC、Major GC、Full GC

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式

    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、方法区(永久代/元空间)等所有部分的模式。

  • Old GC 和 Full GC区别:Old GC只针对老年代,老年代使用超过一定比例就会发生(可调整),而Full GC当老年代或者永久代满了(比如要放一个大对象,放不下了!),就会对新生代、老年代、方法区都GC。

Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC

链接:https://www.zhihu/question/41922036/answer/93079526

STW: Stop-The-World: 指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

触发fullGC的条件

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法去空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

内存分配策略

概念

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代

对象晋升老年代的年龄阀值,可以通过选项-xx:MaxTenuringThreshold来设置

针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden

  • 大对象直接分配到老年代

     尽量避免程序中出现过多的大对象
    
  • 长期存活的对象分配到老年代

  • 动态对象年龄判断

    如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。
    
  • 空间分配担保: -Xx:HandlePromotionFailure

为对象分配内存:TLAB(堆当中的线程私有缓存区域)

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内

小结堆空间的参数设置

https://www.oracle/java/technologies/javase/vmoptions-jsp.html(我随便找的,ppt的已失效)

  • -XX:+PrintFlagsInitial:查看所有的参数的默认初始值

  • -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)(修改过的那行,=号前面有:)

    1. 具体查看某个参数的指令:
    2. jps:查看当前运行中的进程
    3. jinfo -flag xxx  进程id: 查看xx指令
    
  • -Xms:初始堆空间内存(默认为物理内存的1/64)

  • -Xmx:最大堆空间内存(默认为物理内存的1/4)

  • -Xmn:设置新生代的大小。(初始值及最大值)

  • -XX:NewRatio:配置新生代与老年代在堆结构的占比

  • -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例

  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄

  • -XX:+PrintGCDetails:输出详细的GC处理日志

     打印gc简要信息:①-Xx:+PrintGC ② - verbose:gc
    
  • -XX:HandlePromotionFalilure:是否设置空间分配担保

逃逸分析

1.未逃逸:如果方法内的对象没有被外部引用,或者没有return出去,那么方法结束后,当发生gc时,这些对象占用的内存就被回收,就是未逃逸,就是没躲过gc被清理了。

2.逃逸:方法执行完后,发生了gc,这个方法内的对象躲过了gc,比如被外部引用,或者return出去。

若开启逃逸优化:
3.未逃逸的对象很可能存储在“栈”中,这样随着方法执行完,就出栈,可减少gc频率。

4.逃逸的对象存储在“堆”中,逃逸的对象一般很少被gc掉。

发生逃逸

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

如果想要StringBuffer sb不发生逃逸,可以这样写

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

九、方法区

(5条消息) 方法区(Method Area)详解_Damon爱吃西兰花的博客-CSDN博客_方法区 类信息

方法区的基本理解

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据、

方法区主要存放的是 Class,而堆中主要存放的是 实例化的对象

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:ava.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace

    什么时候会报方法区溢出:
    1. 加载大量的第三方的jar包
    2. Tomcat部署的工程过多(30~50个)
    3. 大量动态的生成反射类
    
  • 关闭JVM就会释放这个区域的内存。

  • 方法区是一种概念,具体实现如jdk7的永久代、jdk8的元空间

设置方法区大小与OOM

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。

jdk7及以前

  • 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
  • -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。

JDK8以后:

  • 元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
  • 默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
  • 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

面试题:

⭐⭐常见面试题

百度 三面:说一下JVM内存模型(结构/运行时数据区)吧,有哪些区?分别干什么的?

jvm内存结构分为①程序计数器(操作数栈)、②虚拟机栈、③本地方法栈、④堆、⑤方法区。

线程私有:程序计数器 、虚拟机栈、本地方法栈

线程共享:堆、方法区

① 程序计数器:

  • 当前线程所执行的字节码的行号指示器,保存的是字节码指令的地址,通过改变这个计数器的值来选取下一条需要执行的字节码指令,是对物理PC寄存器的一种抽象模拟。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 内存中唯一无OOM的区域。

② 虚拟机栈:

  • 内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。栈帧中保存了这些方法的信息——局部变量表、操作数栈、动态连接、方法出口(方法返回地址)、附加信息等信息
  • 每一个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 线程请求的栈深度大于虚拟机允许的深度,抛出 StackOverflowError异常。
  • 虚拟机栈可动态扩展,当栈扩展时无法申请到足够的内存会抛出 OOM 异常。

③ 本地方法栈:

  • Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

④ 堆:

  • 几乎所有的对象实例都在这里分配内存,也就是几乎所有对象都存放在这里(几乎是因为如果开启逃逸分析,则对象也能分配在虚拟机栈中)
  • 可设为固定大小,也可扩展,满了且无法扩展会抛出OOM异常

⑤ 方法区:

  • 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 方法区主要存放的是 Class,而堆中主要存放的是实例化的对象

蚂蚁金服

1.Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么(如上)?

2.一面:JVM内存分布/内存结构(如上)?

3.栈和堆的区别?

栈是运行时的单位,而堆是存储的单位

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
  • 堆解决的是数据存储的问题,即数据怎么放放哪里

4.堆的结构(上面)?

5.为什么两个survivor区?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。

  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。

  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生

6.二面:Eden和survior的比例分配

  • 在hotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=8

小米

1.jvm内存分区,为什么要有新生代和老年代.

  • 新生代:主要存放新创建的对象,内存大小一般会比较小,垃圾回收会比较频繁。
  • 老年代:主要存放JVM认为生命周期比较长的对象(经过几次的Young GC的垃圾回收后仍然存在),或者大对象(比如:字符串、数组),垃圾回收也相对没有那么频繁。

为什么划分老年代和新生代,主要对象大小不一样,对象生命周期不一样。划分后,提供垃圾回收效率,节省资源,提升对象利用率等等。

字节跳动

1.二面:Java的内存分区(如上)

2.二面:讲讲jvm运行时数据库区 什么时候对象会进入老年代?

① 当在伊甸园区经过15次minor GC后。

② 大对象直接放入老年代,要尽量避免出现大对象,会容易导致full GC频繁。

京东

1.JVM的内存结构(上面)

2.Eden和Survivor比例(上面)

3.JVM内存为什么要分成新生代,老年代,持久代。

  • 新生代和老年代如上

  • 持久代,java7叫永久代,java8叫元空间,都是方法区的实现。

    • 永久代使用的是堆内存,和新生代、老年代是一片连续的堆内存空间。
    • 元空间使用的是本地内存,这样增加了方法区的空间,本地剩余内存有多大,方法区就有多大,最典型的场景是,在web开发比较多jsp页面的时候,类特别多的时候。

4.新生代中为什么要分为Eden和survivor。(上面)

天猫

1.一面:Jvm内存模型以及分区,需要详细到每个区放什么。

2.一面:JVM的内存模型,Java8做了什么改动。

  • 1.8同1.7比,最大的差异就是:元数据区取代了永久代。元空间的本质和永久代相似,都是对JVM规范中方法区的实现。

  • 永久代使用的是堆内存,和新生代、老年代是一片连续的堆内存空间。

  • 元空间使用的是本地内存,这样增加了方法区的空间,本地剩余内存有多大,方法区就有多大,最典型的场景是,在web开发比较多jsp页面的时候,类特别多的时候。

  • java8虽然元空间使用的是本地内存,但字符串常量池还在堆中。

    仍然保留在堆中是因为方法区回收效率很低,只有在full GC才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,容易导致永久代内存不足。放到堆里,能及时回收内存。

拼多多: JVM内存分哪几个区,每个区的作用是什么?

美团: java内存分配 jvm的永久代中会发生垃圾回收吗?

会,当永久代空间不足时就会发生full GC。

一面:jvm内存分区,为什么要有新生代和老年代?

StringTable(字符串常量池)为什么要调整到堆中?

jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full GC的时候才会触发。而Full GC是老年代空间不足、永久代空间不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,容易导致永久代内存不足。放到堆里,能及时回收内存。

十、对象的实例化与访问定位

⭐⭐ 面试题

美团:
对象在JVM中是怎么存储的?
对象头信息里面有哪些东西?
蚂蚁金服:
二面:java对象头里有什么

对象创建方式

  1. new:最常见的方式(本质是构造器)

    变形1 : Xxx的静态方法
    变形2 : XxBuilder/XxoxFactory的静态方法
    12
    
  2. Class的newInstance方法:反射的方式,只能调用空参的构造器,权限必须是public(在JDK9里面被标记为过时的方法)

  3. Constructor的newInstance(XXX):反射的方式,可以调用空参的,或者带参的构造器,权限没有要求

  4. 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone接口(clone()是native方法)

  5. 使用序列化:从文件中、从网络中获取一个对象的二进制流

  6. 第三方库 Objenesis

创建对象的步骤

  1. 判断对象对应的类是否加载、链接、初始化
    虚拟机遇到一条new指令,
    首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息是否存在)。
    如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象

  2. 为对象分配内存
    首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小

    1.如果内存规整指针碰撞(数组,指针有顺序地换位):

    假设为Java堆中内存是绝对完整的,所有用过的内存放到一边空闲的内存放到另一边中间放着一个指针作为分界点的指示器,所分配的内存就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。

    2.如果内存不规整(集合,无顺序只有记录表):空闲列表
    如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虛拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为“空闲列表(Free List) ”。

说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  1. 处理并发问题
    采用CAS配上失败重试保证更新的原子性
    每个线程预先分配TLAB - 通过设置 -XX:+UseTLAB参数来设置(区域加锁机制)

  2. 初始化分配到的内存
    1 属性的默认初始化
    2 显示初始化
    3 代码块中的初始化
    4 构造器初始化

内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

  1. 设置对象的对象头

    对象头包含两部分信息,一部分用于存储自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

  2. 执行init方法进行初始化
    Java程序的视角看来,初始化才正式开始初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之 后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

对象的访问方式有哪些

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。目前主流的访问方式有使用句柄直接指针两种。

句柄

Java堆中会划分出一块内存来作为句柄,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图

  • 缺点:占用空间、间接指向对象实例。
  • 优点:定位稳定:当对象实例发生移动(垃圾回收的复制算法,标记整理算法),则不需要修改reference到句柄的定位地址,仅需要在句柄内修改间接地址,重新定位到对象实例即可。

直接指针(HotSpot采用)

如果使用直接指针访问,reference中存储的就是对象地址,而Java堆对象的布局需要考虑如何放置访问累类型数据的相关信息,如下图

  • 优点
    • 节省了一次指针定位的时间开销,速度更快
  • 缺点:
    • 当对象位置移动时(垃圾回收的复制算法,标记整理算法),需要改变对应栈中reference的值。

十一、直接内存

  1. 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

  2. 直接内存是Java堆外的、直接向系统申请的内存区间。

  3. 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存

  4. 通常,访问直接内存的速度会优于Java堆。即读写性能高。

    1.因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
    2.Java的NIO库允许Java程序使用直接内存,用于数据缓冲区

直接内存异常:

也可能导致outofMemoryError异常
由于直接内存在Java堆外,因此它的大小不会直接受限于-xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

缺点

1.分配回收成本较高
2.不受JVM内存回收管理

直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-xmx参数值一致

十二、执行引擎

执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

解释器

就是字节码指令逐行解释运行:当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。

什么是JIT编译器

  • 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行

  • 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In
    Time)将方法编译成机器码后再执行

HotSpot JVM执行方式/为什么采用解释器和即时编译器并存的结构

它采用解释器与即时编译器并存的架构,当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能将热点代码编译为本地机器指令,以换取更高的程序执行效率

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码“。

HotSpot VM 可以设置程序执行方法

缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:

  • -Xint:完全采用解释器模式执行程序;
  • -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行
  • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。

十三、String Table

(13条消息) 关于java的String一些问题(创建过程、intern()方法、字符串常量池),jdk6/jdk7/jdk8,详细到底层源码和jvm_凌晨四点的打铁声的博客-CSDN博客_jvm string 创建过程

String的基本特性

  • String:字符串,使用一对 ”” 引起来表示

    1. String s1 = "zgc" ; // 字面量的定义方式
    2. String s2 = new String("zgc");
    
  • string声明为final的,不可被继承

  • String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小

  • string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]

结论:String再也不用char[]来存储啦,改成了byte[]加上编码标记,节约了一些空间。

String的不可变性

string:代表不可变的字符序列。简称:不可变性。

  • 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 通过字面量的方式(不同于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。字符串常量池当中的字符串不允许重复存放。

final

final双引号和变量的拼接

    public static void test4(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);//true
    }

字符串拼接操作不一定使用的是StringBuilder!

如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。

针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。

十四、垃圾回收概述

什么是垃圾

  • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
  • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。

垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收。其中,Java堆是垃圾收集器的工作重点

从次数上讲:

  • 频繁收集Young区
  • 较少收集Old区
  • 基本不收集Perm区(或元空间)

怎么看是不是垃圾/判断对象是否存活/标记算法:

引用计数算法可达性分析算法

  • 引用计数算法:python使用
  • 可达性分析算法:jvm使用。
    • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。

十五、垃圾回收相关算法

⭐⭐ 面试题

JVM 有哪些垃圾回收算法?

标记-清除算法、复制算法、标记-整理算法(标记-压缩算法)、分代算法。

  • 标记-清除算法

    • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。标记的是引用的对象,不是垃圾!!
    • 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
    • 缺点:
      • 效率不算高
      • 在进行GC的时候,需要停止整个应用程序,用户体验较差
      • 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表(类似创建对象分配空间的某种方式)

何为清除?(类似window的格式化机械硬盘)

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址

  • 复制算法:

    按照容量划分二个大小相等的内存区域当一块用完的时候将活着的对象复制到另一块上然后再把已使用的内存空间一次清理掉

    • 优点:

      • 没有标记和清除过程,实现简单,运行高效
      • 复制过去以后保证空间的连续性,不会出现“碎片”问题。
    • 缺点:

      • 内存使用率不高,只有原来的一半,消耗内存。
      • 如果对象存活时间长的就不合适,容易装满然后OOM,适合对象存活时间短的空间,如伊甸园区(新生代)
  • 标记-整理算法(标记-压缩算法):

    1. 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象

    2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。

      优点

      • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
      • 消除了复制算法当中,内存减半的高额代价。

      缺点

      • 从效率上来说,标记-整理算法要低于复制算法。
      • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
      • 移动过程中,需要全程暂停用户应用程序。即:STW
  • 分代算法:

    根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法老年代采用标记清除和标记整理算法。(详细看八、堆)

    分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代

  • 还有增量收集算法分区算法

    • 增量收集算法:**基础仍是传统的标记-清除和复制算法。**让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
    • 分区算法:按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
      • 分代算法按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间

十六、垃圾回收相关概念

System.gc()的理解:

  • 在默认情况下,通过system.gc()或者Runtime.getRuntime().gc()的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  • 然而system.gc() )调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效,只是通知jvm去回收,还有可能完全被拒绝)
  • JVM实现者可以通过system.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()

内存泄漏

  • 也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
  • 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”。
  • 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outofMemory异常,导致程序崩溃。

注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

举例:

  • 单例模式

    单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
    
    
  • 一些提供close的资源未关闭导致内存泄漏

     数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close
    

引用

  • 强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new Object ()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。

  • 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

  • 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

强引用,软引用,弱引用都是基于引用关系还存在

具体查看博客:(13条消息) 尚硅谷2020最新版宋红康JVM教程-16-垃圾回收相关概念_zgcadmin的博客-CSDN博客

⭐⭐面试题:

【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?

强引用(Strong Reference)

我们使用的大部分的引用都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

强引用就是我们经常使用的引用,其写法如下:

Object o = new Object();

特点:

  • 只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
  • 显式地设置 o 为 null,或者超出对象的生命周期,此时就可以回收这个对象。具体回收时机还是要看垃圾收集策略。
  • 在不用对象的时将引用赋值为 null,能够帮助垃圾回收器回收对象。比如 ArrayList 的 clear() 方法实现:
 public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

使用场景:

  • 图片缓存。图片缓存框架中,“内存缓存”中的图片是以这种引用保存,使得 JVM 在发生 OOM 之前,可以回收这部分缓存。
  • 网页缓存。
  • Spring 缓存配置属性缓存,配置属性在应用启动时读取,运行过程中很有可能不会再次使用,所以Spring 大佬使用软引用缓存。
Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
prev = null;					//销毁强引用,这是必须的,不然会存在强引用和弱引用
if(sr.get()!=null) { 
	rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
} else {
	prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
	sr = new SoftReference(prev);       // 重新构建
}

String str=new String("abc");                                     // 强引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用x
softRef.get() //得到str对象,如果str被回收,则返回null

当内存不足时,等价于:   
If(JVM.内存不足()) {
   str = null;  // 转换为软引用
   System.gc(); // 垃圾回收器进行回收
}

弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc();                //手工模拟JVM的gc进行垃圾回收
System.out.println(sr.get());

hello
null
Process finished with exit code 0

虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

Object obj = new Object();
ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
PhantomReference<Object> pf = new PhantomReference<Object>(obj,rq);
obj=null;
System.out.println(pf.get());//永远返回null
System.out.println(pf.isEnqueued());//返回是否从内存中已经删除
System.gc();
TimeUnit.SECONDS.sleep(6);
System.out.println(pf.isEnqueued());

null
false
true

使用场景:

可以用来跟踪对象呗垃圾回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。例如:在 DirectByteBuffer 中,会创建一个 PhantomReference 的子类Cleaner的虚引用实例用来引用该 DirectByteBuffer 实例,Cleaner 创建时会添加一个 Runnable 实例,当被引用的 DirectByteBuffer 对象不可达被垃圾回收时,将会执行 Cleaner 实例内部的 Runnable 实例的 run 方法,用来回收堆外资源。

总结:

表格来说明一下,如下:

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用当内存不足时对象缓存内存不足时终止
弱引用正常垃圾回收时对象缓存垃圾回收后终止
虚引用正常垃圾回收时跟踪对象的垃圾回收垃圾回收后终止

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)。
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。

十七、垃圾回收器

7种经典的垃圾收集器

  • 串行回收器:Serial、Serial old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1

7款经典收集器与垃圾分代之间的关系

  • 新生代收集器:Serial、ParNew、Paralle1 Scavenge
  • 老年代收集器:Serial old、Parallel old、CMS
  • 整堆收集器:G1

G1回收器:区域化分代式

G1垃圾收集器的优点

与其他GC收集器相比,G1使用了全新的分区算法,其特点如下所示:

  • 并行与并发
    1. 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
    2. 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集
    1. 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
    2. 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
    3. 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代。
  • 空间整合
    1. CMS:“标记-清除”算法、内存碎片、若干次Gc后进行一次碎片整理
    2. G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  • 可预测的停顿时间模型(即:软实时soft real-time)
    1. 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
    2. 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    3. G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
    4. 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

G1垃圾收集器的缺点

  • 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
  • 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

G1参数设置

  • -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务
  • -XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
  • -XX:+ParallelGcThread 设置STW工作线程数的值。最多设置为8
  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。
  • -XX:InitiatingHeapoccupancyPercent 设置触发并发Gc周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

垃圾回收器总结

  • 截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。

  • 不同厂商、不同版本的虚拟机实现差距比较大。HotSpot虚拟机在JDK7/8后所有收集器及组合如下图

  1. 两个收集器间有连线,表明它们可以搭配使用: Serial/Serial 0ld、Serial /CMS、ParNew/Serial 0ld、ParNew/CMS、 Parallel Scavenge/Serial 01d、Parallel Scavenge/Parallel 0ld、G1;
  2. 其中Serial 0ld作 为CMS出现"Concurrent Mode Failure"失败 的后备预案。
  3. (红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial 0ld这两个组合声明为Deprecated (JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP214),即:移除。
  4. (绿色虚线)JDK 14中:弃用ParallelScavenge 和Serial0ld GC组合 (JEP 366)
  5. (青色虚线)JDK 14中:删除CMS垃圾回收器 (JEP 363 ) GC发展阶段: Serial => Parallel (并行)=> CMS (并发) => G1 => ZGC

怎么选择垃圾回收器:

  • Java垃圾收集器的配置对于JVM优化来说是一个很重要的选择,选择合适的垃圾收集器可以让JVM的性能有一个很大的提升。
  • 怎么选择垃圾收集器?
    • 优先调整堆的大小让JVM自适应完成。
    • 如果内存小于100M,使用串行收集器
    • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
    • 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
    • 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
      官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
  • 最后需要明确一一个观点:
    • 没有最好的收集器,更没有万能的收集
    • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

GC日志分析

通过阅读Gc日志,我们可以了解Java虚拟机内存分配与回收策略。 内存分配与垃圾回收的参数列表

  • -XX:+PrintGc输出GC日志。类似:-verbose:gc
  • -XX:+PrintGcDetails输出Gc的详细日志
  • -XX:+PrintGcTimestamps 输出Gc的时间戳(以基准时间的形式)
  • -XX:+PrintGCDatestamps 输出Gc的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC在进行Gc的前后打印出堆的信息
  • -Xloggc:…/logs/gc.1og日志文件的输出路径

本文标签: 内存垃圾重点JVM