java虚拟机学习笔记进阶篇

上一节是把大概的流程给过了一遍,但是还有很多地方没有说到,后续的慢慢会涉及到,敬请期待!

这次我们说说垃圾收集器,又名gc,顾名思义,就是收集垃圾的容器,那什么是垃圾呢?在我们这里指的就是堆中那些没人要的对象。

1.垃圾收集器的由来

为什么要有垃圾收集器啊?不知道有没有想过这个问题,你说我运行一个程序要什么垃圾收集器啊?

随意看一下下面两行代码:

User user = new User("root","123456")
user = new User("lisi","123123")

简单画一下内存图,可以看到user这个局部变量本来是指向root这个对象,现在改为指向lisi这个对象,那么此时这个root对象没有人用,假如类似root这样的对象非常多的话,那么jvm性能就会越来越低,直至最后创建个对象可能都要十几秒,而且堆内存总有一天会装满就会报内存溢出异常;

所以我们就要想办法把类似root这种对象给清理掉,这样才能保证jvm高效的运行;

假如虚拟机没有提供gc你觉得会怎么样?其实也行,只不过你每次需要你用代码手动释放不需要的对象,关于这点有好处有坏处,好处就是有利于我们对堆内存的控制,坏处就是我们在一些比较复杂的程序之中由于手动释放内存难免会出错,但是这中错误还不怎么明显,可能要你去慢慢调试好久才能看到!

所以java就把这种工作自己处理了,让一个gc线程一直在后台运行,随时准备清理不需要用的对象,虽然相当程度上会对jvm性能造成一些影响,但是由于gc太好用了,我们不用再人为的去关心垃圾对象的释放,简化了我们编写程序的难度,所以这种影响程度完全可以接受!

这里顺便一提两个基本概念,内存泄漏和内存溢出:

内存溢出(Memory Overflow)比较好理解,就是我们保存对象需要的空间太大了,但是申请内存比较小,于是装不下,于是就会报内存溢出异常,比如说你申请了一个integer,但给它存了long才能存下的数,那就是内存溢出;专业点的说法就是:你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

内存泄漏(Memory Leak)指的就是我们new出来的对象保存在堆中但是没有释放,于是堆中内存会越来越少,会导致系统运行速度减慢,严重情况会使程序卡死;专业点的说法就是:你用malloc或new申请了一块内存,但是没有通过free或delete将内存释放,导致这块内存一直处于占用状态。

对于我们jvm来说,通常情况下我们不用担心内存泄漏,因为有一个强大的gc在我们程序的背后默默地为我们清理,但是也会有特殊情况,比如当被分配的对象可达但已无用(未对作废数据内存单元的赋值null)即会引起,至于这个可达是什么意思,后面会慢慢说到;

相对而言内存溢出我们比较常见,还有gc只会对堆内存进行回收,所以静态变量是不会回收的;

再顺便提一下另外两个小概念,非守护线程(也叫用户线程)和守护线程,看下面这个丑陋的程序运行会有几个线程啊?

public class User{
 public static void main(String[] args){
  System.out.println("我是java小新人");
 }
}

两个线程,一个是执行main方法的线程,后台还有gc执行gc的线程,在这里,用户线程就是执行main方法的那个线程,执行gc的线程就是守护线程,默默地守护者jvm,假如jvm是雅典娜,那么守护线程就是黄金圣斗士;

当用户线程停止之后整个程序直接停止,守护线程也会终止;但是黄金圣斗士挂了雅典娜还是可以好好活着的继续愉快的玩耍的;

2.堆内存结构

哎,内存中的结构如果真的要通过源代码去看,简直让人崩溃,除了专业搞这方面的不然真的很难懂,本来我想自己画一下草图了,发现太丑陋了,于是去顺手借了一张图:

