面渣逆袭-JVM
难受,还是需要看JVM,时间紧任务重呀,加油吧。
一、引言
1、什么是JVM?
JVM是java虚拟机,是java实现跨平台的关键,将java 字节码翻译成系统能够识别的机器码
说说JVM的其他特性?
- JVM自动管理内存,通过垃圾回收器回收不在使用的对象,并释放内存空间
- JVM包含一个及时编译器JIT,他可以在运行时将热点代码缓存到codeCache中, 下次执行的时候不用在一行一行的解释,而是直接执行缓存后的机器码,执行效率大幅提高。
- 任何可以通过Java编译的语言,比如Groovy等都可以在JVM上面运行。
为什么要学习JVM?
可以帮助我们开发者更好的优化程序,避免内存泄漏。
了解JVM的内存模型和垃圾回收机制,可以帮助我们更合理的配置内存,减少GC停顿。
了解JVM的类加载机制,可以帮助我们排查加载冲突或异常。
2、说说JVM的组织架构
推荐阅读:大白话带你认识 JVM
JVM大致可以分为三个部分:类加载器、运行时数据区和执行引擎。
- 类加载器:负责从文件系统、网络或者其他地方加载Class文件,将class文件中的二进制数据读取到内存中。
- 运行时数据区:JVM执行java程序时,需要在内存中分配空间来处理数据,这些内存区域按照java虚拟机规范可以划分为:方法区、堆、虚拟机栈、程序计数器和本地方法栈。
- 执行引擎:jvm的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器JIT和垃圾回收器。
二、内存管理
⭐️ 3、能说一下jvm的内存区域吗?
推荐阅读:深入理解 JVM 的运行时数据区
jvm内存可以分为:方法区、堆、虚拟机栈、程序计数器、本地方法栈。
线程共享的: 方法区、堆
线程不共享的:虚拟机栈、程序计数器、本地方法栈。
介绍一下java虚拟机栈?
虚拟机栈的生命周期与线程相同。
作用:当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口扽个信息,然后栈帧会被压入虚拟机栈中。当方法执行完毕后,栈帧会从虚拟机栈中移除。
对于静态方法,由于不需要访问实例对象this,因此在局部变量表中不会有任何变量。
对于非静态方法,即是时一个完全空的方法,局部变量表中也会有一个用于存储this引用的变量,this引用执行当前实例对象,在方法巧用被隐式传入。
静态方法属于类级别方法,不需要this引用,也就没有局部变量。
介绍一下本地方法栈?
本地方法栈和虚拟机栈的区别在于,虚拟机栈是为了jvm执行java编写的方法服务的,而本地方法栈是为了java嗲用本地native方法服务的,通常由c或者c++写的。
当java调用一个native方法时,jvm会切换到本地方法栈来执行这个方法。
介绍一下java堆?
堆是jvm中最大的一块内存,被所有线程共享,在jvm启动时创建,主要用来存储new出来的对象。
栈上存的是对象的引用,堆上存的真正的对象数据。
堆事垃圾收集器管理的目标区域。
从内存的角度来看,由于垃圾收集器大部分是机遇分代理论设计的,所以堆又被细分为:
- 新生代
- 老年代
- Eden空间
- From Survivor空间
- to Survivor空间
”所有对象都会被分到堆上“就不在那么绝对了。
从jdk7开始,jvm默认开启逃逸分析,意味着如果某些方法中的毒香引用没有被返回或者没有在方法体歪使用,也就是未逃逸出局,那么对象可以直接在栈上分配内存。
堆和栈的区别?
堆线程共享,不由单个方法调用决定,可以在方法调用之后存在,知道没有对象引用,被垃圾回收器回收
栈线程独有,主要存放的是局部变量,方法参数和对象引用,会随方法调用结束而自动释放。
介绍一下方法区?
方法区并不是真实存在的,是一个逻辑概念,用于存储已经被jvm加载的类信息、变量、静态变量、JIT编译后的代码。
变量在堆栈的什么位置?
对于局部变量,存放在栈帧的局部变量表里面。
对于静态变量,存放在方法区中,jdk7中是永久带,jdk8以后是元空间。
4、说一下JDK 6,7,8内存区域的变化?
JDK 6 使用永久带来实现方法区。
JDK 7 也是使用永久带来实现方法区,但是有些细微的变化,就是字符串常量池、静态变量存放在堆上。
JDK 8 的时候,直接划分出一个内存区域,叫做元空间,来取代之前的永久带,来存放字符串常量池、类常量。
5、为什么使用元空间来代替永久带?
因为永久代会导致java应用程序更容易的出现内存溢出的问题,因为它收到jvm内存大小的限制。
java7的时候前进了一小步, 将常量池、静态变量放到了堆上。
java8的时候完成元空间替换永久带,这样的好处就是不在受jvm内存的限制,只要系统内存足够,就可以一直用。
6、对象的创建过程了解嘛?
首先判断是否已经加载过了, 如果加载过了,直接分配内存(初始化变量);如果没有加载过,使用类加载器加载。
后面就是对象内存初始化、设置对象头、执行init方法。
对象的销毁?
使用可达性算法判断对象是否存活,如果不可达就会被回收。
垃圾收集器通过标记清除、标记复制、标志整理等算法来回收内存。
8、new对象时,堆灰发生抢占嘛?
会
new对象时,指针会向右移动一个对象大小的距离,假如线程a正在给对象s分配内存,这个时候线程b为对象l分配内存,两个线程就会发生抢占。
jvm如何解决堆内存分配的竞争问题?
jvm为每个线程保留了一小块内存空间,被称为TLAB,也就是线程本地分配缓冲区,用于存放该线程分配的对象。
当线程需要分配对象的时候, 直接在tlab中分配,只有当tlab用尽或者对象太大需要直接在对中分配时,才会使用全局分配指针。
9、能说一下对象的内存布局嘛?
对象的内存布局有java虚拟规范定义,不同的实现细节个有不同,下面使用hotspot举例说明
说说对象头的作用?
推荐阅读
jvm可以划分为三个部门,分别是类加载器(class loader)、运行时数据区(runtime data area)和执行引擎(execution engine)
一、一文彻底搞懂 Java 类加载机制
类从被加载到jvm开始,到卸载出内存,整个生命周期分为7个阶段,分别是加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析这三个阶段统称为连接。
除去使用和卸载,就是java的类加载过程,这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
- 载入 loading
jvm在改阶段主要是将字节码从不同的数据源转化为二进制字节流,加载到内存中,并生成一个代表该类的java.lang.Class对象。
- 验证 verification
jvm会在这个阶段对二进制字节流进行校验,只有符合jvm字节码规范的才能被jvm正确执行。该阶段保证了jvm安全的重要保障。
- 准备 preparation
jvm在该阶段会对类变量(也称为静态变量,static关键字修饰的)分配内存空间,并初始化默认值,如0、0L、null、false,如下面一段代码:
public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";
chenmo不会被分配内存,而wanger会,但是wanger的初始值不是王二,而是null;static final称为常量,一旦赋值是不能修改的,所以cmower在准备阶段的值为“沉默王二”,而不是null
- 解析 resolution
该阶段将符号引用转化为直接引用。符号引用以一组符号来描述所引用的目标,在编译时,java类并不知道所引用的实际地址,因此只能使用符号引用来代替。直接引用通过符号引用进行解析,找到引用的实际内存地址。
解析阶段主要的工作:类或接口的解析、类方法解析、接口方法解析、字段解析
- 初始化 initialization
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被设置了默认值,而在初始化阶段,类变量将赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
String s =new String("hello")
,使用new关键词来实例化一个字符串对象,那么这个时候,就会调用string类的构造方法来进行实例化。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
初始化时机包括以下这些:
创建类的实例;访问类的静态方法或者静态字段;
使用java.lang.reflect包的方法对类进行反射调用。
初始化一个类的子类。
jvm启动时,用户指定的主类(包含main方法的类)将被初始化。
类加载器
AppClassLoader 应用类加载器
PlatformClassLoader 平台类加载器
按理说,扩展类加载器的上层是启动类加载器,但启动类加载器是虚拟机的内置类加载器,通常表示为null。
类加载器可以分为四种类型:
- 引导类加载器:负责加载jvm基础核心类库,如rt.jar
- 扩展类加载器:负责加载java扩展库中的类,例如jre lib ext 目录下的类或由系统属性java.ext.dirs指定位置的类
- 系统(应用)类加载器:复杂加载系统类路径(java.class.path)上指定的类库,通常是你的应用类和第三方库。
- 用户自定义类加载器:java允许用户创建自己的类加载器,通过继承java.lang.ClassLoader类的方式实现,这里需要动态加载资源、实现模块化架构或者特殊的类加载器时非常有用。
双亲委派模型
是java类加载使用的一种机制,用于确保java程序的稳定性和安全性。在这个模型中,类加载器在尝试加载一个类时,通常会先委派其父加载器去尝试加载,只有父加载器无法加载该类时,子加载器才会尝试自己去加载。
- 委派给父加载器:当一个类加载接受到类加载请求的时候,它首先不会尝试自己加载,而是将这个请求委派给他的父加载器。
- 递归委派:这个过程会递归向上进行,从启动类加载器开始,再到扩展类加载器,最后到系统类加载器。
- 加载类:如果父加载器能够加载,那就使用父类的加载器,如果父类加载不了,子加载器才会尝试自己去加载。
- 安全性和避免重复加载:这个机制可以确保不会重复加载类,并保护java核心api的类不会被恶意替换。
双亲委派模型的优点:那就是java类随着他的类加载器一起具备了一种带有优先级的层级关系,这对于保证java程序的稳定运作很重要。
双亲委派模型能够确保同一个类最终会被特定的类加载器加载。
二、深入理解垃圾回收机制
垃圾回收概念(garbage collection gc)对内存堆中已经死亡或者长时间没有使用的对象进行清除和回收。
垃圾判断算法
- 引用计数法
通过在对象头中分配一个空间用来保存对象被引用的次数,当该对象的引用次数为0时,那么该对象就需要被回收。
问题:无法解决循环依赖的问题
- 可达性分析算法
通过gc root作为起点,然后向下搜索,搜索走过的路径称为引用链,当一个对象到gc root之间没有任务引用相连时,即从gc roots到该对象节点不可达,则证明该对象需要垃圾收集了。
Stop The World
在垃圾回收过程中,jvm会暂停所有用户线程,这种暂停就叫做stop the world事件。这样做的主要目的就是防止垃圾收集过程中,用户线程修改了堆中的对象,导致垃圾收集器无法准确的收集到垃圾。
垃圾回收算法
确定了哪些对象是垃圾,这个时候就需要进行垃圾回收,“如何高效的进行垃圾回收”就成了问题。
- 标记清除算法
mark sweep是最基础的一种垃圾回收算法,它分为两部分,先把内存区域中的这些对象进行标记,哪些属于可回收的标记出来,然后把这些垃圾拎出来清理掉。
清除掉的垃圾变成可使用的空闲内存,等待再次使用,逻辑清晰,并且好操作,但是有一个问题,就是内存碎片太多。碎片太多可能会导致程序运行过程中需要分配较大内存时,无法找到足够的连续内存空间,而不得不触发新一轮的gc。
- 复制算法
copying 是在标记清除算法上演化而来,是将内存划分为两个相等的快,每次只使用其中一块,占满之后,就讲活着的对象复制到另一块上,然后将原来的整个处理掉,保证了内存的连续性、逻辑清晰、运行高效。
但是有一个明显的问题,就是内存使用量变小了。
- 标记整理算法
mark compact,标记过程和标记清除算法一样,但是后续不一样,不是直接将对象清除,而是将所有存货的对象都向一端移动,在清理到端边意外的内存区域。
这种算法看起来很美好,但是内存变动更频繁,需要整理所有活着对象的引用地址,在效率上比复制算法差很多。
- 分代收集算法
generational collction,严格来说不是一种思想,而是上面三种的结合,产生的针对不同情况所采用不同算法的一套组合拳。
根据对象存活周期不同将内存划分为几块,新生代和老年代,根据各个代的特点采用合适的算法。
新生代有大批对象死去,只有少量存活,所以使用复制算法,只需要复制少量对象。老年代对象存活时间长,没有额外空间对它进行分配担保,必须使用标记清理或者标记整理来回收。
eden区
对象会在新生代的eden区进行分配,当eden空间不足时,jvm会发起一次minor gc,相比于major gc,minor gc更加频繁。
eden上没有被回收的对象会到survivor的from区,如果from区不够,直接进入to区
Changelog
4c155
-on