JVM

JVM

什么是JVM

java代码的执行流程:

java程序的运行必须经过编译和运行两个阶段

java源文件编译后成为class文件

java虚拟机将编译好的字节码文件加载进内存,这个过程就是类加载

ClassLoader的种类

虚拟机自带的加载器

启动类加载器:BootStrap C++

扩展类加载器:Extension Java

应用程序类加载器:AppClassLoader,系统类加载器,加载当前应用的classpath的所有类

用户自定加载器

Java.lang.ClassLoader的子类,用户可以定制类加载器的方式

public class MyObject {
public static void main(String[] args) {
  //BootStrap
  Object object = new Object();
  System.out.println(object.getClass().getClassLoader());

  //AppClassLoader
  MyObject myObject = new MyObject();
  System.out.println(myObject.getClass().getClassLoader());
  //Ext
  System.out.println(myObject.getClass().getClassLoader().getParent());
  //BootStrap
  System.out.println(myObject.getClass().getClassLoader().getParent().getParent());
}
}

在这里插入图片描述

双亲委派

java程序在使用某一个类的时候,先去BootStrap跟加载器中去找

找到就使用,找不到,就去拓展类加载器中去找,找不到再往下应用类加载器中找

找得到就使用,找不到就是ClassNotFoundException

这么做的原因

我们自定义一个包java.lang,故意写一个String类

但是java中这个类是一定存在的

package java.lang;
public class String{
//这个类找的还是我们的启动类中的main,所以会有出错,没有main方法
	public static void main(String[] args){
		System.out.println("hello");
	}
}

为了保证自己提供的代码不污染Java中自带的代码

这就是双亲委派机制,保证的是沙箱安全

不管是哪个加载器来加载类,最终都委托给启动类加载器来完成

这样保证不同的类加载都得到的是同一个Object对象

Java的内存结构:

在这里插入图片描述

本地接口

Native是Java的一个关键字

public class JvmDemo {

public static void main(String[] args) {
  Thread t1 = new Thread();
  t1.start();
  t1.start();
}
}

线程不合法状态异常,start只能调用一次

在这里插入图片描述>

Java多线程只跟操作系统有关系

native专门开辟的借助与第三方,跟Java无关的,跟操作系统有关的资源库

native标志的方法运行是放在native本地方法栈中的,Java普通方法,运行时是在Java栈中

PC寄存器

PC寄存器也叫做程序计数器

就是一个指针,记录的是我这个方法运行完成后,下一个方法的地方

收集程序中的执行顺序,每一个PC寄存区存的下一个将要运行的方法的指针

方法区:

供各线程共享的运行时内存区域,存储每一个类的结构信息

运行时常量池,字段和方法数据,构造函数和普通方法的字节码内容

实例变量在堆内存中,和方法区无关

栈管理运行,堆管理存储

栈也叫作栈内存,主管程序的运行,线程创建的时候创建

生命周期跟着线程的生命周期,对于栈来说不存在垃圾回收

线程已结束栈就结束了,是线程私有的

八种基本类型,函数,对象的实例化引用都是在栈中,引用的指向是在堆中

方法就叫栈帧,栈帧中包含了

​ 本地变量:输入参数和输出参数,以及方法内的变量

​ 栈操作:记录入栈,出栈的顺序

​ 栈帧数据:类文件,方法

当一个方法A被调用的时候就产生了一个栈帧F1压入到栈中

A方法又调用了B方法,那么就会产生一个F2,压入到栈中

B方法调用C方法,也会产生一个栈帧F3,压入到栈中

执行完毕后先弹出F3,然后F2,最后F1

SOF(栈溢出)

public class JvmDemo {

public static void main(String[] args) {
  System.out.println("HelloStack");
  m1();
}
public static void m1(){
  m1();
}
}

在这里插入图片描述

OOM(内存溢出)

package com.qr.jvm;

import java.util.Random;

public class Oom {

    public static void main(String[] args) {
        test();
    }
    //死循环
    public  static void test(){
        String str = "ghjfsad";
        while (true){
            //创建一个随机数
            str += str +new Random().nextInt(56789324)+new Random().nextInt(322432142);
            // 返回字符串对象的
            str.intern();
        }
    }
}

在这里插入图片描述

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载读取类文件之后

会将类,方法,常量,变量放到堆内存当中,保存引用类型的真实信息

在这里插入图片描述

所有的类都是在伊甸区new出来的,当伊甸区的空间使用完后,就会触发YGC(新生代GC)

将伊甸区中不在被其他对象引用的对象进行销毁,然后将剩余的对象移动到幸存0区,如果幸存0区也满了

则再次触发一次GC,将存活的对象移动到幸存1区,如果幸存1区也满了,那么就会再次GC,如果还不行

