Java知识点之JVM

1.Java内存区域:

Java虚拟机在运行程序时会把其自动管理的内存划分为方法区、堆、程序计数器、虚拟机栈、本地方法栈,其中方法区和堆属于线程共享的数据区域,而程序计数器、虚拟机栈、本地方法栈属于线程私有的数据区域

方法区 (Method Area):

方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器、编译后的代码等数据,根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。在方法区中存在一个运行时常量池的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放在运行时常量池中,以便后续使用

JVM堆 (Java Heap):

JVM堆属于线程共享的内存区域,它在虚拟机启动时创建,是Java虚拟机管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

程序计数器 (Program  Counter Register):

程序计数器属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

虚拟机栈 (Stack):

虚拟机栈属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈帧来存储方法的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用到结束对应栈帧的入栈和出栈过程

本地方法栈 (Stack):

本地方法栈属于线程私有的数据区域,主要与虚拟机用到的Native方法相关,一般情况下,我们不需要关心此区域

2.Java内存模型

Java内存模型 (Java Memory Model,JMM)是一种抽象的概念,并不真实存在,它描述的是一组规范或者规则,通过这组规范定义了程序中各个变量 (包括实例字段、静态字段和数组对象的元素) 的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存 (有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须 (读取赋值时) 必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成

主内存:

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题

工作内存:

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题

3.Java的四种引用

   强引用:

       比如Object object=new Object();这里的object引用就是一个强引用,如果一个对象具有强引用,垃圾回收器不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,也不会回收具有强引用的对象

   软引用:

       当内存空间足够,垃圾回收器就不会回收它,如果内存不够了,垃圾回收器就会回收这些对象的内存,可以使用SoftReference类来实现软引用,软引用可以和一个引用队列 (ReferenceQueue) 联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM就会把这个软引用添加到引用队列中,我们可以调用ReferenceQueue的poll()方法来检查其中是否有Reference对象存在,若有代表该软引用指向的对象已经被回收,否则返回null

    弱引用:

        只要垃圾回收器在自己的内存空间中线程检测到了,就会立即被回收

    虚引用:

        如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被JVM当作垃圾回收

4.垃圾回收算法

     标记-清除算法:

         首先从根集合进行扫描,对存活的对象进行标记,标记完成后,再扫描整个空间中未标记的对象,并进行回收,它是最基础的收集算法

     缺点:效率不高,标记和清除两个过程的效率都不高,而且会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作

     复制算法:

         将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上,然后原来那一块内存空间完全清理掉

      现在的虚拟机都是采用这种算法来回收新生代,新生代中的对象每次回收都基本只有10%左右的对象存活,需要复制的对象很少,所以改进后的复制算法将内存划分为8:1:1三部分,较大的那部分是Eden区,两块小的是Survivor区,每次使用Eden和其中一块Survivor,回收时将Eden和使用的那块Survivor区域中存活的对象,一次性复制到那块未使用的Survivor区域中,然后清理掉Eden和刚刚使用的Survivor区域,这样每次新生代中可用内存都为90%,只有10%的内存会被浪费。如果此时存活的对象过多,Survivor区域不够存放,这时候就需要老年代来进行分配担保,内存分配担保是指如果另一块Survivor没有足够空间存放上一次新生代垃圾回收后留下的存活对象,那么这些存活对象会直接进入老年代

      标记-整理算法:

          这种算法适用于老年代,标记过程与标记-清除算法一样,但是后续不是直接对可回收对象进行回收,而是让所有存活的对象向一端移动,然后直接清除掉端边界以外的内存

          标记-整理算法与标记-清除算法最大的区别就是:标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记-整理算法会将所有存活对象移动到一端,并对不存活对象进行处理,所以它不会产生内存碎片

        分代收集算法:

            不同对象的生命周期是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存中不同区域采用不同的策略进行回收可以提高JVM的执行效率。新生代对象存活率低,采用复制算法,老年代对象存活率高,采用标记-整理算法

5.JVM中的垃圾收集器

              JVM中的垃圾收集器

6.Minor GC和Full GC什么时候会被触发

Minor GC:从新生代空间(包括Eden和Survivor区域)回收内存被称为Minor GC

Full GC:清理整个堆空间

当Eden区满时,会触发Minor GC

Full GC在满足以下条件时被触发:

1).老年代空间不足;当新生代对象转入和创建大对象、大数组时才会出现不足的现象

