【JVM】垃圾回收 ——自问自答2

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的建立连接和关闭链接的过程。

**8月15日 再补充一个:

ThreadLocal使用后,在线程内没有进行 remove操作:

ThreadLocal内部维护了一个ThreadLocalMap,这个Map的Entry元素都是弱引用,虽然这个是防止OOM的一个手段,(堆空间不够用了,触发GC,一定可以回收弱引用),但是存在一个bug,

TreadLocal的强引用断开,Entry的key的弱引用在GC后,自动断开,TreadLocal对象被回收,Entry的Key就变成了NULL值,如果线程被复用,会出现这样一条引用链:

Thread  ref ---》Thread实例  ---》ThreadLocalMap threadlocals  ------》Entry------>value对象

而且,这个Entry对应的key值是NUll。好在,ThreadLocal的set、get、remove中调用了一个可以清除脏Entry的方法。

所以,无论如何,请记住要在ThreadLocal使用完毕后,调用remove。防止线程复用造成的Entry中的value内存泄漏。


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使用权。
用户态->内核态:由 中断 引发,硬件自动完成变态过程。
中断分为 内中断和外中断,一般程序执行遇到异常,或请求系统调用,执行访管指令都属于内中断。
当进入中断时,用户进程根据不同场景会进入就绪,阻塞,就绪/阻塞挂起,退出等状态。

操作系统关于线程实现方式有三类:内核级线程、用户级线程、组合方式

用户级线程:(图片来自王道,侵删)

早期,操作系统只支持进程,没用线程的概念,内核中只有进程的概念,进程作为资源分配的单位,
用户通过线程库来对一个进程进行拆分执行。最简单的线程库,可以理解为:

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: 强软弱虚来一套

强引用:普通创建对象的语法,都是强引用。
软引用:堆内内存不足时,软引用会被回收
弱引用:发生垃圾回收行为时,一定会被回收
虚引用:一个对象是否有虚引用存在,跟其生存时间没叼毛影响,无法通过一个虚引用获取一个对象实例。
设置虚引用的目的在于垃圾回收该对象时,得到一个系统通知。它其实可以理解为一个对象回收的跟踪。对象回收后,将虚引用加入引用队列,以通知程序该对象的回收情况

//-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("我被调用了呀!!!!");
}
}

                
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值