JVM的组成:
1.java代码执行
2.内存管理
3.线程资源同步与交互机制
2.1 JVM后台运行的系统线程:
1)虚拟机线程:
安全点
JVM进行垃圾回收是一个非常复杂的过程,如何进行垃圾标记、什么时候进行垃圾、如果进行垃圾回收等等都非常复杂,当前主流测JVM在垃圾回收时都会进行STW(stop the world),即使宣称非常快的CMS垃圾回收期早期也会STW标记垃圾状态。那么这里有个问题,什么时候进行标记对象是否可以被回收呢?
CPU在执行运算过程时需要把数据从内存中载入到寄存器,运算完后再从寄存器中载入到内存中,Java中对象地址也是这么个过程,设想如果一个Java线程分配一个对象,此时对象的地址还在寄存器中,这时候这个线程失去了CPU 时间片,而此时STW GC发现没有任何GC ROOTS与该对象关联起来,此时这个对象被认为是垃圾并被回收了,之后CPU重新获得时间片后发现此时对象已经不存在了这时候程序就GG了。
因此不是在任何时候都可以随便GC的,复杂的JVM早就考虑到这个问题,在JVM里面引入了一个叫安全点(Safe Point)的东西来避免这个问题。GC的目的是帮助我们回收不再使用的内存,在多线程环境下这种回收将会变得非常复杂,要安全地回收需要满足一下两个条件:
1.堆内存的变化是受控制的,最好所有的线程全部停止。
2.堆中的对象是已知的,不存在不再使用的对象很难找到或者找不到即堆中的对象状态都是可知的。
为了准确安全地回收内存,JVM是在Safe Point点时才进行回收,所谓Safe Point就是Java线程执行到某个位置这时候JVM能够安全、可控的回收对象,这样就不会导致上所说的回收正在使用的对象。
既然达到Safe Point就可以安全准确的GC,name如何到达Safe Point。
说到这里就要提到如何使线程中断,一般有两种方式:主动式和被动式。主动式JVM设置一个全局变量,线程去按照某种策略检查这个变量一旦发现是Safe Point就主动挂起,被动式就是发个信号,例如关机、Control+C,带来的问题就是不可控,发信号的时候不知道线程处于什么状态。这里HostSop虚拟机采用的是主动式使线程中断。
既然JVM使用的是主动性主动到达安全点,那么应该在什么地方设置全局变量呢?显然不能随意设置全局变量,进入安全点有个默认策略那就是:“避免程序长时间运行而不进入Safe Point”,程序要GC了必须要等线程进入安全点,如果线程长时间不进入安全点这样就比较糟糕了,因此安全点主要咋以下位置设置:
1. 循环的末尾
2. 方法返回前
3. 调用方法的call之后
4. 抛出异常的位置
安全区域
安全点完美的解决了如何进入GC问题,实际情况可能比这个更复杂,但是如果程序长时间不执行,比如线程调用的sleep方法,这时候程序无法响应JVM中断请求这时候线程无法到达安全点,显然JVM也不可能等待程序唤醒,这时候就需要安全区域了。
安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方GC都是安全的,安全区域可以看做是安全点的一个扩展。线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样GC时就不用管进入安全区域的线层了,线层要离开安全区域时就检查JVM是否完成了GC Roots枚举,如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。
2)周期性任务线程:负责中断事件
3)信号分发线程:JVM在OS之上,该线程获取发送给JVM的信号并调用JVM方法处理。
2.2 内存管理
1)线程私有区域:与本地线程生命周期对应
PC:字节码指令位置,无OOM
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
虚拟机实例生命周期:程序的启动、退出与关闭。
虚拟机栈【为执行java方法服务】:一个线程中,调用一个方法对应一个栈帧。栈帧的组成-本地变量表 -操作数栈 -对运行时常量池的引用;方法执行过程为栈帧入栈到出栈全程。
可能存在的异常有:1-栈深度大于JVM允许 2-JVM允许动态扩展,未申请到足够内存。
本地方法栈【为执行本地方法服务】:任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
本地方法:本地方法是由其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码。本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。
这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
HotSpot两栈合二为一
2)线程共享区:生命周期与虚拟机生命周期相同
堆(java堆):线程共享,垃圾收集重要区域-GC角度:新生代、老年代
新生代:Eden ServivorFrom-GC被扫描者 To-GC幸存者 频繁触发MinorGC
MinorGC复制算法:存活对象-是否老年?不够位置?-分配到老年代区与To区-清空前俩-To赋值给From(Eden是被清空的状态)
老年代:触发MajorGC的两种方式:上述and分配稳定对象时空间不够
MajorGC标记清除算法:先标记存活对象-再回收-合并或者标记内存碎片
方法区:永久代,类信息,常量,静态变量
maxpermsize:永久代内存初始大小-最大非堆内存的大小
-垃圾回收:常量池的回收与类型卸载
GC不会在主程序运行期对永久区域进行清理,容易随着Class增多引发OOM
java8:使用本地内存存储元数据代替永久代,静态变量与字符串池等放入java堆中,由于不使用虚拟机的内存,使用本地内存,不受maxpermsize的约束。
3)直接内存:
IO 是主存和外部设备 ( 硬盘、终端和网络等 ) 拷贝数据的过程。 IO 是操作系统的底层功能实现,底层通过 I/O 指令进行完成。
2.3 垃圾回收
1)哪些内存需要回收?
引用计数法(循环引用问题:myObject1与myObject2互为引用,我们知道如果采用引用计数法,myObject1和myObject2将不能被回收,因为他们的引用计数无法为零。)
作为一种回收算法,引用计数法记录着每一个对象被其它对象所持有的引用数。如果一个对象的引用计数为零,那么该对象就变成了所谓的不可达对象,亦即可以被回收的。当一个对象被回收后,被该对象所引用的其它对象的引用计数都应该相应减少。
可达性分析解决循环引用问题:
算法中定义了几个GC Root对象,这几个root对象在GC时不会被JVM回收掉,然后通过这些对象像树枝一样向外延伸,被引用到的对象说明还存活使用,就不会被GC,没有被这些root对象引用到的就会被GC掉。从而解决了循环引用问题。
Root对象主要包括:
①系统类加载器(bootstrap)加载的类。
②JVM方法区中静态属性引用的对象。
③JVM常量池中引用的对象。
④JVM虚拟机栈中引用的对象。
⑤JVM本地方法栈中引用的对象。
⑥活动着的线程。
不可达不等于可回收,两次标记后才面临回收。
几种不同的引用:
强引用:
强引用就是引用了通过new 的方式创建的对象。在root搜索算法的里面,说的引用都指的是强引用关系。不可能被GC,java内存泄漏的主要原因。
Object obj = new Object();
obj这个变量引用了一个object对象,只有obj这个引用被释放了,对象才会被GC。
比如:obj=null;
这样对象就会被释放。
软引用:
软引用的对象只有在内存不足的时候才会被回收掉,如果GC时,内存充足,那么软引用类型不会被回收。
软引用的作用是:软引用可用来实现内存敏感的高速缓存。如果内存充足,就可以一直把缓存放在内存中,加快数据访问速度。如果内存不足了,缓存就会被回收掉。
Object obj = new Object();
弱引用:
弱引用只会生存很短的时间,在第一次经历GC时,无论内存是否充足,都会被回收掉。
弱引用的作用就是:如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么就可以将对象修饰为为弱引用类型。弱引用还可以用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。还有一种情况:当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候就可以用弱引用。
Object obj = new Object();
WeakReference weakReference = new WeakReference(obj);
虚引用:
虚引用就是假的引用,如果一个对象与虚引用关联, 则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用必须配合引用队列使用。主要作用是跟踪对象被垃圾回收的状态。
虚引用的作用:因为虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时, 如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。所以程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pf = new PhantomReference(obj,queue);
2)什么时候回收?
分代的回收条件。
3)怎么回收?
标记清除算法:内存碎片化,大对象找不到可用空间。
复制算法(新生代):内存压缩,存活对象增多时效率降低。
标记整理算法(老年代):整合上两种:标记-存活对象移向一端-清除端边界外的对象
分代收集算法:
老年代:
分区收集算法:
堆划分小区间独立使用与回收。