jvm学习

JVM是Java Virtual Machine(Java虚拟机)的缩写,运行在操作系统之上。

JVM体系结构图:

 

 

类装载器:
  负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的
运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
例如加载一个Car的class文件:

 

 类装载分为两类:虚拟机自带和用户自定义

虚拟机中自带了三种虚拟机:

  • 启动类加载器(Bootstarp):加载lib/rt.jar下的文件
  • 扩展类加载器(Extension):加载lib/ext/*.jar下的扩展文件
  • 应用程序类加载器(AppClassLoader)也叫系统加载器:加载classPath下的所有类,如我们自定义的car类

demo:

class  MyObject {

}
public class ClassLoaderDemo {
    public static void main(String[] args) {
        Object object = new Object();
        //null  Bootstrap 类加载器
        System.out.println(object.getClass().getClassLoader());
       /* //异常  上面已经没有了,Bootstarp是第一个类加载器
        System.out.println(object.getClass().getClassLoader().getParent());
        //异常
        System.out.println(object.getClass().getClassLoader().getParent().getParent());*/

        MyObject myObject = new MyObject();
        //AppClassLoader 类加载器
        System.out.println(myObject.getClass().getClassLoader());
        // Extension  扩展类加载器
        System.out.println(myObject.getClass().getClassLoader().getParent());
        //Bootstarp  类加载器  null
        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());

    }
}

myObject.getClass().getClassLoader():可以获取到加载器得名字

myObject.getClass().getClassLoader().getParent():可以得到当前加载器的上一个加载器,当已经获取到Bootstrap加载器,再调用getParent()会报空指针异常;

类加载器的顺序是从上往下加载

Bootstarp———》Extension————》AppClassLoader

 

双亲委派:

       当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,

每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈

自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

      采用双亲委派的一个好处是,比如加载位于rt,jar包中的类java.lang.Object, 不管是哪个加载器加载这个类,

最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

 

本地接口:

  在我们看源码的时候,有时会看到native标记的方法,这就是本地方法,它是和底层硬件进行交互。

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载native libraies。

 

pc寄存器:

  每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一

条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节这块内存区域很小,它是当前线程所执行的

字节码的行号指示器,字节如果执行的是一个Native方法,那这个计数器是空的。用以完成分支、循环、跳转、异常处理、

线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。

  简单理解就是告诉计算机程序运行到了哪一步。

 

Method Area方法区:

  供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool) 、

字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,

最典型的就是永久伏(PermGen space,1.7以前版本) 和元空间(Metaspace,1.8版本

 

栈stack:

栈是一种数据结构,遵循先进后出/后进先出的原则,例如弹夹。

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,

每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体

JVM的实现有关,通常在256K~ 756K之 间,与等于1Mb左右。

 

 

 

每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈

demo:

class Test{
    public void add(){
        add();
    }
}
public class StackDemo {
    public static void main(String[] args) {
        Test test = new Test();
        test.add();
    }
}

那么栈里面的结构图是这样:

 

 

它会先执行add()方法,再去执行main()方法,但栈都是有内存空间的,当调用的方法多的话,内存空间就会溢出。

例如,上面的demo我们是递归调用,结果就栈内存溢出。发生Exception in thread "main" java.lang.StackOverflowError,这是一个error错误。

 

堆Heap:

堆结构:

新生代+老年代+永久代(1.7之前)

新生代+老年代+元空间(1.8)

元空间与永久代之间最大的区别在于:

永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
 
新生代:
  新生区是类的诞生、成长、消亡的区域,- 一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:
伊甸区(Eden space) 和幸存者区(Survivor pace), 所有的类都是在伊甸区被new出来的。幸存区有两个: 0区 (Survivor 0 space,也成为from区) 和1区(Survivor 1 space,也称为to区) 。
当伊甸园的空间用完时,程序又需要创建对象,
JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC/young Gc),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象
移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移那么这个时候将产生Ma iorGC (FullGC),进行养老区的内存清理。若养
老区执行了Full GC之后发现依然无法进行对象的保存,就会产生00M异常“OutOfMemoryError'
机的堆内存不够。原因有二:
(1) Java虛拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
 
堆中新生代和老年代的结构:

 MinorGC的过程(复制->清空->互换):

1: eden、 SurvivorFrom复制到SurvivorTo, 年龄+1

首先,当Eden区满的时候会触发第一 次GC,把还活着的对象拷贝到SurvivorFrom区, 当Eden

区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回

收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋

 值到老年代区),同时把这些对象的年龄+1

2: 清空eden、 SurvivorFrom

然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

