深入理解JVM--笔记

第二章 java 内存区域 与内存溢出异常

  2.2运行时数据区

      方法区,堆==》所有线程共享的数据区

      虚拟机栈、本地方法栈、程序计数器===》线程隔离使用的数据区

     2.2.1:程序计数器

         程序计数器是一块较小的内存,每条线程隔离使用,用于记录线程执行的行号。

         多线程执行时轮流获取cpu时间片执行的,当线程恢复执行时,需要接着上一次执行的地址继续执行。

    2.2.3 : 虚拟机栈

           虚拟机栈是每条线程隔离使用,线程创建时会在栈中分配空间,线程销毁时该空间释放。

           栈:先进会出的数据结构。

           线程每执行进入一个方法,就会在线程栈中创建一个栈帧,成为当前帧,栈帧用于存储方法的局部变量表,局部变量表中存放着编译期可知的基本数据类型(byte,char,short,int ,long ,double,float,boolean) 和引用对象指针(对象存储在堆中)。为每一个方法创建的帧占用的空间在固定的,因为在编译即可知局部变量个数。

          栈异常 StackOverFlowError :原因:线程请求的栈深度(方法调用深度)大于虚拟机允许的深度,与内存无关。 

         栈异常 OutOfMemoryError: 虚拟机栈空间可以动态扩展,扩展时无法申请到足够的内存

      2.2.4 本地方法栈

        与虚拟机栈,区别是用来执行native方法。

      2.2.5 堆

        堆 java 使用的最大的一块内存,用于存储对象实例和数组,所有线程共享。

        推中的内存空间收GC回收,为了便于回收又划分为:新生代和老年代。

         新生代包括:Eden空间、From Survivor 空间、To Survivor空间。

        通过 -Xmx -Xms 指定推的大小。

         异常:OutOfMemoryError 当堆中没有空间存储对象实例且堆无法扩展时触发。

  2.2.6 方法区

        用于存储 类的信息、常量、静态变量,是所有线程共享的一块内存区域。

        在hotspot虚拟机中,方法区又叫永久代(持久代)

       通过-XX:PermSize -XX:MaxPermSize 指定方法区大小

        异常:OutOfMemoryError ,当方法区无法满足内存分配要求时触 发

    

         2.2.6.1 运行时常量池

             常量池是方法区的一部分。

              Class文件除了有类的版本、字段、方法、接口等描述述信息外,还有常量池,用于存储编译时生

        成的各种字面量和符号引用,这部分数据类加载后进入方法区的运行时常量池。 

               

              运行期也可以运态的在常量池中添加对象,如 String.intern()方法

                  String.intern()方法:

                       方法区内常量池中如果有同等对象直接返回,否则在方法区常量池中创建

                        String对象并返回。频繁在常量池中创建新字串,可能导至内存溢出。

        2.2.7  直接内存

          直接内存并不是由虚拟机管理的内存,如DirectByteBuffer ,在一些场景中可以提高性能。 

 2.3 hotspot虚拟机探秘

       2.3.1对象的创建

             a)分配空间

            堆中为实例分配空间,存在线程安全问题:如A线程创建实例 ,B线程也创建实例,堆中空间有限,

            为了避免相同空间分配给不同对象,可采用如下两种方式:

                  方式一: 分配空间时,使用CAS+重试的机制

                  方式二: 为每条线程在堆中分配一小块内存区域 ,称为本地线程分配缓冲(TLAB),线程创建的实例,优先在此区域分配空间,避免冲突。 只有TLAB空间被占满时,才使同步锁,顺序的分配堆空间。

                                  是否启用TLAB,使用JVM参数:-XX+/-UseTLAB参数来指定

             b) 实例空间分配完成后,将该空间初始化为0.保证了实例在未初始化时就可以使用,值都为0.

                 (不包括对象头)

             c) 初始化对象头,如这个实例是哪个类的,如何找到类的元数据信息,实例的哈希码,GC的分代年龄等信息。 

              d) 初始化方法被调用(构建器及初始化代码块)

        2.3.2 对象实例的内存布局

              在堆中,一个实例的内存布局分成三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

               对象头数据分为两部分:

                      a) 对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID,偏向时间戳。 

                             这部分数据占用空间:32位虚拟机占用32bit。64位虚拟机占用64bit 。

                             这部分空间,官方称为:Mark Word

                      b) 元数据类型指针,即指向这个对象实例的类的信息

              实例数据:存储程序代码中定义的各种属性的值。包含从父类中继承下来的。

              对齐填充:不是必须有的,启占位符的作用,原因:HotSpot虚拟机要求实例的起始地址是8的整数倍。

        2.3.3 对象的访问定位

              在HotSpot虚拟机中: 栈中 对象引用变量直接指向堆中实例的地址。

