Concept

内存泄漏:分配出去的内存回收不了
内存溢出:指系统内存不够用了

为什么学习 JVM

  1. 程序调优
    设置不同的垃圾收集器、设置新生代和老生带的内存配置和占比等,不同的配置对于程序的运行有着千差万别的影响。
  2. 排查程序运行问题
    内存溢出,死锁导致的程序运行缓慢
  3. 掌握了其他语言的通用机制
    jvm还可以运行Scala、Clojure、Groovy,以及时下热门的 Kotlin

**

JVM 生命周期

1)诞生
通过带有 public static void main(String[] args) 函数的class都可以作为JVM实例运行的起点,当java程序启动时,jvm实例就诞生了
2)运行
main线程作为初始线程,其他线程由该线程启动,main属于非守护线程(用户线程),所以main线程不一定时最后一个退出的线程
垃圾回收线程就是一个守护线程
可以主动将某个线程设置为守护线程
3)消亡
所有用户线程结束后,jvm退出

基本概念

  • Mutator:生产垃圾的角色,也就是我们的应用程序,垃圾制造者,通过 Allocator 进行 allocate 和 free。

JDK8 HotSpot内存模型

点击查看【processon】

线程独占区:

1. 程序计数器
占有较小的内存空间, 记录当前线程所执行的字节码文件行号,执行Native方法时,计数器的值为Undefined
2. 本地方法栈:
native方法服务 JNI java调用其他语言 通过-Xss来指定其大小
3. 虚拟机栈:
一个线程的Java栈在线程创建的时候被创建
每次方法执行都会创建一个栈帧(Stack Frame),代码运行完毕自动释放

  • 操作数据栈 计算过程中的变量临时的存储空间
  • 局部变量表 用于报错函数的参数及局部变量 局部变量表存放了编译器可知的各种基本数据类型、对象引用

入栈出栈的时机很明确,所以这块区域不需要进行 GC。 栈满时会Stack OverflowError

线程共享区:

1. 堆

image.png
主要的内存工作区域,虚拟机规范规定对可以在物理存储上内存不连续,逻辑上应该视为连续的

1.1 新生代 New:
存放刚出生不久的对象
三者的默认比例为8:1:1, s0区和s1区大小相等(可以互相角色的空间),完成复制算法的交换
垃圾回收在新生代回收更频繁
绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过 一次

JVM内存结构/参数 - 图2
新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。

1.2 老年代 Tenured:
存放频繁使用的对象
大对象直接进入老生代
新生代和老年代的默认比例为1:2。

2. 方法区
方法区是规范,实现是永久代和元空间
存储被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等数据

1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

2.1永久代(Perm)
移除了永久代(PermGen),替换为元空间(Metaspace);
永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

2.2 元空间
当文件被加载时被初始化

  • 运行时常量池(class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池。)
  • static修饰的静态
  • 异常的定义 OutOfMemoryError

2.3 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
**

执行引擎
虚拟机核心的组件
它负责执行虚拟机的字节码,一般会先进行编译成机器码后执行。

分配对象

1.** **栈上分配
如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁,而不需要垃圾回收器的介入,从而提高系统性能。
技术基础:
一是逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
二是标量替换:允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。
只能在server模式下才能启用逃逸分析,参数-XX:DoEscapeAnalysis启用逃逸分析
参数-XX:+EliminateAllocations开启标量替换(默认打开)。
2.** 检查当前类是否已经被加载,解析和初始化过,如果没有, 先进行类加载过程**

  1. 接下来为新生对象分配内存, 内存大小在类加载时可以确定

Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:
java堆中内存是绝对规整的—则使用碰撞指针(bump pointer)
通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距 离,分配效率较高,但使用场景有限。
java堆中内存是相互交错的—则使用空闲链表(free list)
通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗。

  1. 对象分配在堆上,并发线程在堆上申请空间不安全。两种解决办法:
    1. 分配动作进行同步处理, 使用CAS失败进行重试
    2. 每个线程在java堆中预先分配一小块内存, 本地线程分配缓冲(TLAB), 全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域,这样可以避免线程同步,提高了对象分配的效率。

JVM内存结构/参数 - 图3

大部分new出来的对象被分配在堆上,而不是全部。

  1. TLAB(Thread Local Allocation Buffer)

TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间
参数-XX:+UseTLAB开启TLAB,默认是开启的。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,
可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

LAB空间由于比较小很容易装满。
比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,
第一,废弃当前TLAB,这样就会浪费20KB空间;
第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。
默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。
-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

对象内存布局

对象头
运行时数据: hashCode,GC分代年龄, 锁状态标志, 线程持有的锁,
实例数据
对齐填充

JVM参数及调优

参数调优主要调堆的参数和垃圾回收机制

目标:
· GC的时间足够的小
· GC的次数足够的少
· 发生Full GC(新生代和老年代)的周期足够的长

