JVM基础总结

1. 内存区域(运行时数据区)

1.1 程序计数器

【作用】记录下一条指令的地址

【特点】线程独有,不会报出OOM,也不用GC

1.2 java虚拟机栈

【组成】栈中有很多栈帧(调用方法),栈帧包含:局部变量表、操作数栈、动态链接、方法返回地址。

还有就是一些压栈弹栈的操作

1.2.1 逃逸分析

【简述】新建的对象如果没能够逃逸出方法区,那么它就会在栈上分配

【详述】我们可以这么来看,只要一个方法里面创建的对象没有与外界发生联系(没有返回对象本体 or 传入和里面新建对象关联的),那么这个对象就无法逃逸出此方法区,因为此时还在栈上,我们为它在栈上分配一个空间。——这就叫做栈上分配

【线程同步省略】一些无用的同步标记我们在编译的时候会自动去除。

【标量替换】我们会替换掉一些标量。

1.2.2 StackOverFlowError 和 OutOfMemoryError

当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就会报出[StackOverFlowError]

Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出[OutOfMemoryError]。

【区别】一个是指定的栈区的异常,一个是内存里面的异常。栈区的异常我们可以通过动态扩展或者优化代码使得请求栈的深度变低来规避此错误;OOM异常很有可能是代码可能里面的错误

我们在使用的时候,使用递归来测试出这种错误。

1.3 本地方法栈

本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。

1.4 堆

【特点】堆是被所有线程所共享的,是内存管理最大的一处

【作用】此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

对象分配过程

【JDK改变的变化】随着JIT编译期的发展与逃逸分析技术逐渐成熟,基于逃逸分析的栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

堆的结构

【介绍】Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法

【JDK的演变】

在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:①新生代内存(Young Generation)、②老生代(Old Generation)、③永生代(Permanent Generation)

JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存(直接在内存空间里)。

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

修正(issue552):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。

动态年龄计算的代码如下

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
	//survivor_capacity是survivor空间的大小
	size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);

  size_t total = 0;

  uint age = 1;

  while (age < table_size) {

    total += sizes[age];//sizes数组是每个年龄段对象大小

	if (total > desired_survivor_size) break;

	age++;

  }
  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
	...
}

【常见的错误】堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。

java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误。(和本机物理内存无关,和你配置的内存大小有关!)

对象实例化过程

在局部变量表中创建引用,将引用指向堆空间。

  1. 判断对象所指向的类是否完成加载链接初始化【确定有这个类了】
  2. 分配堆内存,此时我们已经知道了占用大小,给实例对象分配空间【在堆中找空间】
  3. 初始化堆空间,赋默认值
  4. init初始化

1.5 方法区

【特点】方法区也叫做非堆;被各个线程所共享

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

2. JMM(Java的内存模型)

【概念】JMM(Java内存模型)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式

2.1 Java内存模型中的重排序

volatile

  1. 保证线程间变量的可见性。
    1. volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
  2. 禁止CPU进行指令重排序。
    1. 指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。所以在多线程环境下,就需要禁止指令重排序

volatile的实现原理(看不懂)

【解释:看不懂】有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:

将当前处理器缓存行的数据写回到系统内存

这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

volatile的内存语义

【特性】

可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

2.2 Java内存模型带来的问题

可见性问题——volatile

竞争现象

3. GC

3.1 对象存活判断:垃圾标记

引用计数算法

可达性分析算法(根搜索算法)

常见的垃圾回收器:hotspot、JRockit、J9、ZGC

3.2 垃圾回收算法

标记清除算法

复制算法

标记压缩算法

分代收集算法

3.3 java的不同引用

【强】只要强引用关系存在,永远不回收

【软】内存不够就回收,就看内存够不够(不一定报OOM才回收软引用,【只要让GC认为内存不够】,有一种情况:刚刚好放满了堆区老年代,就会去清除软引用。)

【弱】发现就回收

【虚】为了回收的时候给系统发通知,跟踪。

强引用

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

软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

所以,软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用

弱引用与软引用的区别在于:弱引用的对象拥有更短暂的生命周期。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

3.4 内存溢出,内存泄露

【内存溢出】性能监控调优,目前的JDK难以出现OOM。报出OOM之前会放一次Full GC大招。

没有空闲内存,垃圾收集器收拾收拾也无法提供更多内存。(超大对象就不行)