那么就会移动到老年代,如果老年代也满了,那么将会触发一次Full GC,如果还不行,那么就会OOM

对象每经过一次GC就会age+1,如果是新生代中的对象age达到15之后,会移动到老年代

对象的生命周期

S0区和S1区每次GC完后,名称不是固定的,交换完成后,谁空谁是S1

只要产生GC伊甸区必须全部清空

Dden和S0和S1之间的比例为:8:1:1

新生代和养老代占用的堆空间比例额为:三分之一,和三分之二

永久代

​ 是一个常驻的内存区域,一般存放的是的拿来急用的东西,比如jdk中rt.jar包

​ 被装载到此区域的数据是不会被垃圾回收进行回收的

说说栈堆和方法区的理解

堆是垃圾回收的主要区域,new和构造器创建的对象都在堆空间当中

堆空间又可以分为新生代和老年代和永久代,jdk8之后永久代变为元数据区

永久代属于堆当中,需要进行垃圾回收,而元数据区则不用

新生代采用的是复制算法,老年代采用的是标记清除整理算法,

新生代分为Eden区,suv0和suv1区,这三个是进行复制算法的区域

方法区和堆都是线程共享的内存区域,存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;

程序中的字面量,如直接书写的100、”hello”和常量都是放在常量池中,常量池是方法区的一部分

栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,

而堆和常量池空间不足则会引发OutOfMemoryError

什么是JIT编译器

java程序一开始是通过解释器执行的,当jvm发现某段代码执行频繁的时候

会将代码认为是热点代码,jvm会将这些代码编译成本地相关的机器码

并且进行相关的优化, 完成这个任务的编译器就叫做即时编译器:JIT

对象的分配原则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

堆参数调优

参数调优

堆内存调优

-Xms:初始分配大小,默认为物理内存的六十四分之一

-Xmx:最大分配内存,默认为物理内存大四分之一

生产环境中最大大小和最小大小,必须一致,避免GC和应用程序争抢内存

避免峰值忽高忽地,产生停顿,从而产生异常

public class JvmDemo {

public static void main(String[] args) {
  //查看电脑核数
  System.out.println(Runtime.getRuntime().availableProcessors());
  //java虚拟机使用的最大内存量,字节
  System.out.println(Runtime.getRuntime().maxMemory());
  //java虚拟机中的内存总量,字节
  System.out.println(Runtime.getRuntime().totalMemory());
}
}

JVM Server模式和Client模式的区别

​ Client模式一般是32的操作系统

​ 2G 2C(核心)的32位是Server模式 现在的机器都找不到32位的了

在这里插入图片描述

mixed mode

​ java:解释型int 编译型 comp 混合模式 mixed(jvm决定到顶是使用解释型还是编译型)

标准的参数:

​ 版本几乎没有发生什么变化

**X:**非标准的

​ jdk7是有一个永久代的

​ jdk8是元空间

-X解释型

在这里插入图片描述

-X编译型

在这里插入图片描述

XX:

​ 对于JVM调优这个是最重要的

​ 分为两类

boolean类型:

​ -XX:[+/-] name

​ -XX:后面 + 表示的启用 -XX: 后面 - 表示禁用

public class JvmDemo {
public static void main(String[] args) throws Exception {
     Thread.sleep(Long.parseLong("2000"));
     System.out.println("jvm运行情况");
	}
}

在这里插入图片描述

​ 结果是一个**-PrintGCDetails**减号表示并没有开启

如何将参数传入:

在这里插入图片描述

​ 点击Edit进入编辑之后,打印GC的详细信息

在这里插入图片描述

​ 加上这么一段配置,就是打印我们GC的详细信息

​ jps -> pid

​ jinfo -flag name pid

非boolean类型:

​ -XX:name = value

​ 查看元空间代码参数的元空间

在这里插入图片描述

​ 修改代码参数的元空间为128m

在这里插入图片描述

​ 修改完后再次查看元空间

在这里插入图片描述

综合上面

​ 我们知道了调优参数的控制

JVM新生代和老年代的年龄控制:每次GC的年龄+1,默认经过15次

在这里插入图片描述

jinfo详细的使用

20210414010547866.png
)]

jinfo -flag查看堆的大小

jinfo -flag InitialHeapSize 2000

在这里插入图片描述

最大堆的大小:

jinfo -flag MaxHeapSize 2000

在这里插入图片描述

查看一堆的参数

在这里插入图片描述

PrintFlags系列

最初始化的参数值

java -XX:+PrintFlagsInitial

​ -XX:+PrintFlagsInitial

在这里插入图片描述

​ 下面还有很多

​ 参数多不好找怎么办

​ =表示是没有修改的

​ :=表示被修改的

在这里插入图片描述

可能会被改过的参数值

​ -XX:+PrintFlagsFinal

特殊的XX参数-Xmx -Xms

