java虚拟机

1.JVM的运行时数据区

根据JVM规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方发栈五个部分.其中堆与方法区是线程共享,其他三个是线程私有的

a.java虚拟机栈

线程私有;每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈(如sum = i + j 处理后将结果存储到局部变量表中),动态连接,方法返回地址等;每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。

如写一个栈溢出(其实就是方法的递归调用,且没有出口)

int num = 1;
public void testStack() {
    num++;
    this.testStack();
}

b.堆

线程共享;被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例
如让你写一个堆溢出的代码(也就是不断new新的对象)
public void testHeap() {
     for(;;) {
        ArrayList<String> list = new ArrayList<>(1000);
     }
}

c.方法区

线程共享:被所有线程共享的一块区域;用于存放类信息,常量,静态变量,即使编译器编译后的代码.

存放的信息包括:

  类的基本信息:

    1)每个类的全限定名

    2)每个类的直接超类的全限顶名

    3)该类是类还是接口

    4)该类的访问修饰符

    5)直接超类接口的全限定名的有序列表

  已装载类的详细信息

    1)运行时常量池:方法区中,每个类都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了.诸如文字字符串,finale变量值,类名和方法名常量,他们以数组的形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁

    2)字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。字段名称指的是类或接口的实例变量或类变量, 字段的描述符是一个指示字段的类型的字符串,如private A a=null;则a为字段名,A为描述符,private为修饰符。

    3) 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。)

    4) 静态变量:就是类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。

    5)到类classloader的引用:到该类的类装载器的引用。

    6)到类class 的引用:jvm为每个加载的类型(译者:包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。

d.程序计数器

线程私有;是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

e.本地方发栈

线程私有;主要为虚拟机使用到的Native方法服务。

2.JVM内存模型(主要针对的是java的堆和方法区)

 -Xms初始堆大小
        默认物理内存的1/64(<1GB)
-Xmx最大堆大小
        默认物理内存的1/4(<1GB),实际中建议不大于4GB
一般建议设置 -Xms= -Xmx
        好处是避免每次在gc后,调整堆的大小,减少系统内存分配开销

整个堆大小=年轻代大小+年老代大小+持久代大小(方法区,默认情况下256M)

新生代
        新生代=1个eden区+2个survivior区
        -Xmn 年轻代大小(1.4or lator)
        -XX:NewRatio  年轻代(包括Eden区+2个survivior区)与年老代的比值(除去持久代)(基本上不用)
        -XX:SurvivorRatio  Eden区域S区的比值,默认是8:1:1
        用来存放JVM刚分配的java对象
老年代
        老年代=整个堆-年轻代大小-持久代大小(没有对应的参数分配,用计算得出)
        年轻代中经过垃圾回收没有回收掉的对象被复制到年老代
        年老代存储对象比年轻代年龄大的多,而且不乏大对象。
        新建的对象也有可能直接进入老年代
            大对象,可通过 参数设置-XX:PretenureSizeThreshold=1024来代表超过多大时就不在新生代中分配,而直接在老年代中分配
            大的数组对象,且数组中无引用外部对象
        老年代大小无配置参数
持久代(方法区,相对来说持久,有的jdk不能回收,只能扩大,但是现在有的也能回收)
        -XX:PermSize  -XX:MaxPermSize
            一般情况下,推荐将其设置为相等,因为持久代的大小调整也会导致堆内存需要出发fgc
        存放Class、Method元信息,其大小与项目的规模、类、方法的数量有关。一般设置为128M就足够,设置原则是预留的30%的空间
        永久代的回收方式
            常量池中的常量,无用的类信息,常量的回收很多件,没有引用了就可以被回收
            对于无用的类回收,必须保证3点(q其实要达到这三点是非常难得,所有无用的类信息很难被回收)
                类的所有实例都已经被回收
                加载类的ClassLoader已经被回收
                类对象的Class对象没有被引用(既没有通过反射引用该类的地方)

3.java垃圾回收机制

java中,GC的对象主要是堆空间和永久区

a.如何判断某个对象是否为垃圾?

引用计数法和可达性分析法

1)引用计数法

  引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

引用计数法的问题    

  引用和去引用伴随加减法,影响性能

  很难处理循环引用的问题

2)可达性分析法

  从GCRoots节点一直往下走,如果走不通,说明走不通的那些对象是不可用的,那就是垃圾.

  可以作为GCRoots的对象

    虚拟机栈(局部变量表)

    方法区的类属性所引用的对象

    方法区中常量所引用的对象

    本地方法栈中引用的对象

