深入理解JVM:内存结构、垃圾收集与性能调优

目录

JDK、JRE、JVM关系?

启动程序如何查看加载了哪些类,以及加载顺序?

class字节码文件10个主要组成部分?

JVM结构

画一下JVM内存结构图

程序计数器

Java虚拟机栈

本地方法栈

Java堆

方法区

运行时常量池?

什么时候抛出StackOverflowError?

例如:

Java7和Java8在内存模型上有什么区别?

程序员最关注的两个内存区域?

直接内存是什么

除了哪个区域外,虚拟机内存其他运行时区域都会发生OutOfMemoryError?

什么情况下会出现堆内存溢出?

空间什么情况下会抛出OutOfMemoryError?

如何设置直接内存容量?

Java堆内存组成?

Edem:from:to默认比例是?

垃圾标记阶段?

引用计数法?

根搜索算法?

JVM中三种常见的垃圾收集算法?

标记-清除算法(Mark_Sweep)

复制算法(Copying)

标记-压缩算法(Mark-Compact)

分代收集算法?

垃圾收集器

Stop The World

Serial收集器

PartNew收集器

Parallel Scavenge

Parallel Old收集器

CMS 收集器

CMS垃圾回收的步骤?

CMS收集器优点?缺点?

G1收集器?

G1收集器是如何改进收集方式的?


JDK、JRE、JVM关系?

Jdk (Java Development Kit):java语言的软件开发包。包括Java运行时环境Jre。

Jre (Java Runtime Environment):Java运行时环境,包括Jvm。

Jvm (Java Virtual Machine) :

  • 一种用于计算机设备的规范。
  • Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虛拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Jdk包括Jre,Jre包括Jvm。

启动程序如何查看加载了哪些类,以及加载顺序?

Java -XX:+TraceClassLoading 具体类
Java -verbose 具体类

class字节码文件10个主要组成部分?

  • MagicNumber
  • Version
  • Constant_pool
  • Access_flag
  • This_class
  • Super_class
  • Interfaces
  • Fields
  • Methods
  • Attributes

JVM结构

画一下JVM内存结构图

程序计数器

属于线程私有内存。占用一块非常小的空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的指令的字节码,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器来完成。

Java虚拟机栈

属于线程私有内存。它的生命周期与线程相同,虚拟机栈描述的是Java方法执行内存模型;每个方法被执行的时候都会同时创建一个栈桢用于存储局部变量表、操作栈、动态链接、方法出口信息等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧再虚拟机中从入栈到出栈的过程。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,只不过虚拟机栈对虚拟机执行Java方法服务,而本地栈是为虚拟机使用到Native方法服务。

Java堆

是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Tps:但随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标亮替换优化技术将会导师一些微妙的变化发生,所有的对象都分配在堆上就不那么绝对了

方法区

是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池?

是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项是常量池(Constant PoolTable)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放道方法区的运行时常量池中。

什么时候抛出StackOverflowError?

如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError。

例如:

public class RecursiveOverflow {  
    public static void main(String[] args) {  
        recursiveMethod();  
    }  
  
    public static void recursiveMethod() {  
        recursiveMethod(); // 无限递归  
    }  
}
  1. 递归调用没有合适的终止条件:当一个方法直接或间接地递归调用自己,并且没有适当的终止条件时,调用栈会不断增长,直到耗尽栈空间。

  2. 方法调用层次过深:即使不是递归调用,但如果方法调用的层次太深(例如,A 方法调用 B 方法,B 方法调用 C 方法,以此类推),也可能导致栈溢出。
  3. 大量的本地变量:虽然这种情况不太常见,但如果在方法中有大量的本地变量(包括对象引用),也可能导致栈帧过大,从而在短时间内耗尽栈空间。
  4. 线程栈大小设置不当:JVM 启动时可以通过 -Xss 参数来设置每个线程的栈大小。如果设置得过小,可能会导致在正常的程序执行过程中就发生栈溢出。

Java7和Java8在内存模型上有什么区别?

  1. Java内存模型(JMM)的改进
    • Java 8引入了新的JMM特性,这些特性主要为了支持更高效的并发编程和多线程应用。JMM定义了一组规则,用于指导程序员编写正确的多线程代码,以避免出现数据竞争和内存可见性问题。
    • Java 8的JMM更加强调了原子性、可见性和有序性。原子性通过volatile关键字和java.util.concurrent.atomic包中的类来实现,确保对变量的操作是不可中断的。可见性确保了一个线程对共享变量的修改对其他线程是可见的。有序性则允许在不影响单线程程序执行结果的情况下对指令进行重排序。
  2. JVM内部变化
    • 方法区变化:Java 8中,方法区(Method Area)的实现由永久代(PermGen)变为了元空间(Metaspace)。这是为了融合HotSpot JVM与JRockit VM而做出的努力,因为JRockit并没有永久代的概念。元空间使用本地内存,而不是虚拟机内存,从而避免了永久代常见的内存溢出问题。
    • 栈和堆的变化:Java 7和Java 8在栈和堆的基本结构上并没有显著变化。Java虚拟机的内存模型中仍然包括Java虚拟机栈、本地方法栈、堆和方法区。Java虚拟机栈和本地方法栈是线程私有的,而堆和方法区是线程共享的。
  3. 内存模型示例代码
    • 尽管Java 7和Java 8在内存模型上有所不同,但使用volatile关键字的示例代码在两者中的表现是相似的。volatile关键字在Java 8中仍然用于确保变量的可见性和禁止指令重排序。

