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详细的使用
)]
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关键字可以保证线程的可见性