简单介绍下JVM运行时数据区
Jvm在执行Java程序的过程中会把它管理的内存分为若干个不同的区域,这些部分有些是线程私有的,有些则是线程共享的。
线程私有的:程序计数器,虚拟机栈,本地方法栈
线程共享的:方法区,堆
简单介绍下JVM常见异常
StackOverFlowError:当线程请求栈的深度超过当前 Java虚拟机栈的最大深度时
OutOfMemoryError:
1)当线程请求栈时内存用完了,无法再动态扩展了
2)当堆内存或者永久代/元空间不够,无法再分配对象或存放数据,同时堆空间或用就代空间无法再拓展
3)当垃圾回收器占用JVM的资源98%,同时回收效率不到2%
程序计数器
记录当前线程正在执行的字节码的地址或行号,主要作用是为了保证多线程情况下JVM程序的正常运行
讲一讲方法区
方法区所有线程共享,主要用于存储类的信息、常量池、方法数据、方法代码等
方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
JVM中对象的创建过程
虚拟机遇到一跳new指定时,根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,那就必须先执行相应的类加载过程
在类加载的检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后就可以完全确定,
为对象分配空间的任务等同于将一块确定大小的内存从Java堆中划分出来,
真的就这么简单吗?显然不是,具体的实现是比较复杂,下面将描述完整的过程
1、检查加载
先执行相应的类加载过程,如果没有,则进行类加载
2、内存分配
1)假如Java堆中内醋是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器
那所分配内存就仅仅把那个指针向空闲那边挪动一段与对象大小相等的距离
这种分配方式被称为“指针碰撞”
2)如果Java堆中内存不是规整的,已使用的内存和空间的内存相互交错,那么就没有办法简单地进行指针碰撞了
虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块足够大的空间划分给对象实例,并更新列表上的记录
这种分配方式称为“空闲列表”
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
比如使用CMS这种基于Mark-Sweep算法的收集器时,Java堆中的内存并不是规整的,通常采用空闲列表
3、内存空间初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为默认值(如int默认值为0,boolean为false等)
这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就能直接使用,程序能访问到这些字段的数据类型所对应的初始值
4、设置
接下来、虚拟机要对对象进行必要的设置
假如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。
5、对象初始化
以上4步完成后,从虚拟机的视角看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,所有的字段都还为初始值,所以一般来说,执行new指令后会接着把对象按照开发者的意愿进行初始化,这个一个真正可用的对象才算完全产生出来。
对象的访问定位的两种方式
句柄和直接指针两种方式
句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据类型各自的具体地址信息
直接指针:Java堆对象的布局中就必须考虑到如何放置访问类型数据的相关信息,而reference中存储的就是对象的地址。
HotSpot中使用直接指针,使用直接指针访问的方式最大的好处就是速度快,它节省了一次指针定位的时间开销
JVM如何判断对象是否可回收
可达性分析算法
这个算法的基本思想就是通过一系列的成为“GC Roots”的对象作为起点,从这些借点开始向下搜索,节点所走过的路径称为引用链
当一个对象到GC Roots没有仁和应用链相连,则证明这个对象不可用的。
简单的介绍一下Java中的各种引用
1、强引用:
通常我们使用的大部分引用实际上都是强引用,这是使用普通的引用,如果一个对象具有强引用,垃圾回收期绝不会回收它
当内存空间不足时,Java虚拟机则会抛出OutOfMemoryError错误,使程序异常终止。
2、软引用:
如果一个对象只具有软引用,当内存空间足够时,垃圾回收器就不会回收它,但如果内存空间不足时,就会回收这些对象的内存
只要垃圾回收器没有回收它,该对象就可以被程序使用,软引用可以用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(referenceQueue)联合使用;
如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入与之关联的引用队列中。
3、弱引用
弱引用与软引用的区别在于只具有软引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它管辖的内存区域时,不管内存是否足够,都会将扫描到只具有弱引用的对象的内存回收
不过由于垃圾回收是一个优先级很低的线程,因此不一定会很快发现那些只有被弱引用的对象。
4、虚引用
虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,在任何时候都有可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
程序设计中除了强引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题产生。
垃圾收集有哪些算法,各自的特点?
1、标记-清除算法
标记-清除算法分为“标记”和“清除”阶段,首先标记出所有需要回收的对象,在标记完后统一回收所有被标记的对象。
它是最基础的收集算法啊, 效率也很高,但是会带来明显的问题:
1)效率问题
2)空间问题(标记清除后会产生大量不连续的碎片)
2、复制算法
为了解决效率问题,复制收集算法出现了。
它可以将内存分为大小相同的两块,每次使用其中的一块,当这一块内存使用完后,就将还存货的对象复制到另一块去,然后再把使用的空间一次清理掉。
这样就使每次的内存回收都是对内存区间的一半进行回收。
3、标记-整理算法
根据老年代的特点出的一种标记算法,标记过程仍然与“标记-清除”算法一致
但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
HotSpot为什么要分为新生代和老年代
将Java堆分为新生代和老年代,我们就可以根据各个年代的特点,选择合适的垃圾收集算法。
在新生代中,每次收集都会有大量对象死去,所以我们可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
在老年代中,对象的存活几率比较好,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
哪些情况需要对类进行初始化?
虚拟机规范严格规定了有些只有5种情况必须立即对类进行初始化:
1、使用new关键字实例化对象的时候、读取或设置一个类的静态字段时,已经调用一个类的静态方法时。
2、使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类没有被初始化,就会先初始化它的父亲。
4、当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类。
5、使用动态语言支持的时候
如果一个java.lang.invoke.MethodHanddle实例最后的解析结果REF_getStatic.REF_putstatic,REF_invokeStatic的方法句柄
并且这个方法句柄所对应的类没有进行初始化,而需要先触发其初始化。
双亲委派模型
双亲委派模型,要求除了顶层的启动类加载器外,其他的类加载器都应该有自己的父类加载器。
这里的父子关系通常是子类通过组合关系而不是继承关系来服用父类加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求
先把这个请求委派给父类加载器完成,只有父类加载器反馈自己无法完成加载请求时
子类加载器才回尝试自己去加载。