一、内存结构
框架了解
(1)JVM位置
(2)JVM体系结构
简图:
详图:
1.程序计数器Program Counter Register(使用寄存器实现)
(1)作用:记住下一条jvm指令的执行地址
jvm指令->解释器->机器码->CPU执行
(2)特点
是线程私有的:每个线程都有自己的程序计数器,当线程切换运行时,程序计数器可以记住下一条待执行的jvm指令;
是不会存在内存溢出的。
2.虚拟机栈--线程运行时需要的内存空间,每个线程都需要一个
栈的数据结构特点:先进后出。
(1)每个栈由多个栈帧组成,每个线程只能有一个活动栈帧,对应当前正在执行的那个方法。
栈帧:每个方法运行时需要的内存(参数、局部变量,返回地址等)。
注意:垃圾回收不涉及栈内存;栈内存的分配(可自己定义,一般用默认)并不是越大越好,越大会影响线程数量,反而影响效率;如果方法内的局部变量没有逃离方法内的作用范围,则是线程安全的,如果局部变量引用了对象并逃离了作用范围,需要考虑线程安全。
(2)栈内存溢出的情况:
栈帧过多导致的栈内存溢出(递归无出口);栈帧过大导致的栈内存溢出(可能性很小)。
(3)线程运行诊断:
cpu占用过多:要学会定位线程,找出线程占用最多的位置。
程序运行很长时间无结果:可能陷入了死锁状态,也可以通过定位查询。
3.本地方法栈
本地方法:操作系统,c/c++语言编写。
java调用本地方法时本地方法运行的内存空间由本地方法栈提供。
本地方法接口(JNI):在执行引擎执行的时候通过本地方法接口加载本地方法库中的方法。
4.堆
通过new关键字创建对象都会使用堆内存。
特点:线程是共享的,堆中对象都需要考虑线程安全的问题;有垃圾回收机制。
堆内存溢出(outofmemory):对象一直被使用,一直无法被回收导致溢出。
堆内细分为三个区域:新生区(伊甸园区);养老区;永生区(jdk8以后改名为元空间)。
ps:gc垃圾回收主要是在伊甸园区和养老区。
元空间:逻辑上存在,物理上不存在,存储在本地磁盘内,不占用虚拟机内存。
5.方法区
存储类的数据,线程共享。所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量(static)、常量(final)、类信息(构造方法、接口定义)(Class)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
两种jvm的方法区占地不同,jvm1.6用永久代,jvm1.8用元空间。
stringtable面试题:
(1)string s1=s2+s3;其中s2和s3和拼接执行了new string()代码,是堆变量,同时,s2和s3也在常量池中;
(2)string s1=“ab”;是常量池中的变量,栈变量,和(1)不相等;
(3)string s1="a"+"b";和(2)相等;
(4)string s1=s2.intern();
1.8版本intern()作用是将该字符串对象放入串池中(如果串池中没有);
1.6版本ntern()作用是将该字符串对象复制一份,再放入串池中(如果串池中没有);
6.类加载器classloader
(1)定义:.java文件通过java编译器编译成.class文件,当需要使用某个类时,虚拟机将会加载它的".class"文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载。
(2)三种类加载器
- 启动(Bootstrap)类加载器:主要加载的是JVM自身需要的类;
- 扩展(Extension)类加载器:负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库;
- 系统(System)类加载器:负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径。
(3)双亲委派机制
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
双亲委派机制工作原理:为如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
双亲委派机制优点:
- 因为双亲委派是向上委托加载的,所以它可以确保类只被加载一次, 避免重复加载 。
- Java的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如java.lang.Integer,类加载器通过向上委托,两个Integer,那么最终被加载的应该是jdk的Integer类,而并非我们自定义的,这样就 避免了我们恶意篡改核心包的风险 。
7.沙箱安全机制
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
系统资源包括:CPU、内存、文件系统、网络。
组成沙箱的基本组件:
- 字节码校验器(bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 类裝载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
- 防止恶意代码去干涉善意的代码;
- 守护了被信任的类库边界;
- 将代码归入保护域,确定了代码可以进行哪些操作。
二、垃圾回收GC
特点:
- 无法手动垃圾回收,只能手动提醒,等待JVM自动回收
- GC的作用区在堆(Heap)和方法区中
- JVM进行GC时,并不是统一对这三区域(新生区,幸存区,老年区)统一回收,回收都是新生代;轻GC(普通GC)只针对于新生区,偶尔作用幸存区(在新生区满的情况下);重GC(全局GC)全局释放内存。
1.常见垃圾回收算法
-
引用计数算法
原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。 -
复制算法
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中,同时回收未使用的对象。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理。
优点:不会出现碎片化问题
缺点:需要两倍内存空间,浪费 -
标记-清除算法
此算法执行分两阶段。第一阶段从引用根节点开始标记所用存活的对象,第二阶段遍历整个堆,把未标记的对象清除。
优点:不会浪费内存空间
缺点:此算法需要暂停整个应用,同时,会产生内存碎片 -
标记-压缩算法
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有存活的对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
2.分代回收策略(回顾堆)
- 绝大多数刚刚被创建的对象会存放在Eden区
- 当Eden区第一次满的时候,会触发MinorGC(轻GC:使用复制算法)。首先将Eden区的垃圾对象回收清除,并将存活的对象复制到S0,此时S1是空的。
- 下一次Eden区满时,再执行一次垃圾回收,此次会将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1,此时S0变为空。
- 如此反复在S0和S1之间切换几次(默认15次)之后,还存活的对象将他们转移到老年代中。
- 当老年代满了时会触发FullGC(全GC:一般是标记压缩算法)
3.垃圾收集器
1.串行收集器(Serial)
Serial 收集器是 Hotspot 运行在 Client 模式下的默认新生代收集器, 它的特点是:单线程收集, 但它却简单而高效
2.并行收集器(ParNew)
ParNew
3.Parallel Scavenge 收集器
与 ParNew 类似, Parallel Scavenge 也是使用复制算法, 也是并行多线程收集器. 但与其
他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge 更关注系统吞吐量:
系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
4.CMS 收集器(Concurrent Mark Sweep)
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器),
基于”标记-清除”算法实现, 整个 GC 过程分为以下 4 个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark: GC Roots Tracing 过程)
重新标记(CMS remark)
并发清除(CMS concurrent sweep: 已死对象将会就地释放, 注意:此处没有压缩)
5.G1 收集器
G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
为什么要垃圾回收时要设计STW(stop the world)?
- 如果不设计STW,可能在垃圾回收时用户线程就执行完了,堆中的对象都失去了引用,全部变成了垃圾,索性就设计了STW,快速做完垃圾回收,再恢复用户线程运行。
三、JMM(java内存模型)
JMM(java内存模型)Java Memory Model,本身是一个抽象的概念,不是真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程读/写共享变量的副本。
JMM内存模型三大特性
1、原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是 原子性操作。
- 使用 synchronized 互斥锁来保证操作的原子性
2、可见性:可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile,synchronized,final都能 保证可见性。
- volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。
- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
- final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
3、有序性:虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。
- 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的命令。
- 重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
- 处理器在进行重排时必须考虑数据的依赖性,多线程环境线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。