赞
踩
请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新?
什么是OOM,什么是栈溢出StackOverFlowError? 怎么分析?
JVM的常用调优参数有哪些?
内存快照如何抓取,怎么分析Dump文件?
谈谈JVM中,类加载器你的认识?
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。
JDK 1.8 之前:
JDK 1.8 :
线程私有的:
线程共享的:
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
从上面的介绍中我们知道程序计数器主要有两个作用:
注意: 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java 虚拟机栈会出现两种错误:StackOverFlowError
和 OutOfMemoryError
。
StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常异常。扩展:那么方法/函数如何调用?
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
不管哪种返回方式都会导致栈帧被弹出
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:
类诞生和成长的地方,甚至死亡。
GC
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
public class Demo02 { public static void main(String[] args) { // 返回虚拟机示图使用的最大内存 long max = Runtime.getRuntime().maxMemory();//字节 1024*1024 // 返回Jvm的总内存 long total = Runtime.getRuntime().totalMemory(); System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)); System.out.println("total="+total+"字节\t"+(total/(double)1024/1024)); //默认情况下:分配的总内存是电脑内存的1/4;而初始化的内存:1/64 } } /* 执行结果: max=1881145344字节 1794.0 total=128974848字节 123.0 */
修改堆内存空间步骤:
选择Edit Configuration;配置vm如下:
/* max=1029177344字节 981.5 total=1029177344字节 981.5 Heap PSYoungGen(新生区) total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000) eden(eden区) space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000) from(幸存者区) space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000) to(幸存者区) space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000) ParOldGen(老年区) total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000) object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000) Metaspace(元空间) used 3250K, capacity 4496K, committed 4864K, reserved 1056768K class space used 351K, capacity 388K, committed 512K, reserved 1048576K 上面可以看出新生区+老年区=total(981) 可以看出来元空间不属于堆管理,是直接在本地内存中的,不在堆中分配内存空间。 所以元空间(永久区/方法区)是逻辑上存在,物理上不存在于堆。 */
/**
* 出现OOM错误,你怎么解决?
* 1.先尝试把堆内存的空间调大看结果:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
* 2.如果还错,分析内存,看一下那个地方出现了问题(专业工具)
*/
出现OOM错误的例子代码:
运行结果如下:
一开始是轻GC,4次之后发现清不了了,就来一次重Full GC,然后就又可以GC,然后GC不行了,就到Full GC,到最后Full GC也清不动了,堆内存满了,就报出OOM错误;所以这种情况可以选择调大堆内存空间,如果不报错了,说明就是堆内存空间不够导致的错误。如果调大都解决不了就可以使用工具分析错误原因。
在一个项目中,突然出现了OOM故障,那么该如何排除 研究为什么出错~
扩大内存如果没有报错了,那么就是内存太小了;
MAT, Jprofiler作用
Jprofile使用
package demo; import java.util.ArrayList; public class Demo03 { byte[] array=new byte[1*1024*1024]; public static void main(String[] args) { ArrayList<Demo03> objects = new ArrayList<>(); int count=0; try { while (true) { objects.add(new Demo03()); count = count + 1; } } catch (Exception e) { // e.printStackTrace(); System.out.println("count:" + count); e.printStackTrace(); } } }
可以看到try catch是捕获不到这个异常的
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(构造方法、接口定义)、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
下面是一些常用参数:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。
1、JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
2、JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代 。
3、JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
句柄
直接指针
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
图 JVM图解
图 .java->.class的变化
垃圾产生的位置:
作用:
下面的car实例的名字car1,car2,car3是在栈中的,他们都是对象引用,里面存放地址。但是最终他们是要指向堆中的数据,根据栈中地址去堆中找对应的数据。
图 类在经过Class Loader之后的变化
class文件被类加载器加载之后就进入了jvm中,类加载器加载初始化class,Class通过实例化就创建了一个个对象,实例要转化为Class可以通过getClass方法返回,要想变回加载时候,通过getClassLoader方法。
每new出来一个对象,JVM都会给他分配一个新的内存空间,创建对象的引用地址不一样,用来标识不同的内存空间,但是都是一个Class里面出来的。
系统加载class类型的文件主要有三步:加载–>连接–>初始化;连接又分为了三部分:验证–>准备–>解析;
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
public static int a=111
,那么a变量在准备阶段的初始值为0而不是111(初始阶段才会赋值)。特殊情况:比如给a变量加上了final关键字,那么准备阶段a的值就被赋值为111加载.class文件的方法:
1、从本地中直接加载
2、通过网络获取,典型场景:Web Applet
3、从zip压缩包中读取,成为日后jar,war格式的基础
4、运行时计算生成,使用最多的是:动态代理技术(proxy.newInstance() )
5、由其他文件生成,典型场景:JSP应用
6、从专有数据库中提取.class文件,比较少见
7、从加密文件中获取,典型的防Class文件被反编译的保护措施
JVM中内置了三个重要的ClassLoader,除了启动类加载器,其它类加载器均由Java实现且全部继承自java.lang.ClassLoader
:
//Car类: public class Car { String name; String type; double price; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getType() { return type; } public void setType(String type) { this.type = type; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } }
public class ClassLoaderTest { public static void main(String[] args) { Car car = new Car(); Class<? extends Car> aClass = car.getClass(); ClassLoader classLoader = aClass.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); System.out.println(classLoader.getParent().getParent()); //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //获取其上层:扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d //获取其上层:获取不到引导类加载器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader);//null //对于用户自定义类来说:默认使用系统类加载器进行加载 ClassLoader classLoader1 = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器(bootstrap)进行加载的。 ClassLoader classLoader2 = String.class.getClassLoader(); System.out.println(classLoader2);//null } } /* 执行结果: sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1b6d3586 null sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1b6d3586 null sun.misc.Launcher$AppClassLoader@18b4aac2 null */
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
工作原理
Java安全模型的核心就是Java沙箱(sandbox) , 什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是**将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,**通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。 沙箱主要限制系统资源访问,那系统资源包括什么? CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。 所有的Java程序运行都可以指定沙箱,可以定制安全策略。
JDK1.0安全模型:
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱Sandbox)机制。
图 JDK1.0安全模型
JDK1.1安全模型:
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。
图 JDK1.1安全模型
图 JDK1.2安全模型
图 JDK1.6安全模型
栈溢出:比如a方法中调用b方法,b方法中调用a,那么就会陷入死循环,会不断往栈里加东西,栈有一定内存空间,所以就会出现栈满溢出的情况。
例子
明明有main方法的存在,但是说找不到该方法,原因是他找到是原来的String类
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
new和newInstance()的区别
new是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过
而Class类下的newInstance()是弱类型,只能调用无参数构造方法,如果没有默认构造方法,就会抛出InstantiationException异常
【一、初识Java编译】
在开发我们的第一个Java程序之前,首先粗略的了解一下Java代码的编译和执行的整个过程。我们经常会看到Java工程里面有这两种后缀的文件:.java和.class。这两种文件分别代表Java的源代码和字节码文件,而其中的字节码文件就是java实现"Write Once,Run Anywhere"的关键。我们可以先来看看下面两幅图[1]。
Java编译器编译Java代码的流程如下:
JVM执行引擎完成Java字节码的执行:
从上两图可以看到,Java运行程序分两步走,第一步是源码编译成字节码,第二步是字节码编译成目标代码。这就和C、C++直接编译成与机器相关的目标代码不一样了。通过字节码这一中间环节,大家可以拿着编译成功的包发布到任一有JVM环境的机器上,再由JVM来实现到机器相关的最终目标代码的编译,从而做到"Write Once,Run Anywhere",而无需与具体运行平台绑定。所以我们通常所说的对代码进行编译,就是Java源代码编译成JVM字节码的过程。
【二、java与javac】
在上一节我们已经说过,%JAVA_HOME%\bin目录下有大量的Java工具可以使用,我们以后也要逐渐熟悉。那作为初接触java的我们,首先应该熟悉哪个呢?无疑是java与javac了。
IDE要用,可以极大的提高我们的生产效率,但是底层原理也一定要懂,不然都不知道IDE是怎么讲你的程序编译打包运行的,遇到问题的时候就不会解决了。很经常见到的一种情况是,有些人看到服务器上没有IDE就懵了,连怎么运行jar包程序都不会。
回归正传,我们就从java与javac入手,了解怎么在命令行中编译和运行一个java程序。首先,我们分别认识这两个命令是怎么用的。
【三、java命令】
作用:用于执行类或者执行jar文件。
在cmd控制台中输入java回车,我们可以得到java命令运行的格式如下:
【四、javac命令】
作用:用于编译.java文件。
在cmd控制台中输入javac回车,我们可以得到javac命令运行的格式如下:
它的具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。[本地库]
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
JVM中的程序计数寄存器(Program Counter
Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
为什么使用PC寄存器存储字节码指令地址?
为什么设置线程私有
CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
●Sun公司HotSpot Java Hotspot™ 64-Bit Server VM (build 25.181-b13,mixed mode) ●BEA JRockit ●IBM J9VM 我们学习都是: Hotspot
图 GC的作用区
JVM在进行GC时,并不是对这三个区域统一回收。 大部分时候,回收都是新生代~ 堆中的三个区:
GC两种类:
GC常见面试题目:
图 JVM内存模型和分区
假设对象A用了一次就标记1,用了2次就标记2,没用就标记0,给每个对象分配计数器,这个是一个成本,本身计数器就会有消耗;标记为0的就是没有用了,所以就将其清理。一般都用这种。
图 引用计数法 很少使用了
理解幸存区的from和to:比如Eden被GC后Eden为空了,活的进入幸存区,如果上面的是from下面是to,这时候,to中的空间足够,所以from中的存活下来的就把活的放到to中,这时候from就并为空了,因为谁空谁是to,所以一开始上面的是from就变成to了。如果在from还能活下来,就会进入到老年区。
图 复制算法大致图
图 复制算法图解
扫描这些对象,对活着的对象进行标记,对没有标记的对象进行清除。
多几次标记清除然后再压缩,这样可能效率高点。
思考:难道没有最优算法?
答案:没有最好的算法,只有最合适的算法。---->GC:分代收集算法
年轻代:
老年代:
JVM调优就可以在这个地方使用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。