一、两个字符编码的参数

javac和java是JDK自带的工具,其中javac是编译工具,java工具启动Java虚拟机并执行java程序。这两个工具都带有设置字符编码的选项。本文讨论字符编码选项的使用场景,和出现乱码的原因。

javac 的字符编码选项

javac -encoding CharSet XXXX.java   //CharSet为XXXX.java文件的字符编码。

javac编译器根据-encoding后跟随的字符编码,解析.java文件。-encoding不设置的时候,使用系统默认字符集解析.java文件。Windows的默认字符集是GBK。

无论之前的.java文件采用什么编码,编译后的.class文件都使用UTF-8编码。

如果-encoding指定的字符编码与.java文件的字符编码不一致,不一定编译失败,会给后续乱码埋下隐患。

JVM的字符编码选项

java -Dfile.encoding= CharSet XXXX //XXXX为class文件,CharSet是本地设备支持的字符集。

-Dfile.encoding并不是设置JVM虚拟机内存字符编码的。JVM虚拟机内存的字符编码是无法设置的,都是UTF-16,这个参数设置的是JVM输入流、输出流默认使用的编码/解码方式。

  • 当我们不手动设置JVM参数-Dfile.encoding时,系统默认字符集则取决于语言环境和底层操作系统(Windows的CMD下是GBK,Linux下则跟设置的语言环境有关)
  • 当我们手动设置JVM参数-Dfile.encoding=xxx,如果xxx是不支持的字符集,则默认使用’UTF-8’编码。

也可以通过Charset.defaultCharset()返回此 JVM的charset。默认 charset 在虚拟机启动时决定,通常根据语言环境和底层操作系统的 charset 来确定。

Charset.defaultcharset()指的是JVM输入流、输出流默认使用的编码/解码方式。

public static Charset defaultCharset() {if (defaultCharset == null) {synchronized (Charset.class) {String csn = AccessController.doPrivileged(new GetPropertyAction("file.encoding"));Charset cs = lookup(csn);if (cs != null)defaultCharset = cs;elsedefaultCharset = forName("UTF-8");}}return defaultCharset;
}

看到上面说可以通过设置系统属性file.encoding来设置默认字符集,那么有些朋友就想通过在运行时,通过System.setProperty("file.encoding", "GBK");来动态改变字符集,虽然可以通过System.setProperty("file.encoding","GBK")修改属性值,但仅仅是修改了file.encoding这个属性值,并不会影响Charset.defaultCharset()。因为JVM启动时就已经设置了Charset.defaultcharset()。

我们可以验证一下Windows环境下的默认字符集

import java.nio.charset.Charset;
public class CharsetTest {public static void main(String[] args) {System.out.println(Charset.defaultCharset());}
}

注意要使用命令行的方式编译运行

javac CharsetTest.java
java CharsetTest

为什么不在idea直接测试呢?

因为idea可以设置字符集,具体如图

你设置的字符集不同,结果自然不一样。

这里再说明一下   sun.jnu.encoding 和 file.encoding 的区别

sun.jnu.encoding 影响文件名的创建,而 file.encoding 则影响到文件内容。

二、Java在编译到执行过程涉及到的字符编码

这里其实并不是UTF-8,是一种modified UTF-8,这里就姑且认为是UTF-8。 

上图是.java文件从编译到执行的几个过程,这几个过程涉及到字符编码,具体解释如下:

①:A.java就是一个文本文件(以某种编码格式来存储:UTF-8、GBK、ISO-8859-1等),java编译器要解析这个文本文件并编译生成.class文件。而要想解析它,就必须知道它的编码方式。如果指定的编码与文件的编码不一致,要么编译失败,要么导致class文件中的字符乱码。

②:以不同编码方式编码的A.java经过Java编译器编译生成了同一个相同的A.class,编码格式为UTF-8。

③:Java虚拟机以二进制字节流的形式加载A.class,A.class中的字符编码是UTF-8,加载到JVM虚拟机内存中后,字符编码是UTF-16。

④:输出结果,如代码中指定了字符集,则按照代码中指定的字符集输出到设备中。如果代码中未指定字符集,则按照JVM启动时 -Dfile.encoding指定的字符集,如果没有指定-Dfile.encoding为默认的字符集输出到设备中。

从上图可以理解不管采用那种格式的源文件,只要正确告诉编译器源文件的编码格式,编译器就会得到正确的结果。同时只要告诉JVM正确的输出流需要的编码格式,JVM就可以返回正确编码格式的输出流。

那么要想不产生乱码就需要注意如下两个环节:

  1. 告诉编译器你java源文件的编码格式。
  2. 告诉jvm你显示或者构造字符串输出流时的希望的编码。

