JVM
1.什么是Java虚拟机
Java虚拟机(Java Virtual Machine, JVM)是运行Java应用程序的虚拟计算环境。它是Java平台的核心组件,负责解释和执行Java字节码(.class文件),并为Java程序提供运行时环境支持。
核心概念:
•字节码(Bytecode):Java源代码经过编译后产生的中间语言,具有平台无关性,能在任何支持JVM的平台上运行。
•类加载器(ClassLoader):负责加载 .class 文件,将字节码数据转换为JVM内部数据结构(如方法区的类定义),并执行类初始化过程。
•运行时数据区(Runtime Data Areas):
•堆(Heap):存储对象实例,是所有线程共享的内存区域。垃圾收集器管理堆内存,回收不再使用的对象。
•方法区(Method Area):存储类的结构信息(如字段、方法、常量池等)以及类加载器引用的类元数据。
•Java虚拟机栈(JVM Stack):每个线程拥有一个栈,存储方法调用时的局部变量、操作数栈、动态链接信息和方法返回地址等。
•本地方法栈(Native Method Stack):与JVM栈类似,但用于支持使用JNI(Java Native Interface)调用的本地(非Java)方法。
•程序计数器(Program Counter Register):每个线程拥有一个,存储当前正在执行的字节码指令的地址。
•执行引擎(Execution Engine):
•解释器(Interpreter):直接解释执行字节码。
•即时编译器(Just-In-Time Compiler, JIT):将热点代码(频繁执行的代码)编译为与目标平台相关的机器码,以提高执行效率。
•内存管理与垃圾收集(Garbage Collection, GC):自动管理内存分配与回收,防止内存泄漏,确保程序的长期运行。
生命周期:
•加载(Loading):将.class文件字节码数据读入内存,并转换为方法区的运行时数据结构。
•验证(Verification):确保字节码符合Java语言规范,防止非法操作或恶意攻击。
•准备(Preparation):为类的静态字段分配内存,并设置默认初始值。
•解析(Resolution):将符号引用(如类、接口、字段、方法的名称及描述符)替换为直接引用(内存地址)。
•初始化(Initialization):执行类构造器<clinit>()方法,初始化静态字段。•使用:JVM执行类的方法,处理类实例的生命周期。
•使用:new出对象程序中使用
•卸载(Unloading):当类不再被任何对象或类引用时,由垃圾收集器清除与该类相关的数据,并卸载类。
2.Java堆和栈的区别:
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在 堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
栈
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定 义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为 该变量分配的内存空间,该内存空间可以立即被另作它用。
堆
堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管 理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取 值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在 程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个 名称。
总结:
栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。
堆会GC垃圾回收,而栈不会。
栈内存是线程私有的,而堆内存是线程共有的。
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
3. 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中 的栈空间;
而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃 圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、 Survivor(又可分为From Survivor和To Survivor)、Tenured;
方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编 译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、”hello”和常量都是放在常量池 中,常量池是方法区的一部分。
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈 和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和 常量池空间不足则会引发OutOfMemoryError。
类加载器
1.类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
类加载器的类型:
启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库(C++)
扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类
应用类加载器(AppClassLoader):用于加载classPath下的类
自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
双亲委派 :加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
为什么使用双亲委派机制:
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证类库API不会被修改
2.JVM加载class文件的原理机制?
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的 Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java 程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加 载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与 所加载类对应的Class对象。
加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后 就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符 号引用替换为直接引用)三个步骤。
最后JVM对类进行初始化,包括:
1、如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2、如果类中存在初始化语句,就依次执行这些初始化语句。
3.Java类加载过程(难!!!)
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
4.Java对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。
1、对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、 线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即 对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
2、实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
3、对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
垃圾回收机制(GC)
1.什么是垃圾回收?
垃圾回收(GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
2.如何定位垃圾回收?
第一个是引用计数法,第二个是可达性分析算法
引用计数法:一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收(循环应用会引发内存泄漏)
可达性分析算法:扫描堆中的对象,看是否能够沿着 GC Root 对象[吕1] 为起点的引用链找到该对象,找不到,表示可以回收
标记清除算法 复制算法 标记整理算法 分代收集算法
标记清除算法:是将垃圾回收分为2个阶段,分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收
标记整理算法:标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
复制算法:将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
分代收集算法 :新生:老年=1:2
新生代,内部又被分为了三个区域。
伊甸园区Eden,新生的对象都分配到这里
幸存者区survivor(分成from和to)
Eden区,from区,to区【8:1:1】
工作机制
- 新创建的对象,都会先分配到eden区
- 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
4.对象是怎么从年轻代进入老年代的?
在下面四种情况下,对象会从年轻代进入老年代。
1、如果对象够老,会通过提升(Promotion)进入老年代,这一般是根据对象的年龄进行判断的。
2、动态对象年龄判定。有的垃圾回收算法,比如G1,并不要求age必须达到15才能晋升到老年代,它 会使用一些动态的计算方法。
3、分配担保。当 Survivor 空间不够的时候,就需要依赖其他内存(指老年代)进行分配担保。这个时 候,对象也会直接在老年代上分配。
4、超出某个大小的对象将直接在老年代分配。不过这个值默认为0,意思是全部首选Eden区进行分配。
5.MinorGC、MixedGC、FullGC的区别是什么?
6.垃圾回收器种类
串行垃圾收集器(默认) 并行垃圾收集器(默认) CMS(并发)垃圾收集器 G1垃圾收集器
串行垃圾回收器:
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
Serial 作用于新生代,采用复制算法
Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
并行垃圾回收器:
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
Parallel New作用于新生代,采用复制算法
Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器:
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
(初始标记è并发标记è重新标记è并发清理)
G1垃圾收集器:
应用于新生代和老年代,在JDK9之后默认使用G1
划分成多个区域,每个区域都可以充当 eden survivor old humongous,其中 humongous 专为大对象准备
采用复制算法
响应时间与吞吐量兼顾
分成三个阶段:新生代回收、并发标记、混合收集(参与复制的有 eden、survivor、old)
如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
7.Java的四种引用,强弱软虚
强引用:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
8.Java中会存在内存泄漏吗,请简单描述。
先解释什么是内存泄漏: 所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。java中有垃圾回收机制, 它可以保证当对象不再被引用的时候,对象将自动被垃圾回收器从内存中清除掉。 由于Java使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引 用,只要它们和根进程不可达,那么GC也是可以回收它们的。
java中的内存泄露的情况: 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通 俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这 个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系 统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象 一直被缓存引用,但却不再被使用。
JVM实践
1.JVM 调优的参数可以在哪里设置参数值(项目部署)
war包部署在tomcat中设置
jar包部署在启动参数设置
2.用的 JVM 调优的参数都有哪些?
设置堆空间大小: -Xms(堆的初始大小) -Xmx(初始化堆的最大值)
-Xms / -Xmx :堆的初始大小 / 堆的最大大小
-Xmn: 堆中年轻代的大小
-XX:-DisableExplicitGC :让System.gc()不产生任何作用
-XX:+PrintGCDetails:打印GC的细节
-XX:+PrintGCDateStamps : 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize : 设置新生代大小/新生代最大大小
-XX:NewRatio : 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution : 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold :设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio : 设置幸存区的目标使用率
虚拟机栈的设置:每个线程默认会开启1M的内存 -Xss
年轻代中Eden区和两个Survivor区的大小比例:如果不设置,则默认比例为8:1:1
年轻代晋升老年代阈值:(默认值:15)
设置垃圾回收收集器:通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
3.JVM调优工具:
4.Java内存泄漏的排查思路
5.CPU飙高排查方案与思路?
1.使用top命令(FinalShell)查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高
3.使用ps命令查看进程中的线程信息
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
6.JVM 的永久代中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。 注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native 内存区。
7. OOM你遇到过哪些情况,SOF你遇到过哪些情况
OOM:
1、OutOfMemoryError异常 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java Heap 溢出: 一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免 垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转 存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是 内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过 怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
2、虚拟机栈和本地方法栈溢出 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常 这里需要注意当栈的大小越大可分配的线程数就越少。
3、运行时常量池溢出 异常信息:java.lang.OutOfMemoryError:PermGenspace 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作 用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则, 将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区 内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
4、方法区溢出 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是 方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
异常信息:java.lang.OutOfMemoryError:PermGenspace 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在 经常动态生成大量Class的应用中,要特别注意这点。
SOF(堆栈溢出StackOverflow): StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超 过1m而导致溢出。 栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。