程序员最关注的两个内存区域?

堆(Heap)和栈(Stack),一般大家会把Java内存分为堆内存和栈内存,这是一种比较粗糙的划分方式但实际上Java内存区域是很复杂的。

直接内存是什么

直接内存是Java中用于提高性能的一种重要内存管理方式,特别是在处理大文件读写和NIO操作时。然而,由于它不受JVM的GC管理,使用时需要特别注意内存分配和回收的问题。

  1. 定义
    • 直接内存是指Java堆外内存,即不属于Java虚拟机(JVM)运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
    • 它属于操作系统内存,不由JVM管理,而是直接由Java应用程序通过直接方式从操作系统中申请。
  2. 特点
    • 分配:直接内存的分配不会受到Java堆大小的限制,但会受到本机总内存的大小及处理器寻址空间的限制。
    • 性能:由于直接内存位于堆外,读写操作可以直接在内存和磁盘之间进行,避免了Java堆和native堆中来回复制数据,从而在某些场景下可以显著提高性能。
    • 回收:直接内存的分配与释放是通过一个Unsafe类型对象进行的(释放通过调用freeMemory),而不是通过Java的垃圾回收(GC)机制。
  3. 用途
    • 常用于NIO(New I/O)操作中,如ByteBuffer的分配。使用直接缓冲区(DirectBuffer)时,操作系统划分出的直接缓存区可以被Java代码直接访问,减少了内核态到用户态的相互拷贝,从而提高了文件读写操作的效率。
    • 学习直接内存的目的是因为Java 8中的元空间的落地方案就是直接内存实现的。
  4. 配置
    • 直接内存大小可以通过MaxDirectMemorySize参数进行设置。如果不指定,则默认与堆的最大值(-Xmx参数值)一致。
  5. 注意事项
    • 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
    • 如果直接内存使用不当,也可能导致OutOfMemoryError异常。

除了哪个区域外,虚拟机内存其他运行时区域都会发生OutOfMemoryError?

程序计数器

什么情况下会出现堆内存溢出?

  1. 对象过多或过大
    • 应用程序创建了过多的对象,并且这些对象在堆内存中占用了大量的空间。
    • 应用程序创建了非常大的对象,超出了堆内存能够容纳的大小。
  2. 内存泄漏
    • 内存泄漏是指应用程序不再需要某些对象,但由于程序中的错误,这些对象没有被垃圾回收器(Garbage Collector, GC)回收,导致它们仍然占用着内存。随着时间的推移,泄漏的对象会越来越多,最终耗尽堆内存。
  3. 静态集合类
    • 静态集合类(如静态HashMap、静态ArrayList等)的生命周期与应用程序的生命周期相同。如果静态集合类中的对象不再需要,但由于它们是静态的,所以垃圾回收器不会回收它们,从而导致内存泄漏。
  4. 缓存
    • 缓存是为了提高性能而存储的常用数据。但是,如果缓存中的数据量过大,或者缓存的清理策略不当,可能会导致堆内存溢出。
  5. 第三方库
    • 某些第三方库可能存在内存管理问题,导致内存泄漏或大量占用内存。
  6. 堆内存设置过小
    • 如果JVM启动参数中设置的堆内存大小(-Xmx)过小,无法满足应用程序的内存需求,也可能导致堆内存溢出。
  7. 无限递归或大量循环
    • 如果程序中存在无限递归或大量循环,每次递归或循环都创建新的对象,那么这些对象会不断占用堆内存,最终导致溢出。
  8. 大对象数组
    • 如果创建了一个大对象数组,并且数组中的每个对象都很大,那么整个数组会占用大量的堆内存。
  9. 死锁
    • 死锁可能会导致线程阻塞,无法继续执行,从而无法释放已经占用的内存资源,间接导致堆内存溢出。
  10. 垃圾回收器配置不当
    • 如果垃圾回收器的配置不当,可能会导致内存回收不及时或回收效率低下,最终导致堆内存溢出。