b.如何回收(回收策略,垃圾回收算法)

    1)标记-清除算法

  2)标记-整理算法(也就是标记-压缩算法)

  3)复制算法

  4)分代收集算法

1.标记-清除(其他算法的基础)

标记-清除算法是现代收集算法的思想基础.标记-清除算法将垃圾回收分为两个阶段,标记和清除阶段.一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始可达的对象那个因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

不足:
            1.效率问题,标记和清除两个过程的效率都不高
            2.空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

2.标记-整理(标记-压缩算法)(主要针对老年代的收集算法,因为老年代相对来说存活的对象比较多)

标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

3.复制算法(主要针对新生代的收集算法,因为新生代存活的对象比较少)

与标记-清除算法相比,复制算法是一种相对来说比较高效的回收算法,但是此种算法并不适用于存活对象比较多的场合如老年代,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

4.分代收集算法

标记-整理和复制算法结合使用,针对新生代采用复制算法,针对老年代采用标记整理算法。

c.垃圾回收器

1)Serial(串行回收)

  -XX:+UseSerialGC来开启

  最基本,发展最悠久的收集器

  单线程垃圾收集器(先是程序跑一段时间,然后所有线程又停下来,垃圾收集器开始收集垃圾)(打j扫垃圾的时候就不能扔垃圾啦)

  桌面应用使用比较多

  优点:简单.对于单CPU的情况由于没有多线程交互的开销,反而可以更加高效,是Client模式下默认的新生代收集器

  缺点:Stop-The-World

2)Parnew(并发回收)(Serial的多线程加强版)

  -XX:+UseParNewGC开启

       -XX:ParallellGCThreads指定线程数
  复制算法(新生代收集器)
  相比于Serail是多线程收集的,那么收集的间隔会降低一些

3)Parallel Scavenge(并行回收)(目标:达到一个可控制的吞吐量)

   -XX:+UseParallelGC开启

  复制算法(新生代收集器)

  多线程收集器

  达到一个可控制的吞吐量(cpu用于运行用户代码的时间与cpu消耗的总时间的比值)

  -XX:MaxGCPauseMills 垃圾收集器最大停顿时间(如果设置的最大停顿时间短了,则回收的频率就增大了咯)

  -XX:GCTimeRatio 吞吐量大小

  停顿时间越短就越适合与用户交互的程序,良好的相应速度能提升用户体验

  而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务.

4)Cms(Concurrent Mark Sweep并发标记清除)(大多数互联网公司现在都在是哦那个这个收集器)

    标记清除算法(老年代收集器)

   工作过程  

    • 初始标记:值标记GC Roots能直接关联到的对象
    • 并发标记:进行GC Roots Tracing的过程
    • 重新标记:修正并发标记期间因用户程序继续运行而导致标记发生改变的那一部分对象的标记
    • 并发清理:并发收集垃圾

   优点

    并发收集(边扔边打扫)

    低停顿

   缺点

    占用大量CPU(当CPU数量在4个以上时,并行回收时垃圾收集线程不少于25%的CPU资源,而在不足4个,可能影响更大)

    无法处理浮动垃圾(在并发清理阶段用户线程还在运行着,自然会有新的垃圾不断产生,这一部分只能留待下一次GC再清理)

    空间碎片(由于使用的是标记-清除算法

    出现Concurrent Mode Failure

     -XX:ParallelCMSThreads:手工设置CMS线程个数,CMS默认启动的线程数是(ParalleleGCThreads+3/4)

     -XX:+UseConcmarkSweepGC:开启

     -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后发出垃圾收集,默认值为68% 

     -XX:+UseCMSCompactAtFullCollection:由于CMS收集器会产生碎片,此参数设置在垃圾收集后是否需要一次内存碎片整理工程

     -XX:+CMSFullGCBeforeCompaction:设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通过UseCMSCompactAtFullCollection一起使用

 

5)G1(Garbage First)(JDK1.9里)

  • 并行与并发
  • 分代收集 
  • 空间整合
  • 可预测的停顿

d.GC何时开始?

堆:

  新生代    8:1:1,空间利用率百分之九十,因为假设Eden区,假设new了8M对象,再new1M对象,则这1M放在s0区域,Eden区域满了,执行Minor GC,存活率只有百分之十,那么这个百分之十放在s1中,将另外的全部清除掉。\

  Eden 伊甸园 

  Survivor 存活区s0

  Tenured Gen,s1

新生代:划分为三个区域:原始区(Eden)和两个小的存活区(Survivor),两个存活区按功能分为From和To。绝大多数的对象都在原始区分配,超过一个垃圾回收操作仍然存活的对象放到存活区。垃圾回收绝大部分发生在年轻代。