【内存泄露】对象不会再被程序用到,但是GC又不能对其进行回收 → 内存泄漏

3.5 分代收集

3.5.1 young(minor) gc/old(major) gc

3.5.2 full gc

整堆回收

触发条件

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

2.老年代空间不足时。

3.方法区空间不足时。

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

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

4. 类加载子系统


4.1 类加载器分类

  • 引导类加载器
  • 拓展类加载器
  • 应用类加载器
  • 自定义类加载器

【过程】

  • 加载:生成Class对象,加载各种类(引导、拓展、应用/系统类)
  • 连接:
    • 验证:确保Class文件满足JVM字节码的要求
    • 准备:先准备好变量容器(变量就是容器,先赋零值),static final修饰的在编译的时候已经分配好了。
    • 解析:将符号转变为直接引用
  • 初始化:
    • 【0】init执行默认构造器,子类加载init之前,父类完成加载(一个类只会被加载一次
    • 【1】clinit == 执行方法:== class init
    • Tips:执行静态代码块 static {}/静态的变量会在编译的时候就给予赋值

4.2 双亲委派机制

  • 如果一个类加载器接收到了请求,不会自己先去加载,而是会把请求交给父类加载器,直至顶层的类加载器
  • 顶层的类加载器如果能够处理,那么就返回成功,不能处理则交给子类加载,层层下传直到最底层的自定义类加载器。

【好处】

  • 防止类的重复加载,保证Java核心包的类不被替换
  • 按需请求:只有用某个类的时候才会去请求加载某个类,并且将这个类加载到内存中。
  • 区分类:同时双亲委派机制也可以帮助我们去区分两个类,两个类的包名不一样是不同的类,两个类的加载器不一样也不是同一个类。

4.3 沙箱安全机制

我们直接执行一个类的时候,进入方法我们就需要执行main方法,但是此时main方法所在的类需要被加载,而String没有main方法,所以报错。
开始使用的时候,我们自定义的String类会使用引导类加载器加载;而引导类加载器会使用rt.jar包下的String类,因为rt.jar包下的String类里面没有对应的方法,所以报错,这也体现了对源码的保护功能 == 沙箱安全机制

5. JVM调优

JVM调优有什么经验吗?

要明白一点,所有的调优的目的都是为了用更小的硬件成本达到更高的吞吐,JVM的调优也是一样,通过对垃圾收集器和内存分配的调优达到性能的最佳。

简单的参数含义

首先,需要知道几个主要的参数含义。

-Xms设置初始堆的大小,-Xmx设置最大堆的大小

-XX:NewSize年轻代大小,-XX:MaxNewSize年轻代最大值,-Xmn则是相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值

-XX:NewRatio设置年轻代和年老代的比值,如果为3,表示年轻代与老年代比值为1:3,默认值为2

-XX:SurvivorRatio年轻代和两个Survivor的比值,默认8,代表比值为8:1:1

-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。

-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代

-XX:MaxDirectMemorySize当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC

调优

为了打印日志方便排查问题最好开启GC日志,开启GC日志对性能影响微乎其微,但是能帮助我们快速排查定位问题。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log

一般设置-Xms=-Xmx,这样可以获得固定大小的堆内存,减少GC的次数和耗时,可以使得堆相对稳定

-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,方便排查问题

-Xmn设置年轻代的大小,太小会增加YGC,太大会减小老年代大小,一般设置为整个堆的1/4到1/3

设置-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题

6. JDK 监控和故障处理

jps:查看所有 Java 进程

jps:显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier,LVMID)。
jps -q :只输出进程的本地虚拟机唯一 ID。
jps -l: 输出主类的全名,如果进程执行的是 Jar 包,输出 Jar 路径。
jps -v:输出虚拟机进程启动时 JVM 参数。
jps -m:输出传递给 Java 进程 main() 函数的参数。

jstat: 监视虚拟机各种运行状态信息

jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具。

频繁FullGC怎么排查?

jinfo: 实时地查看和调整虚拟机各项参数

jmap:生成堆转储快照

jhat: 分析 heapdump 文件

jstack :生成虚拟机当前时刻的线程快照(死锁可用此命令查看)

JConsole:Java 监视与管理控制台

Visual VM:多合一故障处理工具

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

willorn

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

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

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

打赏作者

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

抵扣说明:

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

余额充值