途中可以很清楚的看到,整块堆内存分为年轻人聚集的地方和老年人聚集的地方,年轻人比较少趋势占用1/3空间(新生代),老年人比较多就占用2/3的空间(老年代),然而啊,年轻人又要分分类,分别是Eden区占新生代8/10,From Survivor区占新生代1/10,To Survivor区占新生代1/10,emmm。。。我特意查了一下百度翻译,Eden---->乐园,Survivor----->幸存者;哦~~~我感觉我仿佛明白了命名人的意图!

那么新生代和老年代到底是干什么的呢?我们创建的对象是放在哪里啊?

新生代:java对象申请内存和存放对象的地方,而且存放的对象都是那种死的比较快的对象,很多时候创建没多久就清理掉了,那些活的时间比较长的对象都被移动到了老年代。

老年代:存大对象比如长字符串、数组由于需要大量连续的内存空间,可以直接进入老年代;还有长期存活的对象也会进入老年代,具体是多长时间呢,其实默认就是经过15 对新生代的清理(Minor Gc)还能活着的对象。

而垃圾收集器对这两块内存有两种行为,一种是对新生代的清理,叫做Minor Gc,另外一种是对老年代的清理被叫做Major Gc。

顺便提一点:很多博客中都把Major GC和Full GC说成是一种,其实还是有区别的,因为很多java虚拟机的实现不一样,所以就有各种各样的名称,比如Minor Gc又叫做Young GC,Major GC也可以叫做Old GC,但是Full GC却有点不同,Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(也叫做方法区)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。当然在我们这里,为了好理解我们也就把Full GC当作Major GC就可以了。 

3.筛选清理对象

GC要工作的话,必须首先知道哪些对象要被清理,你想一下,在新生代和老年代有这么多对象,怎么筛选会又快又省事呢?可以有以下两种方法

1.引用计数算法,相当于给你创建的对象偷偷的添加一个计数器,每引用一次这个对象,计数器就加一,引用失效就减一,当这个计数器为0的时候,说明这个对象没有变量引用了,于是我们就可以说这个对象可以被清理了

2.根搜索算法(jvm用的就是这个),这个怎么理解呢?你可以想象现在有一个数组,这个数组里面包含了一些东西的引用,我们将这个数组叫做”GC Root“,然后我们根据这个数组中的引用去找到对应的对象,看看这个对象中又引用了哪些对象,一直往下找,这样就形成了很多线路,在这个线路上的对象就叫做”可达对象“,不在这个线路上的对象就是不可达对象,而不可达对象也就是我们要清理的对象;

其中可以作为GC Root的对象:

(1).类中的静态变量,当它持有一个指向一个对象的引用时,它就作为root

(2).活动着的线程,可以作为root

(3).一个Java方法的参数或者该方法中的局部变量,这两种对象可以作为root

(4).JNI方法中的局部变量或者参数,这两种对象可以作为root

(5).其它。

关于这个根搜索算法专业一点的说法就是:通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。

4.进行垃圾回收

前面已经筛选出了我们要清理的对象,但是怎么清理比较快呢?难道要一个一个对象慢慢删除嘛?就好像你要清理手机中的垃圾,你会一个应用一个应用去慢慢清理数据吗?当然不可能,这也太浪费时间了!我们当然是用手机管家或者360管家先把要清理的东西给收集起来放在一起,然后我们一清理就是全部,一个字,爽!

ok,在这里也一样,我们要想办法把所有的要清理的对象给放在一起清理,有什么办法呢?

1.标记-----清除算法:这种方法分为两步,先标记然后清除,其实就是需要回收的对象标记一下,然后就是把有标记的对象全部清理即可;这种方式比较适合对象比较少的内存,假如对象太多标记都要好半天,更别说清除了,而且用这种方法清除的内存空间会东一块西一块,下次再创建一个大的对象可能会出问题1

2.复制算法:按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已经使用的那块内存直接全部清理掉;这种方法最大的缺陷就是耗内存啊,只能用总内存的一半,而且如果对象很多复制都要花很多时间。

3.标记----整理算法:结合以上两种方法优缺点进行改良的一种方法,标记和第一种方法一样把要清理的对象做好标记,然后把所有标记的对象移动到本内存的一个小角落,最后集中力量对那个小角落进行消灭

