目录
(2)谈谈对JVM的理解?java8 虚拟机和之前的变化更新?
①类加载检查:虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的 符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必 须先执⾏相应的类加载过程。
②分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
③初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
⑤执行init方法:在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始,
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
①程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
②虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
③本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
④堆: java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
⑤方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
①OOM:
除了程序计数器,其他内存区域都有OOM的风险。
②栈溢出:
专业工具:内存快照分析工具:Jpro
- -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
- //-Xms 设置初始化内存分配大小,默认为电脑内存1/64
- //-Xmx 设置最大分配内存,默认为电脑内存1/4
- //-XX:PringGCDetails 打印GC垃圾回收信息
- //-XX:+HeapDumpOnOutOfMemoryError 打印OOM信息
虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
类加载过程如下
同问题2
堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace(元空间)),新生代包Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区。当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候,就会使用下一个Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存活时间一般比较久远。
标记清除,标记整理(压缩),复制算法,引用计数
具体见下文
新生成的对象首先放到年轻代Eden区,当 Eden空间满了,触发轻 GC,存活下来的对象移动到幸存0区,幸存0区满后触发执行轻 GC,幸存0区存活对象移动到幸存1区,这样保证了一段时间内总有一个幸存区为空。经过多次轻 GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发重 GC,GC期间会停止所有线程等待GC完成,所以对相应要求高的应用尽量减少发生Major GC,避免响应超时。
作用:加载 Class 文件
1. 虚拟机自带的加载器
2. 启动类(根)加载器
3. 扩展类加载器
4. 应用程序加载器
双亲委派机制:是为了保证安全
APP(应用程序加载器)----> EXC(扩展类加载器)----->BOOT(启动类(根)加载器)(最终执行)
双亲委派机制流程:
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境
沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
程序计数器: Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量Static、常量final、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
数据结构:先进后出;
队列:先进先出;
栈:占内存,主管程序运行,生命周期和线程同步;
程序结束,栈内存就释放,对于栈来说,不存在垃圾回收问题;
一旦线程结束,栈就Over。
栈中存放的内容:
栈运行的原理:栈帧
这就可以解释程序为啥从main方法运行了,因为main方法是第一个进栈的,会在栈底,其他的方法都在栈顶,每次运行完都会被弹出,直至所有方法都运行结束,main方法也会被弹出;如果方法互相调用,就会陷入死循环,导致栈溢出~
如果栈满了,会抛出StackOverFlowError
栈+堆+方法区:交互关系
一个对象实例化的过程: (最好能默写出来)
对象头分为两部分:Mark Word(运行时元数据)和类型指针。
Mark Word:用于存储对象自身的运行时数据信息,如上述的哈希码、GC分代年龄、线程持有的锁等。
类型指针:类型指针指向类的元数据,虚拟机通过这个指针确定该类在哪个变量中。
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放在堆中?类,方法,常量,变量,保存我们所有引用类型的真实数据。
堆内存中细分为三个区域:
GC垃圾回收,主要是在伊甸园区和养老区;
假设内存满了,OOM,堆内存不够-----》java.lang.OutOfMemoryError:java heap space
在 jdk8 以后,永久存储区改了个名字(元空间);
新生区
这个区域是常驻内存的,用来存放一些jdk自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收;关闭虚拟机的时候就会释放这个区域的内存。
一个启动类加载了大量的第三方jar包,或者tomcat部署了太多的应用,或者大量动态生成的反射类,不断地被加载,直到内存满,就会出现OOM。
jdk1.6以前:永久代,常量池是在方法区中
jdk1.7:永久代,慢慢的退化了,去永久代,常量池在堆中
jdk1.8:无永久代,常量池在元空间
逻辑上存在,物理上不存在
新生代 + 老年代 = 总空间(通过下面结果发现元空间确实是在物理上不存在)
默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存是 1/64。
遇到OOM该如何排除?
内存快照分析工具:Jpro作用:
- -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
- //-Xms 设置初始化内存分配大小,默认为电脑内存1/64
- //-Xmx 设置最大分配内存,默认为电脑内存1/4
- //-XX:PringGCDetails 打印GC垃圾回收信息
- //-XX:+HeapDumpOnOutOfMemoryError 打印OOM信息
修改代码堆存分配为8M,打印结果:
GC的作用区:垃圾回收大部分发生在堆和方法区(方法区本质也是堆)
JVM在进行GC时,并不是对这三个区域统一回收,大部分回收都是在新生代
GC分为两种类型:
给每个对象分配一个计数器,用一次加一次,清理计数为 0 的对象,下图中 C 就会被干掉~
扫描整个对象空间并对存活的对象标记,然后清除没有标记的对象。
是标记清除的再优化!
压缩:通过再次扫描,减少内存碎片,将空的坑位平移。
最后两张算法可以合并为一种算法——标记-清除-压缩算法
但是扫描时间长的缺点还是没有克服!
是缓存一致性协议,用于定义数据读写的规则。JMM定义了线程工作内存和主内存之间的抽象关系∶线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性的问题:volilate