第三章        垃圾收集器与内存分配策略

   3.2.     对象实例已死的判定算法

              堆中对象实例不再使用了,需要释放该实例占用的空间,那么如何判定实例不在使用?

            3.2.1 引用计数法

                  给对象添加一个引用计数器,每当有一个地方引用它:计数器+1,当引用失败时:计数器-1,

            任何时刻计数器为0的就是不可再引用对象。

                  java虚拟机未采用 引用计数法,原因如下:如两个实例相互引用对方,除此之外没有被任何地方引用,实现上这两个对象该被释放空间了,但按引用计数法,它们还是有用的。

           3.2.2  可达性分析算法

                 通过一系列称为“GC roots" 的对象为起始点,从这些节点开始按引用关系搜索,搜索所走过的路径称为引用链,当一个对象到GC roots 没有任何引用链相连,则认为该对象是不可用的。

                java 中可以做为GC roots 的对象为:

                    a):虚拟机栈中本地变量表引用的对象

                    b):方法区中静态变量引用的对象

                    c):方法区中常量引用的对象

                    d):本地方法栈(java 中native方法)中引用的对象

            3.2.3 引用类型分类

                    1)强引用

                           指程序中普遍存在的类似于 "Object obj  = new Object()" 这类引用,只要强引用还存在

                             垃圾回收器永远不会回收。

                    2)软引用

                           描述有用但不是必须的对象。在发生内存溢出之前进行垃圾回收。 

                    3)弱引用

                           描述非必须的对象,垃圾回收器执行时,不管内存是否足够都 进行回收。

                    4)虚引用

                           最弱的一种引用关系,为虚引用关联一个对象实例的唯一目的,是产生垃圾回收时得到一

                        个系统通知。

     3.3 垃圾回收算法

            3.3.1 标记-清除算法

                     1:首先找到GC roots不可达对象,对其标记

                     2: 对其清理

                    缺点:   1.标记、清理 :这两个过程的效率不高

                                  2.     产生大量不连续的内存碎片,不利于之后为对象分配空间。

            3.3.2  复制算法

                          为了解决标记清除的效率问题,产生了复制算法。

                          它将可用内存划分为大小相同的两块,每次只使用其中一块,当这一块用完了就将还存活的

                  复制到另一块内存上,使用的内存块一次清理掉。这样每次只对一半内存区回收,内存分配也不

                  考虑内存碎片问题,只是移动堆顶指针,按顺序分配空间。 

                      缺点:内存使用原来的一半。

                     进一步优化后:

                          新生代+老年代。

                          新生代:大块eden空间 + 2个小块相等的Survivor空间。

                          每次只使用eden 和其中一个Survivor空间,回收时把eden和Survivor上的存活对象复制到

                    另一个Survior上,最后清理掉Eden 和使用的Survivor.

                          HotSpot默认 Eden与 一个Survivor 比例为 8:1,也就是新生代中90%的空间可用,只浪费                   10%的空间。这些都是基于IBM的研究:98%的对象都是朝生夕死的。 

                         如果另一个Survior上,没有空间存放所有的存活对象,会通过分配担保机制进入老年代。

           

          3.3.3 标记-整理算法

                    复制算法不适用于老年代,因为该空间内的对象存活比例较大,需要额外的另一块内存空间来复制。

                     标记-整理算法: 先标记不可达对象(找到存活对象),从老年代的某一端开始,复制所有存活对象,依次分配内存。

         3.3.4 分代收集算法

               新生代中每次垃圾收集都会有大量对象死去,只有少量存活,那就选择复制算法

              老年代中存活率高,没有额外的空间复制,所有使用 标记-清理 或标记-整理算法。

                                     

  3.4 HotSpot算法    

  3.5 HotSpot 垃圾回收算法

           3.5.1 Serial 收集器

                            Serial 收集器  是单线程垃圾回收器采用单线程方式,停止所有用户线程进行垃圾回收。

                            Serial 收集器: 回收新生代采用 复制算法。

                            Serial old收集器: 回收老年代采用 复制整理算法。            

           3.5.2 ParNew 收集器

                      ParNew收集器其实是Serial收集器的多线程版本,采用多线程方式回收垃圾,暂集所有用户线程。   

                       ParNew是新生代的垃圾回收器,默认线程数==CPU数量。 (在cpu<=2的情况下,并不能比Serial收集器快),可以使用能数-XX:ParallelGCThreads指定线程数。

                        ParNew 收集器采用复制算法。

           3.5.3 Parallel Scavenge 收集器

                     Parallel Scavenge 收集器:回收新生代,采用复制算法,多线程回收,暂停工作线程。

                     Parallel Scavenge收集器与其它收集器 的不同点:

                                其它收集器是:尽可能缩短垃圾回收时工作线程的暂停时间。

                                 Parallel Scavenge:提高吞吐量。

                                                                  吞吐量: cpu运行用户代码的时间/cpu总的消耗时间,即

                                                                                  cpu运行用户代码的时间/cpu运行用户代码时间+垃圾回收时间。如虚拟机总运行时间100分钟,垃圾回收占用1分钟,吞吐量为 99%。

                  高吞吐量:可以高效率的利用CPU,尽可能的完成程序运算的任务,适合后台计算,不需要太多交互的任务。 

                  其它收集器:适合与用户交互的程,停顿时间越短,良好的响应速度提升用体验。

            3.5.4 Serail old收集器

                        Serail  收集器:回收老年代,采用复制整理算法,单线程回收,暂停工作线程。

            3.5.5 Parallel old 收集器

                          Parallel old 收集器:回收老年代,采用复制整理算法,多线程回收,注重吞吐量,暂停工作线程。 

                         注重吞吐量应用:新生代使用 Parallel Scavenge ,老年代使用Parallel old 收集器。

             3.5.6 CMS收集器

                          CMS收集器:回收老年代,采用标记-清理算法,多线程回收,注重最短停顿时间,与工作线程并发执行。

                           