JVM中运行时数据都是使用UTF-16进行编码的。为什么JVM使用的是UTF-16,而不适用兼容性更好的UTF-8呢?

这是因为历史原因导致JVM运行时的数据使用UTF-16的编码。在Unicode最初诞生的时候,由于当时只有一个16位长的基本多文种平面,也就是只有0~65535的空间,两个字节刚好够用。这也解释了为什么Java中char是两个字节。

但是到了2001年,中国人大举入侵ISO和Unicode委员会,用已经颁布的GB18030-2000为基础,在Unicode 3.1标准中一口气加入了42711个CJK扩展字符,整个Unicode字符集一下增大到94205个字符,2个字节放不下了,UTF-16原来是变长编码的事也被人想起来了(中国人偷笑,GB系列从第一天起就是变长编码)。从此UTF-16就变得很尴尬,它一来存储空间利用率不高,二来又是个变长编码无法直接访问其中的码元。但是完全放弃UTF-16成本太高,所以现在JVM的运行时数据依然是UTF-16编码的。

由于成本问题不能放弃UTF-16,但是UTF-8的兼容性和流行程度,又使得JVM必须做点什么来使得其内部数据不会被编码方式影响,于是就有了这个modified UTF-8。

modified UTF-8

在通常用法下,Java程序语言在通过InputStreamReader和OutputStreamWriter读取和写入串的时候支持标准UTF-8。在Java内部,以及Class文件里存储的字符串是以一种叫modified UTF-8的格式存储的。

modified UTF-8 大致和 UTF-8 编码相同,但是有以下三个不同点:

  • 空字符(null character,U+0000)使用双字节的0xc0 0x80,而不是单字节的0x00。
  • 仅使用1字节,2字节和3字节格式,而UTF-8支持更多的字节
  • 基本多文种平面之外的补充字符以代理对(surrogate pairs)的形式表示

使用双字节空字符保证了在已编码字符串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志字符串结尾的。当已编码字符串放到这样的语言中处理,一个嵌入的空字符将把字符串一刀两断。

modified UTF-8在没有超过前三个字节表示的时候,和UTF-8编码方式一样,但是超过以后会以代理对(surrogate pairs)的形式表示。至于为什么要以代理对形式表示。这个是因为JVM的默认编码是UTF-16导致的。

一开始的Unicode只有一个可以用16位长完全表示的基本多文种平面,所以Java中的字符(char)为16位长,一个char可以存所有的字符。后来Unicode增加了很多辅助平面,两个字节存不下那些字符,但是为了向后兼容Java不可能更改它的基本语法实现,于是对于超过U+FFFF的字符 (从0x10000到0x10FFFF,称作扩展字符)就需要用两个16位长数据来表示。modified UTF-8由UTF-16格式的代理码元来代替原先的Unicode码元作为字符编码表的码元。

UTF-16格式的代理码元编码规则如下:

1)先将UTF-16补充码的数值减去0x10000;

2)将减掉之后的数值分为两个10比特的数值,假设高10位的值表示为Vh,低10位的值表示为Vl;

3)对于数值对中第一个16位的双字节来说,用0xD800加上高10位的值Vh;

4)对于数值对中第二个16位的双字节来说,用0xDC00加上低10位的值Vl。

数值对中的每一个16位的值,MUTF-8都会使用3个字节对其进行编码。由于每个UTF-16的补充字符都需要用两个16位的值对来表示,所以MUTF-8编码过后会使用6个字节。

这里可以计算一下,UTF-8要表示16位的数值需要三个字节,是这样表示的,1110xxxx 10xxxxxx 10xxxxxx,其中1和0是标识位,x代表具体的Unicode值.

JVM把UTF-16编码出来的16位长的数据(2字节,操作系统用8位长的数据,即1字节)作为最小单位进行信息交换。这样的话既不改变原来JVM中的编码规则,又减少了很多扩展字符从UTF-8转码到UTF-16时的运算量,是不是很刺激?正常从UTF-8转到UTF-16要先读出Unicode值,然后进行计算代理码元才能得到.

modified UTF-8保证了一个已编码字符串可以一次编为一个UTF-16码,而不是一次一个Unicode码位,使得所有的Unicode字符都能在Java上显示。

不过也不是没有缺点的,使用modified UTF-8进行解码解出来的是UTF-16编码编出来的数据,而UTF-16处理扩展字符需要两个16位长表示。也就是说,要用两个代理码元共同表示一个Unicode码位。原本使用UTF-8编码只需要最多4个字节就能存储一个Unicode码位,使用modified UTF-8编码后却需要6个字节来存储两个代码单元。

总结起来就是,modified UTF-8是对UTF-16的再编码,modified UTF-8和UTF-8是两种完全不同的编码