3: SurvivorTo和SurvivorFrom互换

最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一 次GC时的SurvivorFrom区。 部

分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold

决定,这个参数默认是15),最终如果还是存活,就存入到老年代

 

性能调优:

 

 

 参数简介:

-Xms:设置初始分配大小,默认为物理内存的“1/6”

-Xmx:最大分配内存,默认为物理内存的“1/4”

-XX:+PrintGcDetails,输出详细的Gc处理日志

idea可以在项目Vm options设置这些参数:

 

 demo:可以获取到虚拟机的一些信息。

      //处理器核数
       System.out.println(Runtime.getRuntime().availableProcessors());
       //java虚拟机器使用的最大内存量
         Long memroyMax =  Runtime.getRuntime().maxMemory();
         //java虚拟机的内存总量
         Long memroyTotal =  Runtime.getRuntime().totalMemory();
        System.out.println("memroyMax"+memroyMax/(double)1024/1024+"M,  \nmemroyTotal "+memroyTotal/(double)1024/1024+"M");

 

这样我们就可以写一个堆内存溢出(OOM)的demo

public class HeapDemo {
    public static void main(String[] args) {
       String str = "yang";
       while (true){
            str += str+ new Random().nextInt(888888888)+new Random().nextInt(9999999);
       }
    }
}

打印信息[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space这是一个Error错误。

 

Gc日志:

在上面demo中,如果我们设置了-XX:+PrintGCDetails参数还可以看到Gc日志的详细信息:


[GC (Allocation Failure) [PSYoungGen: 2040K->504K(2560K)] 2040K->743K(9728K), 0.0036043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2331K->483K(2560K)] 2570K->1185K(9728K), 0.0038536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2391K->360K(2560K)] 4338K->2619K(9728K), 0.0142528 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1645K->360K(2560K)] 7642K->6980K(9728K), 0.0059927 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 360K->0K(2560K)] [ParOldGen: 6619K->3805K(7168K)] 6980K->3805K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0141733 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1326K->32K(2560K)] 6378K->6329K(9728K), 0.0007138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 6297K->3182K(7168K)] 6329K->3182K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0152305 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 40K->0K(1536K)] 5714K->5674K(8704K), 0.0006500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5674K->4428K(7168K)] 5674K->4428K(8704K), [Metaspace: 3468K->3468K(1056768K)], 0.0050228 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4428K->4428K(9216K), 0.0005205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4428K->4408K(7168K)] 4428K->4408K(9216K), [Metaspace: 3468K->3468K(1056768K)], 0.0092480 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

YoungGc参数解析:

 

 

Full Gc:

 

 可以使用java自带的jvisualvm进行监控,在应用位于bin/jvisualvm.exe

 

Minor GC和Full GC的区别:

JVM在进行GC时, 并非每次都对上面三个内存区域一 起回收的,大部分时候回收的都是指新生代。

因此GC按照回收的区域又分了两种类型,一种是普通GC (minor GC   or young GC),一种是全局GC (major GC or Full GC)

普通GC (minor GC) :只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以

Minor GC非常频繁,- -般回收速度也比较快。

全局GC (major GC or Full GC) :指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少- -次的Minor GC (但.

并不是绝对的)。Major GC的速度- -般要 比Minor GC慢上10倍以上

 

GC收集算法:

  • 复制算法
  • 标记清除
  • 标记压缩

复制算法:HotSpot JVM把年轻代分为S三部分: 1 个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,-般情况下,新创建的

对象都会被分配到Eden区(- -些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在

Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一-定程度时,就会被移动到年老代中。因为年轻代中的对象

基本都是朝生夕死的(90%以上:), 所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只

用其中一块,当这一块内存用完,就将还活着的对象复制到另外-块上面。

特点:复制算法不会产生内存碎片,但会占用空间。用于新生代

 

标记清除:算法分成标记和清除两个阶段,先标记出要回收的对象,然后统-回收这些对象。

特点:不会占用额外空间,但会两次扫描,耗时,容易产生碎片,用于老年代

示意图:

 

 

 

标记压缩:和标记清除一样,这里还多了一步压缩

特点:不会产生碎片,但是会耗时,标记以后还要整理存活对象的引用地址,用于老年代。

 

三者对比:

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不- -定如此)。

内存整齐度:复制算法=标记整理算法>标记清除算法。

内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是效率高德,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法

相对来少占用内存,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,标记压缩又比标记/清除多了一个整理内存的过程。

 

待更。。。

 

 

posted @ 2019-11-24 22:20  来一杯可乐  阅读(203)  评论(0编辑  收藏  举报