第十二章 java 内存模型与线程

      12.3 java内存模型

             

  java 所有数据存储在主内存中,线程要对某个变量操作时,从主内存中加载数据在工作内存,再由工作内存中读取,再操作,操作完成(赋值)写回工作内存,再从工作内存写回主内存。

      

      工作内存的每条线程私有的,因此多线程同时对于一个变量的操作,可能产生线程安全问题。

      多线程间数据交互通过主内存实现的。

  

      工作内存与主内存之间的交互协议:

            1.lock (锁定):作用于主内存的变量,它把变量标识为一条线程独占状态

             2.unlock(解锁):作用于主内存变量,它把处于锁定状态的变量释放出来,释放后变量才可以被其它线程锁定。 (同一线程程可以对一个变量lock N次,也必须unlock N次)

            3.read (读取):作用于主内存变量,它把一个变量的值从主内存传给工作内存,以便之后的load操作使用。

            4.load(加载):作用于工作内存变量,它把read操作得到的变量放到工作内存的变量副本中。

            5.use(使用):作用于工作内存变量,它把工作内存中的变量传给执行引擎。每当jvm遇到一个需要使用变量值的指令时执行此操作。

            6.assign(赋值):作用于工作内存变量,它把从执行引擎接收到的值赋值给工作内存变量。当jvm遇到一个赋值指令时执行此操作。

            7.store(存储):作用于工作内存变量,它把工作内存中的变量传给主内存,以便之后的write 操作使用。

            8.write(写入):作用于主内存变量,它把store操作的值写入主内存。

      锁相关规定:

              1.一个变量,同一时刻只允许一线程对其进行lock操作,lock操作可以被同一线程执行多次,多次执行lock后,只有执行相同次数所unlock,变量才会被解锁。

              2.对一个变量执行lock操作,会清空工作内存中的值,在执行引擎使用这个变量时,需要重新执行load<---read 操作 加载值。

             3.一个变量unlock之前必须,对其执行store-->write写回主内存。

    volatile型变量:可见性 

              1.每次使用volatile变量时,都必须从主内存刷新最新值到工作内存,再操作,用于保证能够看见其它线程对volatile变量的修改值。

              2.每次对volatile 变量值修改时,都 必须立刻同步到主内存中,用于保证其它线程能够看到自己对volatile变量的修改值。

              3.volatile变量不会被指令重排序优化:即代码执行顺序与程序中的顺序机同。

     普通型变量:

              1.每次使用变量时,不一定会从主内存刷新最新值,比如当前工作内存中就该变量。

              2.每次对变量值修改时,可能不会立刻同步到主内存中。

        可见性 :当一个线程修改了一个共享变量值,其它线程能够及时得知这个修改。

               1.volatile 保证变量读取时从主内存取值-》工作内存-》执行引擎

                                            赋值时执行引擎--》工作内存-》立即写入主内存。

               2.synchronized : 锁定时,清空工作内存此变量的值,从主内存读取,unlock之前,把值写回主内存。

               3.不可变对象及final: 变量不可变,对可见性无影响。

          禁止指令重排优化:

               1.volatail

                2.synchronized

  

          synchronized具有: 可见性,原子性(线程串行执行),禁止指令重排优化,

           volatail具有:可见性,  可见性,非原子性,禁止指令重排优化

  12.4 线程

      1.线程调度

             分配CPU时间片执行

      2.线程状态

              a)新建状态 ,Thread被创建,但未start

              b)可运行状态: 线程启动进入可执行队列,但未分配cpu时间片执行

              c) 运行状态:线程在运行

              d) Waiting :这种状态下线程不会被分配cpu时间片,在等待其它线程唤醒,以下方法会进入waiting状态:

                                   1.没有timeout 参数的Object.wait

                                    2.没有timeout参数的Thread.jon

                                    3.:LockSupport.park

              e)TimedWaiting :这种状态下线程不会被分配cpu时间片,在等待其它线程唤醒或超时,以下方法会进入TimedWaiting状态:

                                  1.Thread.sleep()

                                   2.设置了Timeout参数的Object.wait(TimeOut)

                                   3.设置了Timeout参数的Thread.join(TimeOut)

                                   4.LockSupport.parkNanos(long nanos) 

                                   5.LockSupport.parkUntil(long deadline)

              f)Blocked阻塞状态:

                             线程在等待一个排他锁(synchronized 锁)

              g)结束状态

                       线程执行完成

