JVM
JVM的体系结构
-
JVM的位置
-
JVM的体系结构
- JVM的架构图
类加载器及双亲委派机制
类加载器
作用:加载Class文件
1.虚拟机自带的加载器
2.启动类(根)加载器
3.扩展类加载器
4.应用程序(系统类)加载器
类加载过程示意图:
例题
package com;
public class Car {
public static void main(String[] args) {
//类是模板,对象是具体的
Car car1 = new Car();
System.out.println(car1.hashCode());;
Class<? extends Car> aClass1 = car1.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader.getParent());//ExtClassLoader jre\lib\ext
System.out.println(classLoader.getParent().getParent());//null 1.不存在 2.java程序获取不到
System.out.println(classLoader);////AppClassLoader
}
}
双亲委派机制
运行过程:
- 类加载器收到类加载器请求
- 将这个请求向上委托给父类加载器完成,一直向上委托,直到启动加载器
- 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知加载器进行加载
- 重复步骤
双亲委派机制示意图
沙箱安全机制
概念
-
Java安全模型的核心就是Java沙箱(sandbox)
-
沙箱机制就是将Java代码限定只能在JVM虚拟机中特定的运行范围,并且严格限制代码对本地系统资源访问,通过这样的方式来保证对Java代码的有效隔离,防止对本地操作系统造成破坏。
沙箱组成
-
基本组件:字节码检验器、类装载器、存取控制器、安全管理器、安全软件包。
-
字节码校验器 bytecode verifier
确保java类文件遵循java语言规范,帮助程序实现内存保护。并不是所有类都经过字节码校验器,如核心类。
-
类加载器 class loader
双亲委派机制、安全校验等,防止恶意代码干涉。守护类库边界。
-
存取控制器 access controller
它可以控制核心API对操作系统的存取权限,控制策略可以有由用户指定。 -
安全管理器 security manager
它是核心API和系统间的主要接口,实现权限控制,比存取控制器优先级高。 -
安全软件包 secruity package
java.secruity下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性。包括:安全提供者、消息摘要、数字签名、加密、鉴别等。
沙箱安全机制模型
JDK1.0
JDK1 .0安全模型本地代码可以访问系统资源,远程代码无法访问系统资源,比如用户希望远程代码访问本地系统的文件时候,就无法实现。
JDK1.1
JDK1 .1 安全模型版本中,针对安全机制做了改进,增加了受信任安全策略,允许用户指定代码对本地资源的访问权限
JDK1 .2
JDK1 .2安全模型改进了安全机制,增加了代码签名。不论本地代码或是远程代码,统一按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,从而来实现差异化的代码执行权限控制。
最新的安全模型
目前最新的安全模型引入了域 (Domain) 的概念。JVM虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源系统进行交互,而每个应用域部分则通过系统域的部分代理来对各种需要的资源进行精细划分然后可以进行访问。JVM虚拟机中不同的受保护域 (Protected Domain)对应不一样的权限 (Permission)。存在于不同域中的类文件就拥有了它所包含应用域所有可访问资源之和。
Native、 方法区
Native
编写一个多线程启动类
public static void main(String[] args) {
new Thread(()->{
},"your thread name").start();
}
点进去查看start方法源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();//调用了一个start()方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
-
native:凡是带了native 关键字的,说明java的作用范围达不到,回去调用底层c语言的库
-
JNI: Java Native Interface(Java本地接口)
-
凡是带了native关键字的方法就会进入本地方法栈,其他就是Java栈
Native Interface(Java本地接口)
本地接口的作用是融合不同的编程语言为Java所用 ,他的初衷就是融合c/c++程序,Java在诞生的时候是C\C++横行的时候,想要立足,必须调用C\C++的程序,于是就是在内存中专门开辟了一块区域处理标记native的代码,它的具体做法是在 Native Method Stack 中登记native方法,在(Excution Eninge)执行引擎的时候加载Native Libraies
Natice Method Stack
他的具体做法是在 Native Method Stack 中登记native方法,在(Excution Engine)执行的时候加载Native Libraies[本地库]
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有,就是一个指针,指向方法区中的方法字节码(用来储存指向象一条指令地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area 方法区
方法区是被所有程序共享,所有字段和字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间
静态变量,变量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是,实例变量存在堆内存中,和方法无关
栈
栈的定义
栈(stack)是一种用于存储数据的简单数据结构。栈一个有序线性表,只能在表的一端(PS:栈顶)执行插人和删除操作。最后插人的元素将被第一个删除。所以,栈也称为后进先出(Last In First Out,LIFO)或先进后出(First In Last Out,FILO)线性表。
栈的几种主要基本操作:
void push(int data):入栈(将数据data插入到栈中)
int pop():出栈(删除并返回最后一个插入栈的元素)
int top():返回最后一个插入栈的元素,但不删除
int size():返回存储在栈中的元素个数
boolean isEmpty():返回栈是否是空栈
boolean isFull():返回是否是满栈
void Clear():清除整个栈
栈的基本方法:
public interface Stack<E> extends Iterable<E> {
//获取栈的size大小
public int size();
//判断栈是否为空
public boolean isEmpty();
//入栈 进栈一个元素 在线性表的表尾添加一个元素
public void push(E element);
//出栈 弹出一个元素 在线性表的表尾删除一个元素
public E pop();
//查看当前栈顶元素 并不是移除 查看线性表中最后一个元素
public E peek();
//对当前栈进行清空
public void clear();
}
栈的几种实现方式
- 基于简单数组的实现方式
- 基于动态数组的实现方式
- 基于链表的实现方式
- 基于队列的实现方式
Hotstopt ,堆
Hotspot
三种JVM : Sun公司 Hotspot、BEA 'JRocit'、IBM‘J9 VM’
堆
- Heap,一个JVM只有一个内存,堆内存的大小是可以调节的。
- 类加载器读取了类文件后,一般会把什么东西放在堆中?类,方法,常量,变量~,保存我们所有引用类型的真实对象
- 堆内存还要细分为三个区域:
新生区: Young/New
养老区:Old
永生区:Perm
注意:GC垃圾回收,主要在伊甸园区和养老区 - 假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:Java heap space
- 在JDK8以后,永久存储区改名(元空间)
堆内存还要细分为三个区域:
新生区: Young/New
-
类:诞生 和 成长的地方,甚至死亡;
-
伊甸园(Eden Space):所有的对象都是在 伊甸园去 new出来的
-
幸存区0区:
-
幸存区1区:
养老区:Old
-
如果清理过后幸存区域任然放不下对象,则重GC会将对象直接放在养老区中,如果养老区也不足以放下该对象,则会产生OOM。
-
经过研究,99%的对象都是临时对象!
永生区:Perm
- 这个区域常驻内存。用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息~,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存!
- 一个启动类,加载了大量的第三方jar包。tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载,直到内存满,就会出现OOM;
- jdk1.6 之前 : 永久代,常量池是在方法区;
- jdk1.7 : 永久代,但是慢慢退化了,去永久代,常量池在堆中
- jdk1.8 之后 :无永久代,常量池在元空间
堆内存调优
package com;
public class Demo {
public static void main(String[] args) {
//返回虚拟机试图使用最大内存
long max =Runtime.getRuntime().maxMemory();//字节 1024*1024
//返回JVM的总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+"字节\t"+(max/(double)1024/1024)+"MB");
System.out.println("total="+"字节\t"+(total/(double)1024/1024)+"MB");
//默认情况下:分配的总内存 是电脑内存的1/4,而初始化的内存:1/64
}
//OOM:
//1.尝试扩大内存看结果
//2.分析内存,看一下那个地方出现了问题(专业工具)
//-Xms1024m -Xmx1024m -XX:PrintGCDetails
}
调优:
使用JPfiler工具分析OOM
在一个项目中 ,突然出现OOM故障,如何排除~ 研究为什么出错
-
能够看到代码第几行出错:内存快照分析工具:MAT,Jprofiler
-
Dubug,一行行分析代码!
MAT,Jprofiler作用
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中数据
- 获得大的对象
- ....
package com;
import java.util.ArrayList;
//-Xms 设置初始化内存分配大小 /64
//-Xmx 设置最大分配内存 默认1/4
//-XX:+PrintGCDetails //打印GC垃圾回收
//-XX:+HeapDumpOutOfMemoryError //oom DUMP
// -Xms1m -Xmx8m -XX:+HeapDumpOutOfMemoryError
public class Demo2 {
byte[] array = new byte[1*1024*1024];//1m
public static void main(String[] args) {
ArrayList<Demo2> list = new ArrayList<>();
int count = 0;
try{
while (true){
list.add(new Demo2());//问题所在
count = count + 1;
}
}catch (Error e){
System.out.println("count:"+count);
e.printStackTrace();
}
}
}
GC:垃圾回收机制
JVM在进行GC时,并不是对这三个区域统一回收,大部分的时候,回收都是新生代
新生区 幸存区 老年区
GC两种类:轻GC(普通的GC) 重GC(全局GC)
GC题目:
- JVM的内存模型和分区,详细到每个区放什么?
由栈、堆、本地方法栈、方法区、程序计数器
- 栈:方法、对象的引用、8大基本数据类型
- 堆:实例的对象,成员变量(非static)
- 方法区:方法区是所有线程共享的,所有定义的方法的信息都保存在该区域,静态static修饰的变量和方法,final修饰的,类信息,常量池
- 程序计数器:存储指向下一条指令的地址
- 本地方法栈:它登记native方法,在(Execution Engine)执行引擎的时候加载Native Libraies(本地库)
- 堆里面的分区有哪些?说说他们的特点
Eden,from,to,老年代
- Eden:所有的对象都是在这new 出来的,它是对象出生,生长,也可能是死亡的地方。每次GC后,Eden重新变成空的,因为我们创建的大部分都是临时对象,所以对象的淘汰率还是很高的,适合GC的复制算法(将from的对象复制到to里,之后二者交换身份)。
- from和to: 经过GC清除后,幸存的对象会移动到to里面,它和from是动态交换的,to里面永远是空的
- 老年代:在幸存区经过一定的清除次数后的对象会被移到老年代,这个区域的对象就很难被杀死了,再使用复制算法就成本太高,适合GC的标记清除算法和标记压缩算法的结合
3、GC的算法有哪些?
复制算法 标记清除 标记压缩 引用计数法
4、轻GC和重GC分别在什么时候发生?
在Eden对象满了之后就会触发轻GC,当老年代里面存储的对象也满了之后会触发重GC
GC:引用计数法
每一个对象分一个计数器,空间消耗 , 因为它并不高效,所以jvm并不使用它
GC复制算法
复制算法,在标记清除的的基础上改进而来的,适用于对象存活度低的场景,如年轻代。
-
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
-
注意:
在复制算法下,两个survivor区(From和To区)同⼀时间会有⼀个满⼀个空,是交替的,并且空的总是To区,谁空谁是To。 -
当一个对象经历了15次GC,都还没有死,通过-XX:MaxTenuringThreshold=9999这个参数就可以设定进入老年代的时间。
-
如果一次Young GC后存活的对象过多不能进入Survivor区,那么就直接进入老年代。
-
每次GC都会将Eden活的对象移到幸存区中:一旦Eden区被GC后,就会是空的
优点:没有内存空间碎片
缺点:浪费内存空间,多了一般空间永远是To
GC标记算法
标记—清除
标记—清除算法是现代垃圾回收算法的思想基础。
这个算法将垃圾回收分为了两个阶段,标记阶段和清除阶段。
-
在标记阶段,首先通过根节点,标记所有从根节点开始可达对象,这时的可达对象代表还“有用”,而未标记的对象就是未被引用的垃圾对象。
-
在清除阶段,清除未被标记的垃圾对象。
优点:实现起来简单,不需要额外的空间 。
缺点: 逐渐产生被细化的分块,不久后就会导致无数的 小分块散布在堆的各处,并且在分配对象的时候还得先遍历那些内存块可以用。
标记—压缩
标记-压缩算法适合用于存活对象较多的场合,如老年代。
它在标记-清除算法的基础上做了一些优化,即在标记—清除算法后,将所有存活的对象压缩到内存的一端,然后清理这个边界外的所有空间
优点:堆利用效率高。
缺点: 清除算法中,清除阶段也要搜索整个堆,不过搜索 1 次就够了。但 GC 标记 - 压缩算法要搜索 3 次,这样就要花费约 3 倍的时间,这是一个相当巨大的缺陷,特别是堆越大,所消耗的成本也就越大
GC总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整理度: 复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
年轻代: 存活率低 复制算法
老年代:标记清除(内存碎片不是太多)+ 标记压缩混合 实现
标签:标记,对象,算法,GC,内存,JVM,加载 From: https://www.cnblogs.com/SXDMG/p/16883594.html