说一说JavaGC的来龙去脉

说一说JavaGC的来龙去脉

情景引入:今天面试奇安信.面试官问我如下问题:

  • 在一个web项目中将访问登录时的ip和变量用hashMap绑定在一起,这个变量是用来标记该ip登录失败的次数的,当失败三次之后需要验证码登录,问题是如果同一个ip多次访问是否会出现线程安全问题?
  • 有一个对象A引用了对象B,B里面有静态变量,此时对象A被回收,那么对象B呢?对象B里面的静态变量呢?

当时这两个问题回答的不好,所以自己又深入理解了下,发现其实这两个问题是有联系的,当你看了这篇文章之后,这两个问题也就迎刃而解了.

一.首先说一下JVM的内存模型:

在这里插入图片描述
1.JVM虚拟机数据区.

线程私有

  • 程序计数器.
    就是一个指针,指在当前线程正在执行的字节码的行号.(debug时使用)

  • java虚拟机栈.
    一个栈结构,为java方法调用时使用,每一个线程对应一个java虚拟机栈,
    其声明周期与线程共进退,每个java方法在调用时都会创建一个栈祯,并入栈,
    一旦调用完成,则出栈.所有的栈祯都出栈之后,线程也就完成了使命.

  • 本地方法栈.
    一个栈结构,区别是为虚拟机使用本地(native)方法所使用.

线程共享

  • 堆是JVM内存占用最大,管理最复杂的一个区域.其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配.在堆中又分为新生代和永久代

    声明:1.7以后,字符串常量池从永久带中剥离出来,存放在堆中.堆有自己的内存分块划分,按照GC分带收集角度的划分.

堆又分为以下几个部分:

扫描二维码关注公众号,回复: 8766656 查看本文章
  • 新生代和老年代,新生代又分为Eden,from0,from1区.

在这里插入图片描述

2.本地内存.

线程共享

  • 元数据区.(方法区)
  • 直接内存

二.常量,静态变量,对象在JVM中具体的存储位置.

栈中:

  • 一个栈帧可以代表一个方法,每一个栈帧存储了局部变量,被调用时传进来的参数,它的返回值,

堆中:

  • 存放了对象本身.字符串常量池(1.7以后放在了堆中).
    比如String str=“123”
    “123”,存放在字符串常量池中的某一个位置. str存放在栈中,

方法区:

  • 存放整个类的信息(class文件是存入方法区的),类变量(成员变量),静态变量.

  • 1.因为方法区可以被所有的线程所访问,所以方法区中的变量必须被设置成线程安全的.

  • 2.方法区也会发生GC,就是某个类不再被引用.会被整体GC掉(只有自定义类装载器装载的类型才会被GC).

三.在介绍了基本知识后,开始正式说GC.

一.什么时候会GC?

  • 在堆中,GC主要针对于新生代和老年代,在新生代中,所有新的对象刚开始都会放在eden中,当eden区不足以存放新的对象的时候就会发生GC,称为Minor(小的)GC.对于老年代,当升入老年代的对象所需要的空间大于其剩余空间时,也会发生GC,称为full GC.(full GC是很影响性能的)
  • 在方法区中,GC是很少发生的,而静态变量是存在方法区的,一般不会被回收,如果回收必须符合以下条件.
    1、所有实例被回收
    2、加载该类的ClassLoader 被回收
    3、Class 对象无法通过任何途径访问(包括反射)

二.当需要GC的时候怎么判断要回收哪些对象?

  • 垃圾检测通常通过建立根对象的集合,并且检查从这些根对象开始的可触及性来实现,如果正在运行的程序可以通过根对象访问到某个对象,那么这个对象就是可触及的,而无法被触及的对象被认为是垃圾.

    跟对象的集合通常通过包括下面几个部分:
    1.虚拟机栈中的局部变量的对象引用
    2.传入本地方法栈中,没有被释放的对象引用.
    2.方法区中的成员变量的对象引用.
    3.方法区中的静态变量的对象引用.
    具体实现就是从根节点开始有一个对象引用图,在追踪的过程中以某种方式打上标记,当追踪结束时,为被标记的对象就是无法触及的.

至此,其实最开始的问题已经有了答案:

1.在一个web项目中将访问登录时的ip和变量用hashMap绑定在一起,这个变量是用来标记该ip登录失败的次数的,当失败三次之后需要验证码登录,问题是如果同一个ip多次访问是否会出现线程安全问题?

  • 首先hashMap本身不是线程安全的,如果这个hashMap是一个类变量,放在方法区,那么它可以被所有的线程访问.而一个ip访问,进行连接就会创建一个新的线程去处理,也就达到了多线程的效果,所以是有线程安全问题,
    而局部变量不存在线程安全问题,因为放在栈中,而每个线程都有自己的独立的栈结构.