为了解决堆内存溢出问题,可以采取以下措施:

  • 分析堆转储(Heap Dump)文件,找出哪些对象占用了大量内存,并确定它们是否应该被回收。
  • 优化代码,减少不必要的对象创建和内存占用。
  • 修复内存泄漏问题,确保不再需要的对象能够被垃圾回收器正确回收。
  • 调整JVM启动参数,增加堆内存大小或优化垃圾回收器配置。
  • 使用内存分析工具(如VisualVM、MAT等)来监控和分析应用程序的内存使用情况。

空间什么情况下会抛出OutOfMemoryError?

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

如何设置直接内存容量?

通过 -XX:MaxDirectMemorySize指定,如果不指定,则默认与]ava堆的最大值一样。

Java堆内存组成?

堆大小=新生代+老年代。如果是]ava8则没有PermanentGeneration。
其中新生代(Young)被分为Eden和S0(from)和S1(to)。

Edem:from:to默认比例是?

Edem :from :to=8 :1 :1
此比例可以通过 -XX:SurvivorRatio 来设定

垃圾标记阶段?

在GC执行垃圾回收之前,为了区分对象存活与否,当对象被标记为死亡时,GC才回执行垃圾回收,这个过程就是垃圾标记阶段。

引用计数法?

比如对象a,只要任何一个对象引用了a,则a的引用计数器就加1,当引用失效时,引用计数器就减1,当计数器为0时,就可以对其回收。
但是无法解决循环引用的问题。

根搜索算法?

跟搜索算法是以跟为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活对象都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象已经死亡,便可以在 instanceOopDesc的 Mark World 中将其标记为垃圾对象。
在根搜索算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

JVM中三种常见的垃圾收集算法?

标记-清除算法(Mark_Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有的被标记对象。

缺点:

  • 标记和清除的效率都不高。
  • 空间问题,清除后产生大量不连续的内存随便。如果有大对象会出现空间不够的现象从而不得不提前触发另一次垃圾收集动作。

复制算法(Copying)

他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:
解决了内存碎片问题。
缺点:
将原来的内存缩小为原来的一半,存活对象越多效率越低。


标记-压缩算法(Mark-Compact)

先标记出要被回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。解决了复制算法和标记清理算法的问题。

分代收集算法?

当前商业虚拟机的垃圾收集都采用“分代手机算法",其实就根据对象存活周期的不同将内存划分为几块,一般是新老年代。根据各个年代的特点采用最适当的收集算法。

垃圾收集器

如果说垃圾收集算法是方法论,那么垃圾收集器就是具体实现。连线代表可以搭配使用。

Stop The World

进行垃圾收集时,必须暂停其他所有工作线程,Sun将这种事情叫做"Stop The World"。

Serial收集器

单线程收集器,单线程的含义在于它会 stop the world。垃圾回收时需要stop the world,直到它收集结束。所以这种收集器体验比较差。

PartNew收集器

Serial收集器的多线程版本,除了使用采用并行收回的方式回收内存外,其他行为几乎和Serial没区别。
可以通过选项“-XX:+UseParNewGC"手动指定使用 ParNew收集器执行内存回收任务。

Parallel Scavenge

是一个新生代收集器,也是复制算法的收集器,同时也是多线程并行收集器,与PartNew不同是,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
他可以通过2个参数精确的控制吞吐量,更高效的利用cpu。

分别是:-XX:MaxCcPauseMillis 和 -XX:GCTimeRatio

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。JDK 1.6中才开始提供。

CMS 收集器

Concurrent Mar Sweep 收集器是一种以获取最短回收停顿时间为目标的收集器。重视服务的响应速度,希望系统停顿时间最短。采用标记-清除的算法来进行垃圾回收。

CMS垃圾回收的步骤?
  • 初始标记(stop the world)
  • 并发标记
  • 重新标记(stop the world)
  • 并发清除
    • 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
    • 并发标记就是进行Gc Roots Tracing的过程。
    • 重新标记则是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录,这个阶段停顿时间一般比初始标记时间长,但是远比并发标记时间短。
    • 整个过程中并发标记时间最长,但此时可以和用户线程一起工作。
CMS收集器优点?缺点?

优点:
并发收集、低停顿

缺点:

  • 对cpu资源非常敏感。
  • 无法处理浮动垃圾。
  • 内存碎片问题。

G1收集器?

Garbage First 收集器是当前收集器技术发展的最前沿成果。jdk 1.6_update14中提供了 g1收集器。
G1收集器是基于标记-整理算法的收集器,它避免了内存碎片的问题。
可以非常精确控制停顿时间,既能让使用者明确指定一个长度为 M毫秒的时间片段内,消耗在垃圾收集上的时间不多超过N毫秒,这几乎已经是实时Java(rtsj)的垃圾收集器特征了。

G1收集器是如何改进收集方式的?

极力避免全区域垃圾收集,之前的收集器进行收集的范围都是整个新生代或者老年代。而g1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域垃圾堆积程度,维护一个优先级李彪,每次根据允许的收集时间,优先回收垃圾最多的区域。从而获得更高的效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

非洲养老号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值