-Xms:堆的最小值 -XX:InitialHeapSize 初始化的

在这里插入图片描述

-Xmx:堆的最大值 -XX:MaxHeapSize

在这里插入图片描述

这两个是可以调整的

在这里插入图片描述

这样再次查看最大的和最小的就是一样的了生产中一般设置为一样

初始化堆的大小是内存的64分之一

最大大小是四分之一,生产上一般设置的最小等于最大

在这里插入图片描述

-Xss : -XX:ThreadStackSize

在这里插入图片描述

什么情况下会发生栈内存溢出

思路: 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo。

栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,
用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,
但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
参数 -Xss 去调整JVM栈的大小

运行时数据区:

jvm是支持我们的多线程的

PC计数器,存放当前正在执行的指令的地址

当我们的创建线程的时候,会创建一个jvm虚拟机的栈

异常信息就是从我们的栈里面打出来的

Heap是所有的jvm所共享的

在这里插入图片描述

Method Area

​ jvm是有一个方法区的,也是所有线程共享的

​ 编译我们的code,存储每一个class的结构信息

​ 包括常量,和池,他不是堆里面的

在这里插入图片描述

​ 堆存的信息:new出来的对象

​ MetaSpace class

JVM内存模型

S0和S1同一个时间点,只有一个是开启的

另外一个是空的,new出来的东西首先分到我们的eden区

如果eden区满了就进行垃圾回收,如果对象还活着就丢到S0区

每经过一次就是将eden和S0丢到我们的S1区,其他地方清掉

每次倒腾一次age就进行加1,等到15岁的时候,这个对象就进入我们的老年代

所以这一块是我们GC的关键

在这里插入图片描述

ccs:CompressedClassPoints

压缩类指针,如果开启ccs就是启用了我们的短指针

​ 短指针32,长指针64(默认)

CodeCache

​ JIT有关,跟有没编译本地代码.如果编译就在结构里面,没有编译就不在结构里面

Minor GC和Full GC的区别

Minor gc普通GC只针对于新生代的GC,指的是发生在新生代的垃圾收集

因为大多数的java对象存活都不高,MinorGC频繁,回收速度快

Full GC全局GC指发生在老年代的垃圾收集动作,出现了Major GC,至少会伴随一次的Minor GC

但也不是绝对的,Major GC一般会别Minor GC慢上10倍,因为GC的范围大

垃圾回收算法

引用计数法

如果一个对象开始有多个地方引用,每引用一处就进行加一

没人引用就减少1,,当减少到0的时候,说明没有对象引用了

就进行垃圾回收

缺点

​ 每次对对象赋值的时候,都要维护引用计数器,计数器本身有一定的消耗

​ 处理循环引用较难

复制算法

从跟集合开始从from中找存活对象,然后拷贝到to中

然后from和to交换身份,这种算法没有内存碎片,用在新生代

缺点

​ 比较浪费内存,因为只会占用一半的内存,分成了两个区,其中有一个区肯定是空的

标记清除

用在老年代

先标记垃圾,再进行回收,不浪费空间

缺点

​ 存在大量内存碎片,耗时,扫描两次

标记压缩(标记整理)

在标记清除的基础上,又进行了一次排序

没有碎片,也不浪费内存

缺点

​ 耗时长

分带收集

不同的代,使用不同的算法来完成相应的垃圾回收

JMM

JMM

java内存模型

package com.qr.jvm;

/**
 *  JMM:
 *      可见性,其他类修改了就要通知
 *
 */
public class JvmDemo {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        new Thread(() ->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myNumber.add();
            System.out.println(Thread.currentThread().getName()+"number is"+    myNumber.number);
        },"A").start();
        /**
         * 进来之后Thread处于睡眠,会走到这里
         * 但是Threa走完之后,main这里的值还是10,需要一种通知机制
         * 不然程序会卡在这里,A线程的修改对main线程不可见
         */
        while (myNumber.number == 10){

        }

        System.out.println("结束啦");
    }
}

class MyNumber{
    int number = 10;
    public void add(){
        this.number = 20;
    }
}

package com.qr.jvm;

/**
 *  JMM:
 *      可见性,其他类修改了就要通知
 *
 */
public class JvmDemo {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        new Thread(() ->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myNumber.add();
            System.out.println(Thread.currentThread().getName()+"number is"+    myNumber.number);
        },"A").start();
        /**
         * 进来之后Thread处于睡眠,会走到这里
         * 但是Threa走完之后,main这里的值还是10,需要一种通知机制
         * 不然程序会卡在这里,A线程的修改对main线程不可见
         */
        while (myNumber.number == 10){

        }

        System.out.println("结束啦");
    }
}

class MyNumber{
    volatile int number = 10;
    public void add(){
        this.number = 20;
    }
}

volatile关键字可以保证线程的可见性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值