JVM总结复习
Posted 分享录
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM总结复习相关的知识,希望对你有一定的参考价值。
0. 面试常见:
介绍java内存区域,JVM的内存模型和分区,运行时数据去
jvm中内存大体分为两部分,方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
本地方法栈(Native Method Stacks),本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
程序计数器(Program Counter Register),程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
方法区(Method Area),方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;
而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;
方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分.
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。
变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析?
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tWccTT6x-1622035907206)(C:\Users\1\AppData\Roaming\Typora\typora-user-images\image-20210526204012112.png)]
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
OutOfMemoryError: GC Overhead Limit Exceeded
:当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space
错误。(和本机物理内存无关,和你配置的对内存大小有关!)
JVM的常用调优参数有哪些?
设定堆内存大小,-Xmx:堆内存最大限制。
设定新生代大小。新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio:新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
新生代和老年代的区别
所谓的新生代和老年代是针对于分代收集算法来定义的,便于回收
新生代又分为Eden和Survivor两个区。加上老年代就这三个区。数据会首先分配到Eden区 当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。),当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。
如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,当然晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC
MinorGC和FullGC的区别,GC是什么时候触发的
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:MinorGC和Full GC。轻GC和重GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发轻GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC,对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比轻GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。
说⼀下堆内存中对象的分配的基本策略
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
java对象的创建
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
String s1 = new String(“abc”);这句话创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新?
JVM是Java虚拟机的缩写,Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中。
所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。当然只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。
深拷贝和浅拷贝
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
堆里面的分区有哪些? Eden,from,to,老年区,说说他们的特点!
GC的算法有哪些? 标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
谈谈JVM中,类加载器你的认识
1. JVM:
1.1 JVM的位置
运行在操作系统之上
1.2 JVM 体系结构
百分之99的JVM调优都是在堆中调优
1.3 三种JVM
Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
BEA JRockit
IBM 39 VM
我们学习都是:Hotspot
2. 虚拟机类加载机制
Java是运行在Java的虚拟机(JVM)中的,但是它是如何运行在JVM中了呢?
我们在IDE中编写的Java源代码被编译器编译成.class的字节码文件。
然后由我们得ClassLoader负责将这些class文件给加载到JVM中去执行。
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。这个过程被称作虚拟机的类加载机制。与那些在编译时需要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的
例如,编写一个面向接口的应用程序,可以等到运行时再指定其 实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络 或其他地方上加载一个二进制流作为其程序代码的一部分。这种动态组装应用的方式目前已广泛应用 于Java程序之中,从最基础的Applet、JSP到相对复杂的OSGi技术,都依赖着Java语言运行期类加载才 得以诞生。
2.1 类加载器
通过一个类的全限定名来获取描述此类的二进制字节流
2.2 双亲委派模型
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
加载过程:先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLo ader 逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader加载一 次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
Bootstrap classLoader
:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。ExtClassLoader
:主要负责加载jre/lib/ext目录下的一些扩展的jar。AppClassLoader
:主要负责加载应用程序的主函数类
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//-----??-----
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// Class Not Found异常就是这么来的
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
2.3 为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:
String.java
。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为JVM启动时就会通过bootstarp类加载器把rt.jar下面的核心类加载进来,所以自己重写的不会被加载),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
3. Java 内存区域
3.1 运行时数据区域
3.1.1 native——>本地方法栈
native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
它在内存区域中专门开辟了一块标记区域: Native Method Stack,本地方法栈,登记native方法
进入本地方法栈
调用本地方法本地接口 JNI (Java Native Interface)
**JNI作用:开拓展Java的使用,融合不同的编程语言为Java所用!**Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
在最终执行的时候,通过JNI加载本地方法库中的方法
例如:Java程序驱动打印机,管理系统,掌握即可,在企业级应用比较少
private native void start0();
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
return 语句。
抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
3.1.2 程序计数器
编译后的字节码在没有经过实时编译器编译前,是通过字节码解释器进行解释执行。其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作,字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,在执行引擎读取下一条指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
3.1.3 方法区
保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;,此区域属于共享区间;静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。
运行时常量池
每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;
字面值:就是像string, 基本数据类型,以及它们的包装类的值,以及final修饰的变量,简单说就是在编译期间,就可以确定下来的值;
符号引用:不同于我们常说的引用,它们是对类型,域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用;
存在这里面的数据,类似于保存在数组中,外部根据索引来获得它们
3.1.4 虚拟机栈
栈:就是先进后出
栈,也叫栈内存,主管程序的运行,生命周期和线程同步;线程结束,栈内存也就是释放。
对于栈来说,不存在垃圾回收问题一旦线程结束,栈就Over!每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。类似于下图的结构
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:
栈内存中:8大基本类型+对象引用+实例的方法,既存储局部变量表、操作数栈、动态链接、方法出口等信息。
栈帧:局部变量表+操作数栈
每执行一个方法,就会产生一个栈帧。程序正在运行的方法永远都会在栈的顶部
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
方法区+堆+栈的关系
3.1.5 堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组(保存所有引用类型的真实对象)。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
堆分区:新生代+老年代+永久代(元空间)
GC两种类:轻GC(普通的GC),重GC(全局GC)
为什么分代?
1、给堆内存分代是为了提高对象内存分配和垃圾回收的效率,99%的对象都是临时的
2、新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中。
3、根据不同年代的特点可以采用合适的垃圾收集算法。
4、新生代和老年代是垃圾回收的主要区域。
当新生代、老年代、元空间内存都满了之后才会报OOM
在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错
内存快照分析工具,MAT,Jprofiler,快速定位内存泄露
Dubug,一行行分析代码!
3.2 JVM各区会出现的问题
内存泄露和内存溢出的区别:
内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。
内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
4. 垃圾回收算法
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法:引用计数算法、可达性分析算法
引用类型
强引用:如
Object obj = new Object()
只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被 垃圾收集器回收。
弱引用:它也是用来描述非需对象的,被弱引用关联的对象只能生存到下一次 垃圾收集发生之前。
虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。
4.1 引用计数法:
堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加加减减,为0回收
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
4.2 复制算法:
每次GC都会将Eden活的对象移到幸存区中:一旦Eden区被GC后,就会是空的。当一个对象经历了15次GC还没死,就会被转移到老年代
好处:没有内存的碎片
坏处:浪费了内存空间(一个幸存区的空间永远是空:to)。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候;新生区~
4.3 标记–清除算法
标记:遍历内存区域,对需要回收的对象打上标记。
清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
4.4 标记-整理算法
(标记-清除-压缩算法)
清除之后对内存进行整理
再改进:先标记清除几次之后,再压缩1次
4.5 分代回收算法
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代:对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。
5. JMM
5.1 什么是JMM?
JMM:(Java Memory Model的缩写)缓存一致性协议,用于定义数据读写的规则
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存
5.2 JMM的意义
解决共享对象可见性这个问题: volilate,一旦刷新了就会很快的同步到主内存中。
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
------------END-----------
更多原创文章请扫描上面(微信内长按可识别)二维码访问我的个人网站(https://www.xubingtao.cn),或者打开我的微信小程序:可以评论以及在线客服反馈问题,其他平台小程序和APP请访问:https://www.xubingtao.cn/?p=1675。祝大家生活愉快!
以上是关于JVM总结复习的主要内容,如果未能解决你的问题,请参考以下文章
JVM学习——全面总结Java的GC算法和回收机制---转载自http://www.cnblogs.com/kubixuesheng/p/5208647.html