jvm虚拟机及创建对象流程

JVM 

        JVM是Java Virtual Machine(Java虚拟机)的缩写, 经常聊到 java 都会有几个名词:jdk,jre,jvm。 简单说一说他们的区别

        jre  java运行环境,java程序需要运行,就必须要jre

        jdk 程序编译调试的工具包,JDK的工具也是Java程序,也需要JRE才能运行

        jvm java 虚拟机,也是jre 下面的一部分,一个虚拟的计算机,有完善的硬件架构。Java跨平台的特性就是通过 jvm 来实现

下面是jvm运行时的结构:

jvm虚拟机及创建对象流程

 挨个的看下

        类装载子系统:c++实现,java 所有代码底层都是通过c或者c++实现

        字节码执行引擎: 同上,也是c++实现,我们主要关注部分应该是在运行时数据区

        运行时方法区:

                堆:一般存放 new 的对象(或者说各个对象的值),在栈或者方法区中存放对象的地址,指向new的对象

                        如 String str = new String()    String str 声明存放栈中,但是没有具体的值,只有一个内存地址,指向堆中的 new String() 

                        堆内存溢出:不停的new对象,把老年代放满

               :每个线程都有自己的栈内存空间,我们称为栈帧,调用一个方法时,划分一块栈帧

                        局部变量表:字面意思,比如  a = 1,b=2这样的变量是存放在局部变量表中

                        操作数栈

                                  如a = 1执行流程:先将常量 1 放入操作数栈   第二部将a放入局部变量表第三步将1取出操作数栈,放入局部变量表

                                  组成  a = 1 各种的操作数

                        动态链接:  对应方法内存地址,方便调用来查找

                        方法出口:  方法执行完,要回到的位置

                本地方法栈:native 修饰的方法,底层是c或者c++ 就放这些东西

                程序计数器:   每个方法独有的,存方法的执行流程,字节码执行引擎在执行的时候会修改计数器(保证顺序执行)

                方法区(元空间):常量+静态变量+类信息   用的是直接内存(默认21M,会根据full gc自动调整可大可小,理论无限大,生产

                                                设置一般是256M或者512M)

        每个线程都有自己独立的栈空间,本地栈空间寄程序计数器,堆和方法区是共享的

记录两个关于元空间设置的jvm参数:

        -XX: MaxMetaspaceSize   元空间最大值,默认-1 理论无限大(受硬件大小影响)

        -XX: MetaspaceSize   元空间触发 full gc 初始化值,默认是21M 达到了该值元空间会

                触发gc回收,如果回收很多空间(无用的多,都回收了),那么jvm会适当把元空间调小, 如果回收很少空间(基本都需要存在

                无法回收),jvm会把元空间调大

        一般手动设置会把两个设置成一样的值,而且初始化一般给大一些,比如250M或者更大一些

        当然这个要根据实际情况决定(程序,硬件等等问题),后续我们再说

                