4.分代收集算法:这是集中了上面三种方法的优点所实现的一种最好的方法,是目前大部分JVM所采用的方法,这种算法的核心思想是根据对象存活的时间不同将内存划分为不同的域,一般情况下将GC堆划分为新生代和老年代;新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,少数对象存活,因此可以使用复制算法;老年代的特点是每次垃圾回收时只有少量对象需要被回收,可以选用”标记--清除方法“”或者标记--整理算法“

所以目前大部分JVM的GC都是使用分代收集算法。

5.执行GC的步骤

前面说了这么多无非是介绍堆的内部结构,然后怎么找到要被清理的对象,然后为了提高效率怎么清理最快!

现在我们就大概说说GC的清理步骤(详细版):

1.我们创建对象的时候会进行一个判断,极少数很大的对象直接放进老年代中,除此之外所有新创建的对象都放进新生代的Eden区中;

2.此时新生代中只有Eden区中有对象,两个Survivor区中是空的;当我们创建了很多对象,使得Eden区快满的时候第一次GC发生(就是执行了一次Minior GC),Eden区和”From“区(此时“From”区是空的)存活的对象将会被移动到Surviver区的“To”区,并且为每个对象设置一个计数器记录年龄,初始值为1;每进行一次GC,会给那些存活的对象设置一个年龄+1 的操作,默认是当年龄达到15岁,下次GC就会直接把这种”老油条“丢到老年代中。

3.Minior GC之后,会进行一个比较厉害的操作,就是将”To“区和”From“换个名字,没错,就是换个名字,然后进行下一次Minior GC。

4.由于又创建了很多对象使得Eden区要满了,于是又一次Minior GC,Eden区还存活的对象会直接移动到Surviver区的“To”区,此时”From“区(这里就是交换名字之前的”To“区)中的对象有两个地方可以去,要么年龄满15岁了去老年代,要么就移动到”To“区

5.此时我们看一下,只有”To“区的对象是活着的,Eden区都是垃圾对象可以直接全部清理,”From“区是空的;不管怎样,在进行下一次Minior GC之前保证名为”To“的Survivor区域是空的就ok了

6.当老年代中快要装满之后,就会进行一次Major GC,这个清理事件很慢,至少比Minior GC慢十几倍,甚至更多,所以我们尽量要少执行Major GC

注意:如果在移动过程中”To“ 区被填满了,剩余的对象会被直接移动到老年代中。还有在每次Minior GC之前会先进性判断,只要老年代里面的连续空间大于新生代对象总大小或者历次晋升的平均大小进行Minor GC,否则进行Major GC。

简化版:

(1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;

(2)清空 Eden 和 From Survivor ;

(3)颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。

(4)老年代的Major GC执行时间很长,尽量少执行

只有在Eden空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。

6.知识点补充

通过查看了很多大佬的博客看到的很多有关的东西还是挺有趣的,于是简单做个小笔记:

6.1.新创建的对象是在堆中的新生代的Eden区,由于堆中内存是所有线程共享,所以在堆中分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上Eden区或者老年代进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。

6.2.很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

6.3对象调用.finalize方法被调用后,对象一定会被回收吗?

在经过可达性分析后,到GC Roots不可达的对象可以被回收(但并不是一定会被回收,至少要经过两次标记),此时对象被第一次标记,并进行一次判断,如果该对象没有调用过或者没有重写finalize()方法,那么在第二次标记后可以被回收了;否则,该对象会进入一个FQueue中,稍后由JVM建立的一个Finalizer线程中去执行回收,此时若对象中finalize中“自救”,即和引用链上的任意一个对象建立引用关系,到GC Roots又可达了,在第二次标记时它会被移除“即将回收”的集合;如果finalize中没有逃脱,那就面临被回收。因此finalize方法被调用后,对象不一定会被回收。

6.4.如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象直接进入老年代。不需要等到15岁。

总结

这篇说的就是java虚拟机怎么去收集对内存的垃圾,首先是要通过可达性分析判断哪些对象是可达的,哪些是不可达的,那些不可达的对象就是我们要处理的对象!这些不可达对象可能在新生代和老年代都有,在新生代用复制算法去处理垃圾,老年代用标记整理算法处理垃圾,这种处理方式也可以叫做分代收集算法!而且还简单说了一下Minor GC和Major GC的触发方式!

基本的东西就这么多,假如要深入的话可以深入很多,比如我们可以控制新生代的大小,还有很多种垃圾处理器的实现产品等等,都是可以去慢慢了解的。

(0)

相关推荐

  • java虚拟机学习笔记基础篇

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...) 所谓jvm,又名java虚拟机.我们平常写java程序的时候几乎是感觉不到jvm的存在的,我们只需要根据java规范去编写类,然后就可以运行程序了,当然只有我们程序出现bu

  • 详解Java虚拟机30个常用知识点之1——类文件结构

    1. Java文件 ClassFileTest.java package com.zxs.ssh.template.service; public class ClassFileTest { int m = 1; public int inc(){ return m+1; } } 2. Class文件ClassFileTest.class javac  ClassFileTest.java  编译.java文件得到.class文件 JDK版本  1.8.0_201 .class文件可以用WinH

  • Java虚拟机处理异常的最佳方式

    前言 欢迎来到Under The Hood专栏.本专栏旨在让Java开发人员一瞥在运行Java程序底层的神秘机制.本月的文章继续讨论Java虚拟机的字节码指令集,方法是检查Java虚拟机处理异常抛出和捕获的方式,包括相关的字节码.本文不讨论finally条款 - 这是下个月的主题.后续文章将讨论字节码系列的其他成员. 下面话不多说了,来一起看看详细的介绍吧 Exceptions Exceptions允许您顺利处理程序运行时发生的意外情况.要演示Java虚拟机处理异常的方式,请考虑一个名为NitP

  • java命令调用虚拟机方法总结

    java命令调用虚拟机 java的虚拟机调用,按住Win+r命名,如图所示: 继续点击确定按钮,如图所示: 可以看到后台命令,如图所示: 调用虚拟机编译Test.java代码:如图所示: Test.java可以看到在E盘JavaTest文件夹下,,如图所示: 回到命令后台,输入:E: 按回车键,然后在输入:cd JavaTest,按回车键, 然后输入javac Test.java,按回车键,这个是调用虚拟机编程的java代码, 最后输入:java Test,按回车键,可以看到后台输出:Hello

  • 带着新人看java虚拟机01(推荐)

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...),电子档pdf文件链接:https://pan.baidu.com/s/1bxs4i0gnVpz7Lkjl2fxS9g 提取码:n5ou ,有兴趣的小伙伴可以自己下载自己好好

  • 作为程序员必须掌握的Java虚拟机中的22个重难点(推荐0

    Java虚拟机一直是比较重要的知识点,是Java高级开发必会的.本文为你总结了关于JVM的22个重点.难点,图文并茂的向你展示和JVM有关的重点知识.全文共7000字左右. 概念 虚拟机:指以软件的方式模拟具有完整硬件系统功能.运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现.常用的虚拟机有VMWare,Visual Box,Java Virtual Machine(Java虚拟机,简称JVM). Java虚拟机阵营:Sun HotSpot VM.BEA JRockit VM.IB

  • java虚拟机学习笔记进阶篇

    上一节是把大概的流程给过了一遍,但是还有很多地方没有说到,后续的慢慢会涉及到,敬请期待! 这次我们说说垃圾收集器,又名gc,顾名思义,就是收集垃圾的容器,那什么是垃圾呢?在我们这里指的就是堆中那些没人要的对象. 1.垃圾收集器的由来 为什么要有垃圾收集器啊?不知道有没有想过这个问题,你说我运行一个程序要什么垃圾收集器啊? 随意看一下下面两行代码: User user = new User("root","123456") user = new User("

  • Vue学习笔记进阶篇之函数化组件解析

    这两天学习了Vue.js 感觉函数化组件这个地方知识点挺多的,而且很重要,所以,今天添加一点小笔记 介绍 之前创建的锚点标题组件是比较简单,没有管理或者监听任何传递给他的状态,也没有生命周期方法.它只是一个接收参数的函数. 在这个例子中,我们标记组件为 functional, 这意味它是无状态(没有 data),无实例(没有 this 上下文). 一个 函数化组件 就像这样: Vue.component('my-component', { functional: true, // 为了弥补缺少的

  • Vue学习笔记进阶篇之vue-router安装及使用方法

    介绍 vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用.vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来.传统的页面应用,是用一些超链接来实现页面切换和跳转的.在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换. 本文是基于上一篇文章(Vue学习笔记进阶篇--vue-cli安装及介绍)vue-cli脚手架工具的. 安装 在终端通过cd命令进入到上一篇文章中创建的my-demo1项目目录里

  • Vue学习笔记进阶篇之过渡状态详解

    这两天学习了Vue.js 感觉渡系统这个地方知识点挺多的,而且很重要,所以,今天添加一点小笔记. Vue 的过渡系统提供了非常多简单的方法设置进入.离开和列表的动效.那么对于数据元素本身的动效呢,比如: 数字和运算 颜色的显示 SVG 节点的位置 元素的大小和其他的属性 所有的原始数字都被事先存储起来,可以直接转换到数字.做到这一步,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态. 状态动画和watcher 通过 watcher 我们能监听到任何数值属性的数值更

  • 详解Vue学习笔记进阶篇之列表过渡及其他

    本文将介绍Vue中的列表过渡,动态过渡, 以及可复用过渡是实现. 列表过渡 目前为止,关于过渡我们已经讲到: 单个节点 同一时间渲染多个节点中的一个 那么怎么同时渲染整个列表,比如使用 v-for ?在这种场景中,使用 <transition-group>组件.在我们深入例子之前,先了解关于这个组件的几个特点: 不同于 <transition>, 它会以一个真实元素呈现:默认为一个<span>.你也可以通过 tag 特性更换为其他元素. 内部元素 总是需要 提供唯一的

  • Vue学习笔记进阶篇之vue-cli安装及介绍

    介绍 Vue-cli是Vue的脚手架工具 主要作用:目录结构.本地调试.代码部署.热加载.单元测试 地址:https://github.com/vuejs/vue-cli 安装 全局安装vue-cli npm install -g vue-cli 当然了,要想使用npm工具,就必须安装Node.js,node.js的安装方法这里就不做介绍了. 安装完成后,在终端输入以下命令, 可以查看vue的版本:vue -V 输入以下命令,可以查看官方提供的模板: vue list 我们可以看到,vue官方提

  • Vue学习笔记进阶篇之多元素及多组件过渡

    本文介绍了vue 多元素及多组件过渡,这个地方知识点挺多的,而且很重要,所以,今天添加一点小笔记. 多元素的过渡 对于原生标签可以使用 v-if/v-else.但是有一点需要注意: 当有相同标签名的元素切换时,需要通过 key 特性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容.即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践. 示例: <transition> <button v

  • Vue学习笔记进阶篇之单元素过度

    概述 Vue 在插入.更新或者移除 DOM 时,提供多种不同方式的应用过渡效果. 包括以下工具: 在 CSS 过渡和动画中自动应用 class 可以配合使用第三方 CSS 动画库,如 Animate.css 在过渡钩子函数中使用 JavaScript 直接操作 DOM 可以配合使用第三方 JavaScript 动画库,如 Velocity.js 单元素/组件的过度 Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加 entering/leaving 过渡 条

  • Java 注解学习笔记

    注解说明 Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据.为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些数据.Java语言中的类.方法.变量.参数和包等都可以被标注.和Javadoc不同,Java标注可以通过反射获取注解内容.在编译器生成类文件时,注解可以被嵌入到字节码中.Java虚拟机可以保留注解内容,在运行时可以获取到注解内容. 内置注解 Java定义了一套注解,共有7个,3个在java.lang中,剩下4个

随机推荐