第十三章 线程安全与锁优化

        13.1 线程安全的实现方法

                1.互斥同步

                       synchronized 和ReentrantLock,都是可重入锁具有机同的内存语义,synchronized 语法层面,ReentrantLock API层面上的

                       两者区别: 

                              

                                   1.等待可中断

                                          synchronized 会一直等待持有锁的线程释放锁

                                           ReentrantLock.lockInterruptibly() 会一直等待持有锁的线程释放锁,或其它线程中断此线程。

                                   2.公平锁

                                           synchronized 是非公平锁

                                            ReentrantLock默认也是非公平锁,可以设置为公平锁

                                  3.锁绑定多个条件

                                        synchronized 只允锁定对象为一个隐含的条件,调用其.wait .notify.notifyAll 

                                         ReentrantLock 可以通过newCondition 创建多个条件

                                  4. ReentrantLock 可以使用tryLock方法判定是否其它线程持有锁。

                                         适用于多个线程有一条线程处理就可以的场景。

                                  5.同步代码中异常时,synchronized会自动释放锁,ReentrantLock需要在代码finally里释放锁。

                2.使用CAS (compare and swap)  

                     此方式不会阻塞线程,性能比互斥锁好,适用于 极短时间内简单操作。

                     CAS 需要3个参数:变量在内存中的地址,原值,新值。

                                                     如果原值与内存地址中的值相同,则用新值替换,否则失败。

                     

                     CAS存在"ABA"问题。

                3.不使用同步机制

                     3.1 不使用共享数据

                             方法涉及的数据都是私有或不可变的。

                     3.2.不可变对象

                           多线程访问同一个不可变对象是安全的。

                     3.3. 线程内对象

                             ThreaLocal

                

猜你喜欢

转载自java12345678.iteye.com/blog/2381537