Q: System.gc() 的理解
System.gc()底层调用的是 Runtime.getRuntime.gc(),会现实出发FullGC。
但是,它的调用附带一个免责声明,无法保证对垃圾收集器的调用。
Q: 内存溢出和内存泄漏?
内存溢出: 简而言之,内存不够用了
可能因为堆内存在设置大小的时候 -Xms ,-Xmx 设置的比较小
前面也提到,虚拟机栈可以动态扩容时,当物理内存空间不足以给栈扩容,也会导致OutofMemory:stack
在jdk不同版本之中,方法区所产生的OOM是不同的。
jdk7及以前,是永久代 permgen space,而jdk8之后则是 Metaspace
一般情况下,在系统抛出OOM之前,GC会进被触发,尽可能清理出空间给新对象。
比如软引用就是在内存不足的情况下,会被触发回收。
内存泄漏(memory Leak)
当一个对象不会再被程序用到了,但是GC有不能回收他们,此时称之为内存泄漏。(一个不再被使用的对象,仍然有GC roots引用)
内存泄漏的举例:
1、单例模式。
首先明白一个问题,一个JVM对应的是一个进程(有一个与之对应的单例对象 Runtime),
平时说的微服务也好,分布式也好,不管怎样,请搞清楚。一个java进程,对应一个JVM,或者可以理解为,一个运行起来的服务,它就是一个进程。
如果在这个服务(进程)中,有一个单例对象,那么此对象的生命周期是随着JVM进程的消亡而消亡的。
如果此时单例对象内强引用了一个其他对象,而没有手动释放,那么这个被引用的对象的生命周期会和引用它的单例对象一样,一直随着JVM进程结束,才能结束。
如果这个被引用的对象后续不再被使用,但是它无法被GC销毁,这时候,就是个典型的内存泄漏场景。
我们可以用软引用来解决这种问题。
2、没用关闭某些资源
java的面向对象思想,把很多资源使用场景都有一个建立链接和关闭链接的过程,这个其实是一种网络分层模型中,提供可靠服务的前提。
比如数据库链接,套接字链接(socket),IO,他们都是要进行手动close的。前两个其实都是基于TCP的建立连接和关闭链接的过程。
Q: 谈一谈Stop the World
STW GC线程在工作的过程中,与用户线程是并发执行的,二者抢占CPU资源,GC执行,用户线程被暂停。
可达性分析算法中枚举根节点会导致Java执行线程停顿。
因为: 分析工作要保证一致性结果,一般是在当前进程快照中分析
如果分析过程中,一直有用户线程在执行,就会导致分析结果不准确。
(开发中尽量少使用System.gc 会产生STW)
Q: 垃圾回收的并行,并发。
垃圾回收事件的并行与串行是对立的,它描述的是垃圾收集线程的工作关系。
串行,指在进行垃圾收集时,只有一个GC线程在工作。
并行:指在进行垃圾收集时,有多个GC线程在同时工作。
注意上述两个场景中,用户线程都是暂停的,并行与串行机制只是在描述垃圾收集线程。
但是请注意一点:并行,虽说是要建立在多处理器条件下,才可能发生的,但是在操作系统相关的知识体系中,"并行"的概念并不是绝对的。
因为并行的垃圾回收行为,在单处理器环境下,是通过并发执行表现的。但是请注意发生CPU争抢的,只有GC线程而没有用户线程。
这个道理其实就像理解操作系统的共享特性。共享分为独占式共享,和同时共享。
独占式共享,就是串行的,一个作业完成对资源的使用后,再运行下一个作业去访问资源。
但是同时访问共享,并不真是"同时访问",本质上来说,也是通过并发的特性来实现"同时"。
垃圾收集的并发行为:
指的是垃圾收集线程,可以和用户线程 "同时"执行(这里的同时,和上面的解释是一样的,还是取决于CPU和工作线程的个数)
因此,垃圾收集上下文环境所说的并发和并行,和操作系统层面说的并发,并行是有区别的。
Q: 安全区和安全点
程序执行时并不是在任何地方都能停下来开始GC,只是在特定位置才能停顿下来,开始GC,这些位置称为 安全点
安全点的选择一般是以"是否具有让程序长时间执行的特征",比如选择一些执行时间较长的指令作为安全点,比如方法调用,循环跳转和异常跳转。
如何在GC发生时,检查所有线程都跑到了最近的安全点停顿下来呢?
1、抢占式中断:弃用
2、主动式中断:设置标志位,各个线程运行到Safepoint,主动轮训这个标志,若中断标志为真,将自己进行中断挂起。
这里补充一个操作系统的知识:
进程在执行的过程时,CPU会有内核态和用户态,当发生系统中断,CPU会从用户态转换为内核态,使操作系统夺回CPU控制权(也是唯一途径)
内核态->用户态:执行一条特权指令----修改PSW的标志位为用户态。操作系统让出CPU使用权。
用户态->内核态:由 中断 引发,硬件自动完成变态过程。
中断分为 内中断和外中断,一般程序执行遇到异常,或请求系统调用,执行访管指令都属于内中断。
当进入中断时,用户进程根据不同场景会进入就绪,阻塞,就绪/阻塞挂起,退出等状态。
操作系统关于线程实现方式有三类:内核级线程、用户级线程、组合方式
用户级线程:(图片来自王道,侵删)
早期,操作系统只支持进程,没用线程的概念,内核中只有进程的概念,进程作为资源分配的单位,
用户通过线程库来对一个进程进行拆分执行。最简单的线程库,可以理解为:
cpp
int main(){
int flag =0;
while(true){
if(flag==0){
do A;
}
if(flag==1){
do B;
}
if(flag==3){
do C;
}
}
}
用户线程是建立在用户空间的线程库上,系统内核不感知线程存在的实现,用户线程的创建、同步、销毁和调度完全在用户状态中完成。
但是这种设计的时间片划分仍然是以进程为单位的,进程A有一个线程,进程B有100个线程,假设两者的时间片长度相同,这种分配结果是不公平的。
而且,某一线程被阻塞,整个进程都会被阻塞,内核分配CPU资源也是按进程分配,也就是说进程B中的所有线程都是并发执行,每次只能执行1个线程。
内核级线程:
操作系统内核支持的线程,这种线程有内核来完成线程切换。每个线程都有自己的TCB
换句话说,操作系统内核已经支持线程的概念了,内核已经将线程作为最小的资源分配单位。
用户线程的实现是通过线程库来创建管理的,线程库分为有内核支持和无内核支持,区别在于在调用库内函数时,是否需要进行内核的系统调用。
java线程库就是常用的线程库之一。它以来与宿主系统的线程库,windows上是 windows API,Unix上是POSIX pthreads。
这些模型是建立在支持内核级线程的系统中的:
多线程模型
用户线程和内核线程链接方式不同,分为一对一,多对一,多对多。
一对一 :一个用户线程对应一个内核线程,用户线程被阻塞,并不会影响其他线程的运行。
并发能力较强,一个线程阻塞,并不会影响到其他线程,但是一一对应开销也很大。(见上图)
多对一 :一个内核线程对应多个用户线程,这些线程一般属于一个进程,线程的调度和管理都是在用户空间完成的(执行效率比较高)。
缺点很突出:任意时刻,只能有一个线程与内核线程进行映射,一个线程阻塞,整个进程会发生阻塞,多个线程不能同时在多个处理器上运行。
java每个线程都对应了一个本地线程,其实java的线程是在JVM虚拟机中通过一些C++写的方法调用,来调用本地线程库的API,对线java程状态进行管理,映射。
所有这个对应的本地线程,实际上是本地线程库中的用户级的线程,而非内核线程,至于本地线程库到底使用了那种多线程模型,要具体系统具体分析,这也是JVM实现跨平台的一个手段。
这里补充这些知识,是想说,用户线程在安全点的STW,其受到中断而挂起,其实是通过
JVM------本地线程库------内核级线程状态切换 这一套执行流程的。这个安全点造成的中断,并非操作系统里说的中断,OS里的中断粒度是针对进程来说的。
安全区:
如果某些线程在准备进入安全点之前,是阻塞态,该线程此时是无法响应JVM的中断请求的,运行到安全点,再中断挂起,
安全区是一段代码片段中,对象的引用关系没有发生变化,再这个区域中任何位置开始GC都是安全的。
从一个点 被扩大成了一个片段。
执行过程:
当线程运行到safe Region代码段时,首先标识已经进入了safe Region,若这段时间内发生GC,JVM会忽略标识为 safe Region状态的线程。
当线程即将离开safe Region时,会检查JVM是否已经完成GC,若完成了,则继续运行,否则线程必须等待到收到可以安全离开safe Region的信号为止。
Q: 强软弱虚来一套
强引用:普通创建对象的语法,都是强引用。
软引用:堆内内存不足时,软引用会被回收
弱引用:发生垃圾回收行为时,一定会被回收
虚引用:一个对象是否有虚引用存在,跟其生存时间没叼毛影响,无法通过一个虚引用获取一个对象实例。
设置虚引用的目的在于垃圾回收该对象时,得到一个系统通知。它其实可以理解为一个对象回收的跟踪。对象回收后,将虚引用加入引用队列,以通知程序该对象的回收情况
java
//-xms9m -xmx9M -XX:+PrintGCDetails
class ReferenceDemo{
//强引用,ReferenceDemo对象实例化后,objRf_inheap位于实例对象所在的堆空间中
Object objRf_inheap =new Object();
public void foo(){
//强引用,引用存在于局部变量表中的第二个位置,slot标号为1
Object objRf_inLV =new Object();
//软引用,当内存空间不足的时候,会在GC中对其进行回收
Object obj =new Object();
SoftReference obj_softRf =new SoftReference<Object>(obj);
obj=null;
//可以获取代到软引用包装的对象
obj_softRf.get();
//创建一个大对象,当前堆装不下了,目的是要触发GC,堆内内存不足时,软引用会被回收
byte[] buffers =new byte[7*1024*1024];
....
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("我被调用了呀!!!!");
}
}