JVM:虚拟机 java的运行环境
JVM运行在操作系统上
1.终端里执行javac命令把.java文件变成.class文件
2.把class类通过类加载器(Class Loader)加载到JVM虚拟机里才能运行。
JVM内存图
线程共享区:堆,方法区;随着虚拟机启动/关闭而创建/销毁
线程私有区:生命周期与线程相同,随着线程的启动/关闭而创建/销毁
JVM调优:基本都是指堆
类加载器
作用:把磁盘里的.class文件加载到JVM内存中
打印类加载器
打印其父类加载器
appClassLoader>>>extClassLoader>>>bootStapClassLoader
为什么是null?因为那是这个最父的类加载器是c写的,java调不到
bootStapClassLoader:D:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar
extClassLoader:D:\Program Files\Java\jdk1.8.0_111\jre\lib\ext
appClassLoader:D:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar\java.lang.ClassLoader
类加载器的双亲委派机制:
1.首先类加载器收到类加载请求
2.将这个请求向上委托给父类加载器,一直向上委托,直到启动类加载器
3.启动类加载器是否能够加载这个类,能加载就结束使用当前类加载器否则抛出异常,通知自类加载器进行加载
问题:如果我自己定义一个类叫String并且还创建了一个java.lang文件夹装它怎么办
运行时会抛出异常ClassNotFound
问题:如果我把我自己写的类编译的class放在其父类加载器的文件夹里能运行吗
能,但是!最后别动它,覆盖了以前叫这个名字的类就没了
沙箱安全机制
字节码校验器:即编译器
类加载器
类文件校验器
类管理器
native关键字:没听懂咋就能和别的语言交互通信呢?
方法区
方法区里都有什么:静态变量,常量,类信息,运行时的常量池(static,final,class模板...new的对象虽然在堆中,但是new一个类都会在常量池中有一个模板)
以上代码的内存分配图
栈
先进后出
和栈对应的是队列(先进先出)FIFO
示例代码内存图
生命周期:和线程同步
线程结束:栈内存释放了,所以对栈来说,不存在垃圾回收!
栈运行原理,栈帧 每个方法执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等
方法调用对应方法入栈对应栈帧创建
执行完成对应方法出栈对应栈帧销毁
本地方法栈和虚拟机栈的区别
本地方法栈:Native方法服务(别的语言封装的方法库啥的)
虚拟机栈:执行java方法服务
栈满了,StackOverError
堆(Heap)
一个JVM只有一个堆内存,堆内存大小可以调节
JVM调优通过这里来传参
堆中内存分三个区:新生区(Young/New),养老区(Old),永久区(Perm)
GC垃圾回收主要发生在Eden区
堆内存满了:OOM(OutOfMemoryError)
从GC回收的角度分为
新生区: 类生长或死亡的地方
Eden:new出来的,满了触发轻GC(GC)
幸存者区:分为from区和to区,都满了触发重GC(Full GC)
养老区:剩下的进入养老区
永久区:这个区域常驻内存,用来存放JDK自身class对象,存储JAVA运行时的环境或类信息,不存在垃圾回收,关闭JVM才会释放这个区域的内存
JDK1.6之前叫永久代,常量池在方法区里
JDK1.7还在堆中
但是在JDK1.8以后没有永久代,取而代之的是元空间(元空间只是逻辑上在堆里物理上不存在)
Tomcat部署了太多应用,大量动态生成反射类,不断地被加载,直到内存满,出现OOM
(但是其实这个图还是不对,JDK1.8叫元空间,不在虚拟机中而是使用本地内存)
默认情况下,分配的总内存是是电脑内存的1/4,初始化内存/总内存=1/4
打印堆内存情况 -XX:+PrintGCDetails
调参语句,把JVM初始化大小内存和JVM最大内存设置成一样:-Xms1024m -Xmx1024m
(截取自GC调优-XX:PrintGCDetails深度解析 - debug的勇士 - 博客园)
面试问题:万一遇到OOM
1.首先把堆内存空间变大
2.要是扩大了还提示OOM,检查代码有没有死循环代码
3.分析内存看哪里有问题
使用JPofiler工具分析OOM原因(9)
GC垃圾回收(10)
分代收集算法(这是我画的略有捡漏)
什么是复制算法?
创建(new)出来的对象存放在Eden区和From区中,当整两个区的内存到达一定的占用量后,会进行轻量级的垃圾回收(Minor GC),将存活下来的对象年龄+1,并将存活下来的对象复制到To区,此时From区和To区进行交换(区分From区和To区:谁空谁是To,并且From区和To区是不断交换的)。当一个对象的年龄达到15时,将此对象从新生代移动到老年代。优点:不产生内存碎片问题,能保持对象的完整性。缺点:因为要一致保持To区是空的,浪费了一定的内存空间。
(https://blog.csdn.net/AD_plus/article/details/99448182)
新生代主要用到复制算法,因为新生代对象存活率较低
好处:没有内存碎片
坏处:多了一半to区,永远是空的,很浪费空间
(摘抄自 Java虚拟机详解04----GC算法和种类【重要】 - 千古壹号 - 博客园)
什么是Mark-Sweep?
标记清除算法:Mark-Sweep
步骤一:标记:从根集合开始扫描,对存活的对象进行标记;
步骤二:清除:再次扫描整个内存空间,回收未被标记的对象,使用free-list记录可用区域。
缺点:两次扫描,耗时严重,会产生内存碎片。(内存碎片就是指很小的内存空间,不够给别人分配的,就只能浪费掉了)
优点:不需要占用额外空间。
什么是Mark-Compact?
标记整理算法:Mark-Compact标记/整理算法
引入:
如果在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选中这种算法。
概念:
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间。
- 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
- 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
上图中可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
- 但是,标记/整理算法唯一的缺点就是效率也不高,因为多了移动成本。
不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
所以一般应该先sweep几次,再compact,JVM调优就调这个,搜集了多少内存碎片之后再压缩!
复制算法>标记清除算法>标记整理算法。
内存整齐度:复制算法=标记整理算法>标记清理算法。
内存利用率:标记整理算法=复制算法>标记清理算法。
补充概念(吞吐量:用户程序时间/用户程序时间+GC时间)
JAVA内存模型JMM(Java Memory Model)
作用:MESI缓存一致性协议,用于定义数据读写规则
java多线程:容易出现读写异常
volatile原子性解决了共享对象可见性
synchronized同步性
java的主内存只有一个,每一个线程是copy出一个自己的工作台
JMM定义了线程和主内存的抽象关系
线程之间的共享变量存在主内存中,每个线程都有一个私有的本地内存