2.有一个对象A引用了对象B,B里面有静态变量,此时对象A被回收,那么对象B呢?对象B里面的静态变量呢?

  • B对象会被回收,如果没有其他引用的话,就是如果变成不可触及的话.
    里面的静态变量会存放在方法区中,一般不会被GC.只要类存在,静态变量就存在,被回收必须满足那几个苛刻的条件.

当然既然说到JAVAGC,就要说清楚来龙去脉啊,下面继续探讨.

三.现在知道了哪些对象需要被回收,在介绍具体的回收算法时,还需要说一下对象的晋升过程.

  • 1.所有的对象刚开始new出来都是会先放在新生代中的eden区(除了有些对象特别大,eden区放不下,会直接放在老年代).
    2.在eden区经过第一次GC之后,存活的对象会被放到某一个幸存者区(survivor),这里假设是survivor0.
    3.此后,每当eden区GC后,存活的对象都会放再同一个survivor区.在这里也就是survivor0区.
    4.当survivor0饱和之后,会执行复制算法,就是检测哪些是垃圾,然后把还有用的对象全部移到survivor1区,survivor0区的垃圾全部clear.
    5.以上步骤重复N次,在经历15次(默认值)GC之后还存活的对象,会从survivor区晋升到老年代.

    这里再放以下堆空间的存储图.
    在这里插入图片描述

四.现在开始正式介绍常用的垃圾回收算法.

  • 引用计数法:这是个比较古老而经典的垃圾收集算法,其核心就是在对象被其他所引用时计数器加1,而当引用失效时则减1,但是这种方式有非常严重的问题:无法处理循环引用的情况、还有就是每次进行加减操作比较浪费系统资源.

    标记清除法:就是分为标记和清除俩个阶段进行处理内存中的对象,当然这种方式也有非常大的弊端,就是空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内间

    标记压缩法:标记压缩法在标记清除法基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。(java中老年代使用的就是标记压缩法).

    分代算法:就是根据对象的特点把内存分成N块,而后根据每个内存的特点使用不同的算法。对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时都很短,而老年代回收频率较低,
    但是耗时会相对较长,所以应该尽量减少老年代的GC.

    分区算法:其主要就是将整个内存分为N多个小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收都少个小空间和那些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时长.

补充:GC停顿现象.

  • 垃圾回收器的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有 新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于更好低标记垃圾对象。因此在垃圾回收时,都会产生 应用程序的停顿.

五.垃圾收集器.

Java虚拟机垃圾回收器不仅仅只有一种,什么情况下该使用哪种,对性能又有什么样的影响,这都是我们需要了解的.

  • 1.串行垃圾回收器.

    • 串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行
      回收器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收

    2.并行垃圾回收器.

    • ParNew回收器是一个工作在新生代的垃圾收集器,他只是简单的将串行回收器多线程化,他的回收策略和算法和串行回收器一样.

    • 新生代 ParallelGC
      回收器,使用了复制算法的收集器,也是多线程独占形式的收集器,但ParallelGC回收器有个非常重要的特点,就是它非常关注系统的吞吐量

    • 老年代 ParallelOldGC 回收器 也是一种多线程的回收器和新生代的
      ParallelGC 回收器一样,也是一种关注吞吐量的回收器,他使用了标记压缩算法进行实现

    3.CMS垃圾回收器.

    • CMS全称为:Concurrent Mark Sweep 意为并发标记清除,他使用的是标记清除法,主要关注系统停顿时间.
    • CMS并不是独占的回收器,在CMS回收过程中,应用程序仍然在不停的工作,又会有新的垃圾不断产生,在使用CMS的过程中应该确保应用程序的内存足够可用。CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置,-XX:CMSInitiatingOccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收。如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingOccupancyFraction的设置要根据实际的情况.

    4.G1回收器.

    • G1回收器(Garbage-First)
      拥有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代、老年代的空间都连续,它使用了分区算法.

    • 它是兼顾新生代和老年代一起工作,之前的垃圾收集器他们或者在新生代工作,或者在老年代工作,因此这是一个很大的不同.

以上就是全部内容了,由于编者水平也有限,如果错误欢迎各位多多指正,谢谢.

Tmi
发布了13 篇原创文章 · 获赞 7 · 访问量 2195

猜你喜欢

转载自blog.csdn.net/wangliangluang/article/details/103093596