文章目录
1、介绍一下JVM的内存区域(运行时数据区)
JVM中内存分成若干部分:
堆
、方法区
、虚拟机栈
、本地方法栈
、程序技术器
其中堆和方法区是线程共享的部分,其他是线程隔离的(也可以说是线程安全的)
堆
堆的基本介绍
Java堆是用来存储实例对象和数组对象的,由于存在逃逸分析技术(分析这个对象不会被其他方法或者线程调用),也可以分布在栈上,随着出栈而销毁,同时,java堆也是垃圾回收的主要区域,由于现在垃圾收集器基本都采用分代垃圾收集算法,所以Java堆还是可以细分为:新生代和老年代。
堆上对象分配内存情况
Java堆在物理上可以不连续,只要逻辑连续就好。在堆上分配对象的方法有:
指针碰撞
和空闲列表
,前者是在堆内存规整的情景下,所有用过和空闲的内存中间有明确的分界线,而后者用空闲列表来记录内存的使用情况,规整由垃圾回收器是否压缩整理来决定(这里又设计垃圾回收期的知识
)堆上爆出的异常
空间不足抛出OutOfMemoryError
堆上对象的访问方式
通过栈上的reference数据来操作堆上的具体对象,目前主流的访问反说个事有
使用句柄
和直接指针
。句柄的话,Java堆中将会划分出一块内存来作为句柄池, reference中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据域类型数据各自的具体地址信息。直接指针的话:reference中存储的直接就是对象地址。前者稳定后者效率高。2、方法区
方法区域Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量,即时编译后的代码、运行时常量池中的数据。*
其中运行时常量池,用于存放编译期生成的各种字面量和符号引用(字面量:文本字符串,8中基本数据类型的值,被声明为final的常量等。符号引用:类的方法和全限定名,字段的名称和描述符,方法的名称和描述符)。运行时常量池相对于Claass常量池具有动态性;
可以在运行期间利用String.inteern()方法将常量放入池中
- 当方法区无法满足内存分配需求时,抛出OutOfMemoryError
3、虚拟机栈
虚拟机栈是java方法执行的内存模型,线程私有,每个方法执行都活创建一个栈帧,
用于存储:
局部变量表
,操作数栈
、动态链接(运行时将方法区的符号引用转换为直接引用,真的可以调用了)和
方法出口信息`每一个方法从调用直至执行结束,就对应栈帧从虚拟机中入栈到出栈的过程
StackOverflowError:线程请求的栈深度大于虚拟机所云溪的深度
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存
4、本地方法栈
JVM调用本地方法
5、程序技术器
记录当前线程锁执行到的字节码的行号,每个线程都有一个程序计数器,唯一没有
OutOfMEMORY
的区域。6、直接内存
**运行时内存区域外规定的堆外内存:直接使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中 显著提高性能, 因为避免了在Java堆和Native堆中来回复制数 **
2、如何判断对象已经死亡
引用计数法和可达性分析算法
引用计数法
** 对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加1; 当引用失效时, 计数器值就减1; 任何时刻计数器为0的对象就是不可能再被使用的**
目前主流的虚拟机中并没有选择这个算法来管理内存,其中最主要的原因是他很难解决对象之间循环引用的问题
2、可达性分析算法
这个算法基本思想就是通过一系列被成为GC Roots的对象作为起点,从这些节点 开始向下搜索,节点所走过的路径成为引用链,当一个对象到GC ROOTS 没有任何引用链相连的话,则证明此对象不可用。
可以作为GC ROOTS的对象包括
- 虚拟机栈中引用对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法引用的对象
3、对象帧的死了吗
即使在可达性分析算法中不可达的对象, 也并非是“ 非死不可” 的, 这时候它们暂时处于“ 缓刑” 阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程: 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize( ) 方法。 当对象没有覆盖finalize( ) 方法, 或者finalize( ) 方法已经被虚拟机调用过, 虚拟机将这两种情况都视为“ 没有必要执行 。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被帧的回收
3、简述强、软、弱、虚引用
强引用
如果一个对象具有强引用,垃圾回收器绝不会回收它,当内存空间不足,
Java虚拟机宁愿抛出OutOfMemorryError错误,使得程序异常终止
。
软引用
只要内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存
。创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。PS:图片编辑器,视频编辑器之类的软件可以使用这种思路。
- 弱引用
相对于软引用,弱引用关联的对象只能生存到下一次垃圾回收之前;弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,
在ThreadLocal中使用到
虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个需引用加入到域之关联的引用对列中。这样就可以通过判断引用队列中是否已经加入了虚引用来了解被引用的对象是否将要被垃圾回收器回收
虚与软、若的区别:软 弱引用都是在引用对象被垃圾回收器回收后,Java虚拟机才把引用加入到域之关联的引用队列中,而需饮用对象在垃圾回收前,Java虚拟机把引用加入到与之关联的引用对列中
4、简述垃圾收集算法以及各自特点
- 标记清除算法
首先标记处所需要回收的对象,在标记完成之后统一回收所有被标记的对象,他是最基础的收集算法
缺点 1、效率问题 2、空间碎片
- 标记整理算法
标记过程扔与标记-清除算法一样,但后续步骤不是直接对客回收对象回收,而是让所有存货的的对象都向上移动一段,然后直接清除掉端边界以外的内存
- 复制算法
将内存分为大小相等的两块,每次使用其中的一块,当着一块的内存使用完后,就将会存货的对象赋值到另一块去,然后再把使用的空间一次清理掉。这样就是的每次的内存回收都是对内存空间的一半进行回收。但是,为了手机垃圾,将内存使用量降低为一般,成本较高。所以,一般不会1:1划分边界,可以分一块较大的Eden区空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor。当回收时,将Eden和Survivor中还存货的对象一次性复制到另一块Survivor上,最后清理Eden和Survovore空间。大小比例一般为8:1:1,每次浪费10%的Survivor空间。
但是这里有一个问题就是如果存货大于10%,怎么办?
这里采用一种分配担保策略:多出来的对象直接进入老年代。
- 分带收集算法(结合以上算法)
一般将Java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
**比如在新生代中,每次收集对象都会有大量对象死去,所以可以选择复制算法 ,只需要付出少量对象的赋值成本就可以望海潮呢个每次垃圾收集。而老年到对象的存活几率比较高所以我们可以选择 标记-清理或标记整理算法
Major GC与Full GC分别发生在什么时候
二者的区别
Minor GC
是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常平凡,一般回收速度也比较i快。
Major GC/Full GC
是老年代GC,指的是发生在老年代的GC,出现Major GC一般经常会伴有Minor GC,Major GC的速度比Minor GC慢的多。
何时发生
(1)Minor GC发生:当jvm无法为新的对象分配空间的时候就会发生Minor gc,所以分配对象的频率越高,也就越容易发生Minor gc。
(2)Full GC:发生GC有两种情况,①当老年代无法分配内存的时候,会导致MinorGC,②当发生Minor GC的时候可能触发Full GC,由于老年代要对年轻代进行担保,由于进行一次垃圾回收之前是无法确定有多少对象存活,因此老年代并不能清除自己要担保多少空间,因此采取采用动态估算的方法:也就是上一次回收发送时晋升到老年代的对象容量的平均值作为经验值,这样就会有一个问题,当发生一次Minor GC以后,存活的对象剧增(假设小对象),此时老年代并没有满,但是此时平均值增加了,会造成发生Full GC
5、常见的垃圾收集器有哪些
如果说收即算法是内存回收的方法论,那么收集器就是内存回收的具体实现。
垃圾回收器主要有
- Serial
- Serial Old
- ParNew
- Paralle Scavenge
- Paralle Old
- CMS
- G1
Serial(新生代)
单线程收集器,不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,重要的是在它进行垃圾收集工作的时候必须暂停其他所有工作线程(Stop the World)
ParNew(欣赏带)
其实就是Serial收集器的多线程版本,随着CPU的增加,可以显示出有时
并行(pARalllel):值多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发(Concurrent):指用户线程与垃圾收集线程同时工作(单不一定是并行,可能会交替执行)用户程序在继续执行,而垃圾收集器运行在另一个CPU上、
Parallel Scavenge(新生代)
使用复制算法,并行的多线程收集器,与Parnew类似,但侧重点是吞吐量,可以设置参数来调整最大垃圾收集停顿时间和吞吐量的大小。
以上是新生代收集器,采用复制算法
Serial Old
是Setial收集器的老年版本,它同样是一个单线程收集器\
Parallel Old
是Parallel Scavenge收集器的老年版本。使用多线程和标记整理算法。在注重吞吐量以及CPU资源配合的场合,都可以优先考虑Parallel Scavenege收集器和Paralklel Old收集器
CMS(Concuurent Mark Sweep)
老年代收集器,采用标记-清除算法,是一种 以获得最短回收停顿时间为目标的收集器
它非常符合注重用户体验的应用上使用,,整个过程分为四个步骤:初始标记,并发标记,重新标记和并发清除
- 初始标记:暂停所有其他线程,并记录下直接与GC roots相连的对象,速度很快
- 并发标记:同时开启GC和用户线程,从GC root继续向下进行标记,但是用户线程会继续更新对象的引用域
- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行时而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段时间长,远远比并发标记时间段
- 并发清除:开启用户线程,同时GC线程开始为标记的区域8做清扫
优点
:并发清除,低延迟。
缺点
:
- 采用标记-清除会产生碎片
- 无法清除浮动垃圾,因为在并发清除过程中,用户线程还在继续执行,还会不断产生垃圾,这部分职能等待下次GC时才处理
- 对CPU特别敏感,由于并发标记和并发清除适合用户线程并发执行,所以会导致用户程序变慢,总的吞吐量降低
G1收集器
唯一一个同事可以用于老年代和年轻代的垃圾收集器,采用标记-整理算法,避免碎片
该收集器,将堆内存分为大小相等的region区域,并维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region,把内存化整为零,但是由于引用关系的存在,仍然存在如何避免全集扫描问题,这里采用一个region用一个remembered Set进行记录用用关系,避免可达性分析阶段的全区域垃圾扫描
G1大致分为四个步骤:初始标记、并发标记、最终标记、筛选回收
初始标记,并发标记和CMS收集器类似
最终标记,将并发阶段对象变化记录在线程Rememver Set logs里面,最终把ReMemberesd Set LOGS 的数据合并到Remember Set中,这一阶段需要停顿先生,但是可并行执行。
筛选回收,对每一个Region的价值和成本进行筛选,根据用户期望的GC停顿时间,得到做好的回收方案并回收
特点:并发性强,分代回收,标记整理进行空间整合,可以预测挺短时间
6、吞吐量优先和响应优先的垃圾收集器如何选择
- 吞吐量优先
新生代采用
Parallel Scavenge
,老年代采用Parallel Old
,并配置多个线程进行回收,设置参数来调整最大垃圾收集挺短时间和吞吐量的大小。
响应时间优先
设置老年代的收集器是CMS,年轻代是ParNew(多线程)
7、内存分配与回收策略(对象何时激怒老年代)
大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发去一次Minor GC.
1、Minor gc和Full GC有什么不同?
Minor GC
:值发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般较快
Major GC
:值发生在老年代的GC
Full GC
:清理整个空间包括年轻代和老年代(major GC比Minor GC慢10倍以上
)2、什么时候对象进入老年代?
大对象
(需要大量内存连续的)直接进入老年代空间分配担保
:新生代对象放在Eden,当Edern被填满后,垃圾回收后存货的对象赋值放入From,当From满了,回收后存货的对象被赋值到To区域,Edenc存货的也直接进入To区域,源From区域被清空,当To被填满后,如果存活的对象还活着,直接进入老年代(空间分配担保)年龄判定
:年龄计数器会为对象记录年龄,每经过一次GC仍然活着的,年龄加1,当超过设定的值,直接进入老年代。或者动态对象年龄判定,当如果Survivor空间中相同年龄所有对象大小的综合大有Survivor空间的一般,年龄大于或等于该年龄对象就可以直接进入老年代,无需达到要求的年龄3、空间分配担保
安全的Minor GC:老年代中最大可用的连续空间大于新生代所有对象的空间;
冒险的Minor GC:老年代中最大可用的连续空间大于历代晋升到老年代的平均水平且允许担保失败;如果小于平均值,则直接进行full gc,让老年代腾出空间
8、虚拟机性能监控和故障处理工具?
jps:JVM process Status Tool:显示指定系统内所有HotSpot虚拟机进程
jstat:JVM statistics Monitoring Tool
:用于收集HotSpot虚拟机各方面的运行数据
jinfo:Configuration Info for Java
:显示虚拟机的配置信息
jmap:Memory Map for Java
:生成虚拟机的内存转储快照
jstack:Stack Trace for JAVA
:显示虚拟机栈的快照信息
9、Class类文件结构
Class文件是Java虚拟机执行引擎的数据入口,也是java技术体系的基础构成之一。
包括:
魔数
: 确定这个文件是否为一个能被虚拟机接受的Class文件
版本号
常量池
: 常量池中主要存放两大类常量:字面量和符号引用。
访问标志
: 这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,abstract类型,
类索引,父类索引,接口索引集合
:确定一个类的继承关系
字段表集合
: 字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量
方法表集合
: 法里的Java代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里
属性表
:Code属性
,Exception属性
,LineNumberTable
,LocalVaraiavleTable
字段表包含什么信息?
可以包括的信息有: 字段的作用域( public、 private、 protected修饰符) 、 是实例变量还是类变量( static修饰符) 、 可变性( final) 、 并发可见性( volatile修饰符, 是否强制从主内存读写) 、 可否被序列化( transient修饰符) 、 字段数据类型( 基本类型、 对象、 数组) 、 字段名称。
10、简述JVM中的类加载机制
虚拟机把描述类的数据从class文件加载到内存,并对数据进行
校验
,准备
,解析
,初始化
加载
通过一个类的全限定 类名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构化转为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
> 为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
为static变量分配空间并设置静态变量的默认初始值,即零值。
- 解析
作用:将常量池中的符号引用解析为直接引用
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式定义在虚拟机规范中
- 直接引用就是直接指向目标的指针、相对便宜量或一个简洁定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口放啊、方法类型等,对应常量池中的
- 初始化
开始真正执行定义的java代码,执行<clinit()>反方, 自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来 ,首先会执行父类*
11、简述JVM中类加载器以及双亲委派模型?
启动类加载器 BootStrap ClassLoader
这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别的,如
rt.jar
,名子不符合的类库即使放在lib目录中也不会被加载),类库加载到虚拟机内存中。2、扩展类加载器 Extension clASSlOADER
这个加载器有
sun.misc.Launcher&ExtClassLoaders
实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量锁指定的路径中的所有类库,开发者可以直接使用扩展类加载器3、应用程序类加载器(Application ClassLoader)
这个类加载器有
sun.misc.Launcher&AppClassLoader
实现,由于这个类加载器是ClassLoader
中的getSystemClassLoader()
方法的返回值,所以一般称为系统类加载器。它负责加载用户类路径(ClassPath)上锁指定的类库,开发者可以直接使用这个类加载器,要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里父子关系通常是子类同归哦组合关系而不是继承关系来复用父加载器的代码双亲委派模型的工作过程
如果一个类加载器收到类加载的请求,先把这个请求委派给父类加载器去完成(所有的加载请求最终都传送到顶层的
启动类加载器
中),只有当父类加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。(原因是如果用户自己编写一个称为java.lang.object的类,并放在程序的ClasspATH中,那么系统中会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也会变得一片混乱)如果自己写一个java.lang.String会被加载吗
答案是否定的。我们不能实现。因为双亲委派机制是就可以打破的,我们可以自己写一个
CalssLoader
来加载自己写的JAVA.LANG.String
类,但是你会发现也不会加载成功,具体就是因为针对Java,.*
开头的类,JVM
的实现中已经保证了必须有bootStrap
来加载但是双亲委派也可以被破坏掉,比如自定义一个
String
类但是包名不是Java.*
,可以放在用户目录中进行加载
12、简述JVM中静态分派和动态分派
- 静态分派:依赖静态类型定位方法的分排,发生在编译期间,典型的应用为方法的重载
- 动态分派:在运行时期根据实际类型来确定方法的分派,发生在程序运行时,典型应用是方法重写,也会动态的一种体现。根据转型来确定是否调用父类还是子类的方法
- 虚方法和非虚方法
- 非虚方法(所有static方法+final/private方法)通过
invokeSpecial
指令调用,对这个非虚方法的符号引用将转为对应的直接应用,即转为直接引用方法,在编译完成时就确定唯一的调用方法**- 虚方法是通过
invokevirtual
指令调用,且会有 静态或者动态分派。具体先根据编译时期方法接收者和方法参数的静态类型来分派,再在运行期只根据方法接受者的实际类型来分派
13、JVM启动模式之client与Server
- 指定jvm启动模式
jvm启动时,通过
-server
或者-client
参数指定启动模式2、 client模式server模式的区别
编译器方面
当虚拟机运行在client模式时,使用的是一个代号为c1的轻量级编译器,而server模式启动时,虚拟机采用的是相对重量级,代号为c2的编译器;c2编译器比c1编译器编译的时候相对来说更加彻底,服务起来之后,性能更高。
gc方面
client模式下的新生代(Serial收集器)和老年代(Serial Old)选择的是串行gc
server模式下的新生代唉选择并行回收gc,老板年带选择并行gc
启动方面
client模式启动快,编译块,占用内存少,针对桌面应用程序设计,优化客户端环境的启动时间。
Server模式启动慢,编译更完全,编译器是自使用编译器,效率高,针对服务端因公设计,优化服务器环境的最大化程序执行速度
注:一般来说系统应用选择有两种方式:**吞吐量优先和停顿时间优先,对于吞吐量优先的采用
Server
的并行gc(Paralelel Scavenge),对于时间优先的选择并发gc(CMS)
14、如何优化Java GC
这个属于较为开放题目,可以结合自即项目实战触发,体现JVM调优。
- 可以说下GC是什么?可以说到垃圾回收算法、收集器等
- 然后优化方向
- 将进入老年代的对象数量降到最低==
- 减少Full GC的执行时间
- 优化JVM参数:比如堆和栈大小,设置垃圾收集器的模式
- 使用
StringBuilder
或StringBuffer
来代替String
- 尽量少输出日志
- 。XML和JSON解析过程往往占用了最多的内存
- 最后,从实际出发:大对象、置空、对象重用等。
表1:GC优化需要考虑的JVM参数
类型 参数 描述 堆内存大小 -Xms
启动JVM时堆内存的大小 -Xmx
堆内存最大限制 新生代空间大小 -XX:NewRatio
新生代和老年代的内存比 -XX:NewSize
新生代内存大小 -XX:SurvivorRatio
Eden区和Survivor区的内存比
,
15、JVM进程有哪些线程启动?
首先要明白一点:JVM本身是一个多线程的程序,和我们编写的java应用程序一样,当JVM启动执行时就是在操作系统中启动了一个JVM进程。我们编写的java单线程或多线程应用进程都是在JVM这个程序中作为一个或多个线程运行。
每当使用java命令执行一个带main方法的类时,就会启动JVM(应用程序),实际上就是在操作系统中启动一个JVM进程,JVM启动时,必然会创建以下5个线程:
1-main 主线程,执行我们指定的启动类的main方法
2-Reference Handler 处理引用的线程 , :它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
3-Finalizer 调用对象的finalize方法的线程,就是垃圾回收的线程
4-Signal Dispatcher 分发处理发送给JVM信号的线程
5-Attach Listener 负责接收外部的命令的线程
16、Java8的元空间
方法区是所有线程共享。主要用于存储类的信息,常量池,方法数据,方法代码等
方法区是JVM规范,永久带(PermGen Space)是HotSpace对着中规范的实现。在JDK1.8中,HotSpot已经没有永久带,取而代之的是MetaSpace(元空间)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过源空间与永久代之间的最大区别在于:源空间并不在虚拟机中,而是使用本地内存**
为什么要进行源空间代替持久代呢
- 字符串存在永久代中,容易出现性能问题和内存溢出
- 类级方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困哪,太小容易垂涎永久代溢出,太大则容易导致老年代溢出
- 永久代会为GC带来不必要的复杂度,并且回收效率偏低
m