2).持久代空间不足;当系统中要加载的类,反射的类和调用的方法较多时,持久代可能会被占满

3).CMS GC时出现promotion failed和concurrent mode failure

promotion failed:在进行Minor GC时,Survivor空间放不下,对象只能放到老年代,而此时老年代也放不下

concurrent mode failure:是在执行CMS GC的过程中同时有对象要放到老年代,而老年代空间不足造成的

4).统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间

5).代码中执行System.gc()

7.JVM类加载

               Java中的类加载

8.双亲委派模型

双亲委派模型要求除了顶层的启动类加载器之外,其他的类加载器都要有自己的父类加载器

如果一个类加载器收到了类加载的请求,它不会去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是这样,因此所有的加载器请求最终都会传递到顶层的启动类加载器中,只有当父加载器无法完成这个加载请求时,子加载器才会尝试自己去加载

9.内存泄漏和内存溢出

内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间

内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory

出现内存泄漏的情况:

1).静态集合类,例如HashMap和Vector,如果这些容器是静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前不能被释放,从而造成内存泄漏

2).各种连接,如数据库的连接,网络连接,IO连接等没有被释放

3).监听器,在Java中,一个应用中会用到多个监听器,但是在释放对象的同时往往没有相应的删除监听器,可能导致内存泄漏

4).变量不合理的作用域,如果一个变量定义的作用域大于其使用范围,很有可能会造成内存泄漏,另一方面如果没有即时的把对象设置为Null,也可能导致内存泄漏

5).单例模式可能会造成内存泄漏

内存溢出的情况:(OOM)

1).内存中加载的数据量过大,如一次从数据库取出过多数据

2).集合类中有对对象的引用,使用完后未清空,使得JVM不能回收

3).代码中存在死循环或者循环产生过多重复的对象实体

4).启动参数内存值设定的过小

10.内存屏障

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序

JVM把内存屏障指令分为4类:

屏障类型 指令实例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStore Barriers Store1;StoreStore;Store2 确保Store1数据对其它处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
LoadStore Barriers Load1;LoadStore;Store2 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
StoreLoad Barriers Store1;StoreLoad;Load2 确保Store1数据对其他处理器变得可见,先于Load2及所有后续装载指令的装载,StoreLoad Barriers会使该屏障之前的所有内存访问指令完成之后,才执行该屏障之后的内存访问指令

StoreLoad Barriers是一个全能型的屏障,它同时具有其他3个屏障的效果

内存屏障的使用:

1).通过Synchronized关键字修饰的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新值,对数据的读取不能从缓存读取,只能从内存中读取,保证了数据的有效性,这就是插入了StoreStore屏障

2).使用了Volatile修饰变量,对变量的写操作,会插入StoreLoad屏障

3).其余的操作,需要通过Unsafe这个类来执行

11.如何判断一个对象是否存活

1).引用计数法

引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为0时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收

引用计数法的缺陷就是无法解决循环引用问题,当A对象引用对象B,对象B又引用对象A,此时A,B对象的引用计数器都不为0,造成无法进行垃圾回收

2).可达性算法(引用链法)

从GC Roots开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,说明此对象不可用

可以作为GC Roots的对象有以下几种:

虚拟机栈中引用的对象

方法区类静态属性引用的对象

方法区常量池引用的对象

本地方法栈JNI引用的对象

12.访问对象时,如何定位到该对象

1).使用句柄

Java栈的reference中存储的是对象的句柄地址,Java堆中划分出一块内存来作为句柄池,而句柄池中包含了对象的示例数据(存放在堆中)与类型数据(存放在方法区)各自的具体地址信息

优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,reference本身不需要修改

2).直接指针

Java栈的reference中存放的直接就是对象地址

优点:节省了一次指针定位的时间开销,速度快

13.new一个对象创建的过程

        

猜你喜欢

转载自blog.csdn.net/ys_230014/article/details/88577648
今日推荐