系统级别的参数配置

-XX 是系统级别的jvm之上的比如日志信息、使用什么样的GC机制。

[必须的参数] -XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutMemoryError
发生OOM时,导出堆的信息到文件。
-XX:+HeapDumpPath
表示,导出堆信息的文件路径。
-XX:OnOutOfMemoryError
当系统产生OOM时,执行一个指定的脚本,功能比如生成当前线程的dump文件,或者是发送邮件和重启系统。
[必须的参数] -XX:+PrintGCDetails 更详细的GC日志

-XX:-UseBiasedLocking 禁用偏向锁:偏向锁在只有一个线程使用到该锁的时候效率很高,但是在竞争激烈情况会升级成轻量级锁,此时就需要先消除偏向锁,这个过程是 STW 的。在已知并发激烈的前提下,一般会禁用偏向锁 来提高性能。

-XX:MaxTenuringThreshold 参数来控制晋升年龄,每经过一次 GC,年龄就会加一,达到最大年龄就可以进入 Old 区,最大值为 15(因为 JVM 中使用 4 个比特来表示对象的年龄)

-XX:NewRatio=2 新生代和老年代的比例,默认1:2。比如:1:4,就是新生代占五分之一。

基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。
除了可以设置新生代的绝对大小(-Xmn),可以使用(-XX:NewRatio)设置新生代和老年代的比例
为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小
如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:
(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理
(B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间

-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.

-XX:PetenureSizeThreshold=1000000 单位为B,对象大小超过1M时,在老年代(tenured)分配内存空间。
如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC

-XX:+UseSerialGC 串行回收

-XX:+PrintGC 每次触发GC的时候打印相关日志

-XX:PermSize -XX:MaxPermSize
设置永久区的内存大小和最大值。永久区内存用光也会导致OOM的发生。

-XX:ParallelGCThreads=5 JVM在进行并行GC的时候,用于GC的线程数

-XX:+UseConcMarkSweepGC 详解
-XX:+UseParNewGC 详解
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80

尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。-XX:+LargePageSizeInBytes设置内存页的大小

-XX:MetaspaceSize元空间
初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

查看初始化默认值 java -XX:+PrintFlagsInitial -version

应用层面的参数配置**

非-XX 是应用层面的配置的。后面的+号表示启用、-号表示禁用。

1. -Xms -Xmx
-Xms 为jvm启动时分配的堆内存最小值,比如-Xms200m,表示分配200M
-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存
-Xms:等价于 -XX:InitialHeapSize
-Xmx:等价于 -XX:MaxHeapSize
为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值

先开辟指定的最小堆内存,如果经过数次GC后,还不能,满足程序的运行,才会逐渐的扩容堆的大小,但也不是直接扩大到最大内存。

2. -Xmn
设置新生代堆的内存大小。
新生代大小,一般设为整个堆的1/3到1/4左右

新生代由于其对象存活时间短,且需要经常gc,因此采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另一个区中,当对象存活时间大于某一阈值时,将其放入老年代。
3. -Xss
设置栈的大小。栈都是每个线程独有一个,所有一般都是几百k的大小。
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

参数实战

JAVA_OPTS=”-server -Xmx9g -Xms9g -Xmn3g -XX:+DisableExplicitGC -XX:MetaspaceSize=2048m -XX:MaxMetaspaceSize=2048m -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -Dfile.encoding=UTF8 -Duser.timezone=GMT+08”

常见问题

1. 堆内存溢出

java.lang.OutOfMemoryError: Java heap space 堆内存溢出

**

2. 栈内存溢出

错误原因: java.lang.StackOverflowError 栈内存溢出
栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
解决办法:设置线程最大调用深度

3. tomcat内存溢出

Tomcat内存溢出在catalina.sh 修改JVM堆内存大小
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m"

4. JDK8 为什么去除了永久代,迎元空间

在Java 8之前:静态变量存储在permgen空间(也称为方法区域)。PermGen空间用来存储3件东西类级数据(元数据)内串静态变量, 从Java 8开始静态变量存储在堆本身中,
从Java 8开始,PermGen空间被删除,新的空间被命名为MetaSpace,它不再是堆的一部分,不像以前的Permgen空间。元空间存在于本机内存(操作系统提供给特定应用程序的内存,供其自己使用),现在它只存储类元数据。内部字符串和静态变量被移到堆本身中。
**
随着 Java8 的到来,我们再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域就是我们要提到的元空间。
这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次 Full GC 发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。
同时,HotSpot 虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化 Full GC 以及对以后的并发隔离类元数据等方面进行优化。

总结



java 中会存在内存泄漏吗,请简单描述。

答:会;存在无用但可达的对象,这些对象不能被GC 回收,导致耗费内存资源。


参考文章:

  1. https://blog.csdn.net/weixin_42762133/article/details/95735737