创建对象流程

        当我们 new 一个对象的时候,会经历一下流程:

        类加载检查》分配内存》初始化》设置对象头》执行init方法

        类加载检查  检查类是否被加载过,否  执行相应的类加载过程,并保存   是  直接往下

        分配内存  

                类加载通过后,需要给新生对象划分一块内存(类加载完成会确定大小),内存划分有

                两种方式:

                1. 指针碰撞(默认)

                        如果java堆内存是规整的,没有使用的在一边,使用了的内存在另一边。然后中间就会有个指针隔开两块内存(分界)

                        那么要划分内存,只需要将指针从使用了的方向,向没有使用的内存方向位移该对象的大小就可以了

                2. 空闲列表

                        如果java堆内存不规则,使用了的和未使用的相互交错,那么指针碰撞明显不合适那么虚拟机就需要维护一个记录表

                        记录那些使用过,在未使用的内存找一块大小合适的位置存放new的对象,并更新表记录为使用

                分配内存还有可能会出现问题,比如在高并发情况下,可能出现正在给对象A分配内存

                指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

                解决:

                        1. CAS  (compare and swap)

                                比较与交换,加上失败重试(乐观锁机制)的方式来保证分配内存

                        2. 本地线程分配缓冲

                                预先在内存中划分一块内存(TLAB),不与其他线程发生冲突,各创建各自的(1.8 默认方式)

                                -XX: TLABSize 设置大小

                                流程大概是 :

                                        线程开始创建TLAB > 分配内存,能放TLAB直接放,放不下edan区 其实内部还有些机制,本章不详解TLAB

                             详细关于TLAB参考 (参考了下,其他有些文章说法有问题,这章比较全面):一篇文章搞定 TLAB 原理_zhxdick的技术博客_51CTO博客_tlab是什么tlab是什么,TLAB 原理,一篇文章搞定 TLAB 原理,全系列目录:通过JFR与日志深入探索JVM-总览篇什么是TLAB?TLAB(ThreadLocalAllocationBuffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。既然是一个内存分配区域,我们就先要搞清楚Java内存大概是如何分配的。我们一般认为Java中new的对象都是在堆上分配,这个说法不够准确,应该是大部分对象在堆上的TLAB分配,还有一部分在栈上分配或者是堆上直接分配,jvm虚拟机及创建对象流程https://blog.51cto.com/u_11418075/2610347        初始化  赋初值

        设置对象头  

                jvm虚拟机及创建对象流程

                 存放内容如上图

                         mark word , 指针,数组长度

                单独提一下指针压缩

                        指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。但是64位的在存放时会将指针压缩为32位存放(默认

                        可关闭压缩),取出到cpu寄存器时再解压成对应位数,称为指针压缩

                为什么要开启指针压缩?           jvm虚拟机及创建对象流程

        执行 init 方法    属性赋值,调用构造方法

对象内存分配

        直接上图(结合下面的步骤描述来看):jvm虚拟机及创建对象流程

1. 可以看到,new对象时第一步居然是往栈上分配,也就是说new的内容是可以放栈中的,前面提到new的对象大部分都是存放在

堆里面没有使用的对象需要通过gc进行回收,当大量需要回收的对象出现,就会给gc带来压力,影响性能。为了减少临时对象对堆内

存中分配的数量,java通过逃逸分析来确定对象会不会被外部访问,不逃逸的方法直接栈内分配,方法调用完,出栈时直接就销毁了

不需要gc去处理

想要栈上分配对象需要依赖(1.7后默认开启) :逃逸分析  标量替换

        逃逸分析: 

                public void test(){

                        User user = new User();

                        //调用test方法,user对象在方法外部引用不到,逃逸不出,可分配到栈中

                }

                public User test(){

                        User user = new User();

                        return user;//外部能访问到User对象,那么就逃逸了,不能分配到栈

                }

        标量替换: 

                栈空间并不算大,分配对象时可能没有连续的空间来存放,将对象拆开来存放。

        

        如果都不满足,那么对象就会放到堆中去,那么我们看下堆里面是如何存放数据:

先说下堆中的一个划分:

        jvm虚拟机及创建对象流程

         主要三大块: 新生代,老年代,永久代(永久代1.8没有这个概念了,元空间代替了。元空间的本质和永久代类似,都是对JVM

                规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存)

                新生代分为  eden(伊甸园区) ,s1  s2(survivor区,幸存者区)    默认比例:8:1:1

2. 判断是否是大对象(需要大量连续空间),大对象直接放入老年代

        -XX:PretenureSizeThreshold 设置大对象,单位是字节

                只在Serial和ParNew两个收集器下有效(后面垃圾回收器再说)。

        这样的好处是避免大对象在新生代分配内存操作降低效率

3.  大部分情况(前面两种都不满足),也不放入TLAB, 新生对象直接放eden区 ,当eden区放满了,就会触发Minor GC (Young GC),基本eden 百分之八九十内容都会被回收,没有回收的就放入survivor区 ,对象如果每次minor gc都还存在,那么就会在 s1 s2中来回存放,每次年龄 +1 当年龄超过15(默认,不同垃圾回收器略微有些差别,但都不超过15),还没有回收,放入老年代  当老年代到达回收阈值,触发Major GC(Full GC)

        minor gc: 指发生新生代的的垃圾收集动作,MinorGC非常频繁,回收速度一般也比较快。

        full gc: 一般会回收老年代,年轻代,方法区的垃圾,MajorGC的速度一般会比MinorGC的

                    慢10倍以上。full gc完了还是放不下,就OOM错误

        ps: 优化垃圾回收其实就是尽量减少full gc

这里面有几个机制需要重点注意下:

        1. 大对象直接放入老年代(上面第二点)

        2. 长期存活对象放入老年代 (第三点中的年龄)

        3.动态年龄判断

               minor gc时触发,对象年龄 >=1 ,大小超过了Survivor区域的50%,放入老年代

                为了让长期存活对象,尽早进入老年代

        4. 老年代分配担保机制

              jvm虚拟机及创建对象流程

         

对象内存回收

         通俗的说就是gc的时候 虚拟机要怎么判断哪些内容需要回收,哪些还要存活

1. 引用计数法,这种方式的特点是实现简单,而且效率较高

          在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当counter为0时,表明该对象已经被废弃,不处于存活状态,就可以回收。这种方式一方面无法区分软、虛、弱、强 引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不能GC。

2. 可达性分析(现在都是用这个)

        通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。如果对象在进行可行性分析后发现没有与GC Roots相连的引用链,也不会理解死亡。它会暂时被标记上并且进行一次筛选,筛选的条件是是否与必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。

        可达性分析算法是通过枚举根节点来实现的,最重要的问题是GC停顿。为了确保一致性(即所有对象之间的关系是确定下来的)而导致GC进行时必须进行停顿。在HotSpot的中,使用OopMap的数据结构存储特定位置上的调试信息,存储栈上那个位置原来是什么东西,这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 这样,GC在扫描时就可以得知这些信息了。这样做的目的是使HotSpot能够快速准确的完成GC Roots枚举,以期望减少GC停顿所带来的影响。HotSpot没有在所有的指令生成OopMap,所以只是在“特定位置”记录这些信息,这些位置就是安全点。程序执行时并非在所有的位置上都能停顿下来GC,只有在到达安全点时才能暂停。安全点选取基本上是以“是否让程序长时间执行的特征”选定。此外,HotSpot虚拟机在安全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。

补充:

        finalize()方法最终判定对象是否存活,即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。标记的前提是对象在进行可达性分析后发现没有与GCRoots相连接的引用链。

        1.第一次标记并进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。

        2.第二次标记如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

        注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

方法区回收

        主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

        1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

        2. 加载该类的ClassLoader已经被回收。

        3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

吐槽:内容有点多,写得想吐了都,拆成两部分吧,jvm的内容也还没讲到那么多。后面再说关于垃圾回收器和jvm调优问题吧

一个想过得更好的码农---邋遢道人      

上一篇:JVM——GC


下一篇:2020年Java篇:蚂蚁金服,这10个经典又容易被人疏忽的JVM面试题