所以JVM无需解码UTF-16的数据,modified UTF-8代理码元会处理这个映射关系

参考文章

java jvm 编码_Javac和JVM的字符编码问题_weixin_39950764的博客-CSDN博客

Java 编译、运行过程中的字符编码 - 简书 (jianshu.com)

字符、编码和Java中的编码 - 简书 (jianshu.com)

MUTF-8(Modified UTF-8) - 腾讯云开发者社区-腾讯云 (tencent.com)

Java在编译到执行过程的编码问题相关推荐

  1. 执行引擎的工作过程、Java代码编译和执行的过程、解释器、JIT编译器

    执行引擎概述 执行引擎是Java虛拟机核心的组成部分之一. "虚拟机"是-一个相对于"物理机"的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接 ...

  2. JVM学习笔记(二)------Java代码编译和执行的整个过程

    Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: ● Java ...

  3. 60.Java 代码编译和执行的整个过程

    60.Java 代码编译和执行的整个过程 60.Java 代码编译和执行的整个过程 Java 代码编译是由 Java 源码编译器来完成,流程图如下所示: Java字节码的执行是由 JVM 执行引擎来完 ...

  4. JVM原理(Java代码编译和执行的整个过程+JVM内存管理及垃圾回收机制)

    转载注明出处: http://blog.csdn.net/cutesource/article/details/5904501 JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.e ...

  5. Java 代码编译和执行的整个过程

    一.简述 Java代码编译和执行的整个过程包含了三个重要的机制: ①Java 源码编译机制: ②类加载机制: ③类执行机制 二.Java 源码编译机制 Java 代码编译是由 Javac 编译器来完成 ...

  6. 机器语言,汇编语言,高级语言,c,java语言编译到运行过程

    机器语言:    计算机的硬件作为一种电路元件,它的输出和输入只能是有电或者没电,也就是所说的高电平和低电平,所以计算机传递的数据是由"0" 和"1"组成的二进 ...

  7. Java 版本、语言规范、API、JDK、IDE、Java 源程序编译、执行原理(跨平台性根本原因)、特殊字符用法、8 大数据类型小结

    文章目录 前言 一.三大版本类型 二.Java 语言规范 三.应用程序接口(API) 四.Java 开发工具包(JDK) 五.集成开发环境(IDE) 六.Java 运行环境(JRE) 七.Java 源 ...

  8. javascript 编译与执行过程

    Javascript预编译和执行过程 1. 在执行前会进行类似"预编译"的操作:首先会创建一个当前执行环境下的活动对象,并将那些用 var申明的变量设置为活动对象的属性,但是此时这 ...

  9. 初探 Go 的编译命令执行过程

    引言 Go 语言这两年在语言排行榜上的上升势头非常猛,Go 语言虽然是静态编译型语言,但是它却拥有脚本化的语法,支持多种编程范式(函数式和面向对象).Go 语言最最吸引人的地方可能是其原生支持并发编程 ...

最新文章

  1. Centos 7 解压文件
  2. 计算机硬件技术 教案,教案07-计算机硬件技术基础.doc
  3. python3教程-Python3 教程
  4. 算法--360面试:使用递归实现:a0=1,a1=1;a2=a0+a1;a3=a1+a2...以此类推,求a30
  5. 基于geopandas的空间数据分析——空间计算篇(下)
  6. C++const与#define 相比,有何优点?
  7. 洛谷P2497:基站建设(splay、斜率优化)
  8. 计蒜客---函数规律
  9. eclipse图标含义
  10. 让li不显示超出内容,显示... (编程方法和CSS方法)
  11. 凸优化第七章统计估计 7.3 最优检测器设计及假性检验
  12. 博弈论 | 三姬分金与囚徒困境
  13. android微信摇一摇(抽奖)
  14. 信息学奥赛一本通 铲雪车
  15. android 电池检测软件,AccuBattery手机电池损耗检测软件
  16. 如果面试时直接怒怼面试官
  17. ESP8266连接阿里云(二)烧录MQTT固件
  18. swoole的初步学习
  19. 亚马逊如何创业?身为小白的我适合创业亚马逊吗
  20. python实现输入一个字符串,输出每个字符的ASCLL码形成的列表

热门文章

  1. Oracle “ORA-00942: 表或视图不存在 “的原因和解决方法
  2. 算法总结——大整数乘法
  3. Java到底好不好学
  4. 了解Panda3D引擎的配置变量
  5. C# WPF MVVM框架搭建
  6. openldap + samba为openldap添加smb属性----群晖synology
  7. implicit declaration of function 警告解决方法 (函数的隐式说明)
  8. ..\Watch\alarm.c(149): error: #268: declaration may not appear after executable statement in block
  9. 限制guest账号访问硬盘权限
  10. Java键盘输入一个int数组