老年代:存储年轻代中经过多个回收周期仍然存活的对象,对于一些大的内存分配,也可能直接分配到永久代。

持久代:存储类、方法以及它们的描述信息,这里基本不产生垃圾回收。

 

e.内存分配策略

优先分配到Eden(当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC)

大对象直接分配到老年代(如byte数组等)

长期存活的对象直接进入老年代

  为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。每熬过一次年龄就增加1.当达到(默认是15,有参数可以设置)一定程度,就会被晋升到老年代中

  为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保(新生代内存不够向老年代中借)

 

有了上面的知识后回答GC何时开始?
Eden内存满了之后,开始Minor GC(从年轻代空间回收内存被称为 Minor GC,而清理老年代成为Major GC/Full GC),将存活的对象copy;升到老年代的对象所需空间大于老年代剩余空间时开始Full GC(但也可能小于剩余空间时,被HandlePromotionFailure参数强制Full GC)
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度比较快
老年代GC(Major GC/Full GC):指发送在老年代的GC,出现了Minor GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上

4.java类加载

a.运行流程

b.基本结构

  类加载器,执行引擎,运行时数据区,本地接口

    Class Files -> ClassLoader -> 运行时数据区 ->执行引擎,本地库接口 ->本地方法库

  类的装载

    加载:查找并加载类的二进制数据

      通过一个类的全限定名来获取定义此类的二进制字节流(虚拟机并没有指明二进制字节流要从一个Class文件中获取,准确地说根本没有指明要从哪里获取、怎样获取等)

        从ZIP包中读取,这很常见,最终成为JAR、EAR、WAR格式的基础

        从网络中获取,典型应用Applet

        运行时计算生成,这种场景使用最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“$Proxy”的代理类的二进制字节流

        ......

      将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

      在内存中生成一个代表这个类的java.lang.Class对象(并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面)这个对象那个将作为程序访问方法区中的这些类型数据的外部接口。

             连接(加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始了)
        验证:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求 
       准备:为类的静态变量分配内存,并将其初始化为默认值
          解析:把类中的符号引用转换为直接引用
  初始化:为类的静态变量赋予正确的初始值
  使用
  卸载
Java程序对类的使用方式分为两种
  主动使用
  被动使用
我们的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化他们。(对于什么时候类加载,java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把我,但是对于初始化阶段,虚拟机严格对定了以下几种情况)
主动使用
  创建类的实例
  访问某个类或接口的静态变量,或者对静态变量赋值
  调用类的静态方法
  反射
  初始化一个类的子类
  java虚拟机启动时被表明为启动类的(如一个类中含有main方法)
public class ClassLoaderDemo3 {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getinstance();//主动使用,SingleTon类会被加载和初始化
        System.out.println("count1 = " + SingleTon.count1);//结果为1
        System.out.println("count2 = " + SingleTon.count2);//结果为0(因为在初始化时,先是new SingleTon()里面执行++但此时count1与count2都是默认值0,执行完后count2又被赋值为0)
    }
}

class SingleTon{
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
    private SingleTon() {
        count1 ++;
        count2 ++;
    }
    public static SingleTon getinstance() {
        return singleTon;
    }
}

 

c.类加载器使用双亲委派模型

1)JDK已有的类加载器

            BootStrap ClassLoader( 启动(Bootstrap)类加载器C++编写的,所以返回时会用null来表示)(根加载器,必须先启动jvm,jvm也是一个程序,也需要加载的)===》 这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的 (仅按照文件名识别,如rt.jar,名字不符合的类库即使放在该目录下也不会被加载)。
          Extension ClassLoader extends ClassLoader( 标准扩展(Extension)类加载器Java编写的)==》主要加载%JAVA_HOME%/lib/ext/*.jar
          App ClassLoader extends ClassLoader( 系统(System)类加载器Java编写的)  ====> 它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
          自定义类加载器(必须继承ClassLoader)====》加载的自定义路径的类复写findClass(String name)方法,实现自定义加载器。

 2)双亲委派模型

            如果一个类加载器接收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器取完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈自己无法完成这个 加载请求(它搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

3)为什么要使用这种双亲委派模型呢?

            比如两个类A和类B都要加载System类,这样的话,我们可以随便定义String类,然后都使用自己的加载器去加载:              
         如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
         如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,
         如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载, 这样内存中就只有一份System的字节码了。

 

posted @ 2018-08-28 21:41  刘丽刚  阅读(138)  评论(0)    收藏  举报